It is time to implement the last Order Management feature for the staff role.
The page will contain two lists of orders.
pending.preparing or rejected with the status cancelled.preparing and can be marked as ready for delivery or cancelled.Lists completed orders with the status ready or rejected with the status cancelled. In other words, these are fulfilled orders, and no further action can be taken.

Let's update the createStaffRole() method in RoleSeeder and add order.update permission to the staff role.
database/seeders/RoleSeeder.php
public function createStaffRole(){ $permissions = Permission::whereIn('name', [ 'order.update', ])->get(); $this->createRole(RoleName::STAFF, $permissions);}
To make things easier to test, let's add the staff() method to UserFactory so we can easily assign that role through Factory.
database/factories/UserFactory.php
public function staff(){ return $this->afterCreating(function (User $user) { $user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first()); });}
Then update the seedDemoRestaurants() method in DatabaseSeeder. This will create staff users for every restaurant seeded.
database/seeders/DatabaseSeeder.php
public function seedDemoRestaurants(){ $products = Product::factory(7); $categories = Category::factory(5)->has($products); $staffMember = User::factory()->staff(); $restaurant = Restaurant::factory()->has($categories)->has($staffMember, 'staff'); User::factory(50)->vendor()->has($restaurant)->create();}
Because the relationship and Factory Model are different and can't be matched automatically, we specify the relationship name as a second argument in the ->has(..., 'staff') method.
Add static methods toArray() and values() to the OrderStatus enum.
app/Enums/OrderStatus.php
public static function toArray(): array{ $cases = []; foreach (self::cases() as $case) { $cases[$case->name] = $case->value; } return $cases;} public static function values(): array{ return array_column(self::cases(), 'value');}
The toArray() method will help transform the enum into an array. We will pass it to the view to let the front end know our possible statuses. It will return the following array.
> OrderStatus::toArray()= [ "PENDING" => "pending", "PREPARING" => "preparing", "READY" => "ready", "CANCELLED" => "cancelled", ]
The values() method will return only values of the OrderStatus enum. It will become handy when we validate the order status in the update request.
> OrderStatus::values()= [ "pending", "preparing", "ready", "cancelled", ]
Now, add two Eloquent Local Scopes to the Order Model. Scopes help us filter orders by their status by calling a single current() or past() method.
app/Models/Order.php
public function scopeCurrent($query): void{ $query->whereIn('status', [ OrderStatus::PENDING, OrderStatus::PREPARING, ]);} public function scopePast($query): void{ $query->whereIn('status', [ OrderStatus::READY, OrderStatus::CANCELLED, ]);}
Create an OrderController for staff members.
php artisan make:controller Staff/OrderController
app/Http/Controllers/Staff/OrderController.php
namespace App\Http\Controllers\Staff; use App\Enums\OrderStatus;use App\Http\Controllers\Controller;use App\Http\Requests\Staff\UpdateOrderRequest;use App\Models\Order;use Inertia\Inertia;use Inertia\Response; class OrderController extends Controller{ public function index(): Response { $currentOrders = Order::current() ->with(['customer', 'products']) ->where('restaurant_id', auth()->user()->restaurant_id) ->latest() ->get(); $pastOrders = Order::past() ->with(['customer', 'products']) ->where('restaurant_id', auth()->user()->restaurant_id) ->latest() ->get(); return Inertia::render('Staff/Orders', [ 'current_orders' => $currentOrders, 'past_orders' => $pastOrders, 'order_status' => OrderStatus::toArray(), ]); } public function update(UpdateOrderRequest $request, $orderId) { $order = Order::where('restaurant_id', $request->user()->restaurant_id) ->findOrFail($orderId); $order->update($request->validated()); return back(); }}
In the index() method, we can use current() and past() methods on the Order Model to query orders that fall into constraints we defined in the scope.
The OrderStatus is transformed to an array using the toArray() method we just added to that enum class.
'order_status' => OrderStatus::toArray(),
We can re-use that enum like this order_status.PREPARING in Vue files.
Now let's talk about the update() method. Why didn't we resolve the Order Model like usual?
public function update(UpdateOrderRequest $request, Order $order) { $order->update($request->validated()); //...}
It would help if you carefully used Route Model Binding with tenancy features. Tenancy refers to architecture when a single instance serves multiple independent clients or tenants. In our case, the order can belong to a different restaurant.
The problem is that using the update(..., Order $order) method signature, ANY existing order in the database can be updated. We want everyone to refrain from messing with other restaurants' orders.
To fix this problem, we resolve the order we want to update by the id and the restaurant of the currently logged-in staff member.
public function update(UpdateOrderRequest $request, $orderId){ $order = Order::where('restaurant_id', $request->user()->restaurant_id) ->findOrFail($orderId); // ...}
Create a new Request class.
app/Http/Requests/Staff/UpdateOrderRequest.php
namespace App\Http\Requests\Staff; use App\Enums\OrderStatus;use Illuminate\Foundation\Http\FormRequest;use Illuminate\Support\Facades\Gate;use Illuminate\Validation\Rule; class UpdateOrderRequest extends FormRequest{ public function authorize(): bool { return Gate::allows('order.update'); } public function rules(): array { return [ 'status' => ['required', Rule::in(OrderStatus::values())], ]; }}
Create new routes file for staff.
routes/staff.php
use App\Http\Controllers\Staff\OrderController;use Illuminate\Support\Facades\Route; Route::group([ 'prefix' => 'staff', 'as' => 'staff.', 'middleware' => ['auth'],], function () { Route::resource('orders', OrderController::class);});
And include it in the web.php file.
routes/web.php
require __DIR__ . '/admin.php';require __DIR__ . '/vendor.php';require __DIR__ . '/customer.php';require __DIR__ . '/staff.php';
It is time to create the Orders page.
resources/js/Pages/Staff/Orders.vue
<script setup>import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'import { Head, useForm } from '@inertiajs/vue3'import PrimaryButton from '@/Components/PrimaryButton.vue'import SecondaryButton from '@/Components/SecondaryButton.vue'import DangerButton from '@/Components/DangerButton.vue' defineProps({ current_orders: { type: Array }, past_orders: { type: Array }, order_status: { type: Object }}) const form = useForm({ status: null}) const updateStatus = (order, status) => { form.status = status form.put(route('staff.orders.update', order), { preserveScroll: true })}</script> <template> <Head title="Restaurant Orders" /> <AuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 leading-tight">Restaurant Orders</h2> </template> <div class="py-12"> <!-- Current Orders --> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 mb-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900 overflow-x-scroll"> <header> <h2 class="text-lg font-medium text-gray-900">Current Orders</h2> </header> <table class="table mt-6"> <thead> <tr> <th>Order ID</th> <th>Items</th> <th>Customer</th> <th>Total</th> <th>Status</th> <th></th> </tr> </thead> <tbody class="align-top"> <tr v-for="order in current_orders" :key="order.id"> <td>{{ order.id }}</td> <td> <div v-for="product in order.products" :key="product.id" class="border-b"> {{ product.name }} </div> </td> <td>{{ order.customer.name }}</td> <td> <div class="whitespace-nowrap">{{ (order.total / 100).toFixed(2) }} €</div> </td> <td> <div class="badge" :class="{ 'badge-yellow': order.status === order_status.PENDING, 'badge-secondary': order.status === order_status.PREPARING }" > {{ order.status }} </div> </td> <td> <div class="flex flex-row gap-2 justify-end"> <SecondaryButton v-if="order.status === order_status.PENDING" @click="updateStatus(order, order_status.PREPARING)" class="btn-sm" type="button" > Prepare </SecondaryButton> <PrimaryButton v-if="order.status === order_status.PREPARING" @click="updateStatus(order, order_status.READY)" class="btn-sm" type="button" > Ready </PrimaryButton> <DangerButton @click="updateStatus(order, order_status.CANCELLED)" class="btn-sm" type="button" > Cancel </DangerButton> </div> </td> </tr> </tbody> </table> </div> </div> </div> <!-- Past Orders --> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8 mb-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900 overflow-x-scroll"> <header> <h2 class="text-lg font-medium text-gray-900">Past Orders</h2> </header> <table class="table mt-6"> <thead> <tr> <th>Order ID</th> <th>Items</th> <th>Customer</th> <th>Total</th> <th>Status</th> </tr> </thead> <tbody class="align-top"> <tr v-for="order in past_orders" :key="order.id"> <td>{{ order.id }}</td> <td> <div v-for="product in order.products" :key="product.id" class="border-b"> {{ product.name }} </div> </td> <td>{{ order.customer.name }}</td> <td> <div class="whitespace-nowrap">{{ (order.total / 100).toFixed(2) }} €</div> </td> <td> <div class="badge" :class="{ 'badge-primary': order.status === order_status.READY, 'badge-danger': order.status === order_status.CANCELLED }" > {{ order.status }} </div> </td> </tr> </tbody> </table> </div> </div> </div> </div> </AuthenticatedLayout></template>
Then insert the Restaurant Orders navigation link to the top bar.
resources/js/Layouts/AuthenticatedLayout.vue
<template> <!-- ... --> Staff Management </NavLink> <NavLink v-if="can('order.update')" :href="route('staff.orders.index')" :active="route().current('staff.orders.index')" > Restaurant Orders </NavLink> </div> </div> <!-- ... --></template>
If you want your buttons to be consistent with the primary button and add more badge styles, feel free to apply these changes.
resources/css/app.css
.badge-danger { @apply bg-danger-50 text-danger-700 border-danger-300;} .badge-secondary { @apply bg-gray-50 text-gray-700 border-gray-300;} .badge-yellow { @apply bg-yellow-50 text-yellow-700 border-yellow-300;}
resources/js/Components/DangerButton.vue
<template> <button class="btn btn-danger"> <slot /> </button></template>
resources/js/Components/SecondaryButton.vue
<template> <button :type="type" class="btn btn-secondary"> <slot /> </button></template>
Congratulations, you've completed the course!
The code is in the repository on GitHub.