Back to Course |
Laravel Vue Inertia: Food Ordering Project Step-By-Step

Category/Product DB and "Menu" Item

Now let's move on to working with the functionality for the restaurant owner (vendor) to manage the restaurant menu consisting of Categories and Products.

By the end of this lesson, we will have this screen:


Category/Product Database Structure

Let's generate the structure:

  • Categories (Pizza, Pasta, etc.)
  • Products (Carbonara, Penne, etc.)

Each restaurant will have its own categories/products. So, in terms of relationships, category belongs to a restaurant and may have many products.

php artisan make:model Category -m
php artisan make:model Product -m

Category migrations:

use App\Models\Restaurant;
 
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Restaurant::class)->constrained()->cascadeOnDelete();
$table->string('name');
$table->timestamps();
});

app/Models/Category.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Category extends Model
{
protected $fillable = ['name'];
 
public function restaurant(): BelongsTo
{
return $this->belongsTo(Restaurant::class);
}
 
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}

Product migration:

use App\Models\Category;
 
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Category::class)->constrained()->cascadeOnDelete();
$table->string('name');
$table->unsignedInteger('price');
$table->timestamps();
});
}

app/Models/Product.php:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Product extends Model
{
protected $fillable = ['category_id', 'name', 'price'];
 
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}

Adding Category/Product to Permissions

Now, for managing categories and products, we must also fix the permission seeder and allow vendors to access those features.

database/seeders/PermissionSeeder.php:

class PermissionSeeder extends Seeder
{
public function run(): void
{
$actions = [
'viewAny',
'view',
 
// ...
];
 
$resources = [
'user',
'restaurant',
'category',
'product',
];
}
}

database/seeders/RoleSeeder.php:

class RoleSeeder extends Seeder
{
public function run(): void
{
$this->createAdminRole();
$this->createVendorRole();
}
 
protected function createVendorRole(): void
{
$permissions = Permission::query()
->orWhere('name', 'like', 'category.%')
->orWhere('name', 'like', 'product.%')
->pluck('id');
 
$this->createRole(RoleName::VENDOR, collect());
$this->createRole(RoleName::VENDOR, $permissions);
}
}

Now, we need to re-migrate the database with the new permissions. Luckily, we can afford to do that, as we haven't really added any significant information.

php artisan migrate:fresh --seed

As a result, our users with the vendor role will have permissions like category.viewAny or product.viewAny.


Menu item "Menu"

Now, let's create the new navigation item "Menu", available only for restaurant owners.

First, we create a Controller in its dedicated new namespace:

php artisan make:controller Vendor/MenuController

Then, we attach that Controller to the route. As with routes/admin.php, we separate the routes/vendor.php, too.

routes/vendor.php:

use App\Http\Controllers\Vendor\MenuController;
use Illuminate\Support\Facades\Route;
 
Route::group([
'prefix' => 'vendor',
'as' => 'vendor.',
'middleware' => ['auth'],
], function () {
Route::get('menu', [MenuController::class, 'index'])->name('menu');
});

routes/web.php:

// ...
 
require __DIR__.'/auth.php';
require __DIR__ . '/admin.php';
require __DIR__ . '/vendor.php';

Then, in the Controller itself, we do this:

  • Check the permission
  • Get the categories with products and pass that to the Inertia View

app/Http/Controllers/Vendor/MenuController.php:

namespace App\Http\Controllers\Vendor;
 
use App\Http\Controllers\Controller;
use App\Models\Category;
use Inertia\Inertia;
use Inertia\Response;
 
class MenuController extends Controller
{
public function index(): Response
{
$this->authorize('category.viewAny');
 
return Inertia::render('Vendor/Menu', [
'categories' => Category::query()
->where('restaurant_id', auth()->user()->restaurant->id)
->with('products')
->get(),
]);
}
}

Finally, the Vue file. For now, we will implement just the list of categories and products without add/edit buttons - we will tackle those CRUDs in the next lesson.

resources/js/Pages/Vendor/Menu.vue:

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head } from '@inertiajs/vue3'
 
defineProps({
categories: {
type: Array
}
})
</script>
 
<template>
<Head title="Restaurant Menu" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Restaurant Menu</h2>
</template>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 overflow-x-scroll flex flex-col gap-8">
<div v-for="category in categories" :key="category.id" class="flex flex-col gap-4">
<div class="flex justify-between">
<div class="">
<div class="text-2xl font-bold">{{ category.name }}</div>
</div>
<div class="flex gap-4 items-center">
Edit / Delete Category Buttons: Coming Soon
</div>
</div>
<div>
Add Product Button: Coming Soon
</div>
<div class="flex flex-col gap-6">
<div
v-for="product in category.products"
:key="product.id"
class="flex items-center justify-between pb-6 border-b gap-4"
>
<div class="flex flex-col">
<div class="font-bold">{{ product.name }}</div>
<div class="">{{ (product.price / 100).toFixed(2) }} &euro;</div>
</div>
<div class="flex gap-4">
Edit / Delete Product Buttons: Coming Soon
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

The structure is very similar to the Restaurants CRUD earlier, so there's not much to comment on here.

We have two v-for loops:

  • <div v-for="category in categories" :key="category.id"
  • And inside of that: <div v-for="product in category.products" :key="product.id"

Finally, let's add a menu item "Menu" on top of the main layout. With checking the permission, of course.

resources/js/Layouts/AuthenticatedLayout.vue:

<NavLink
v-if="can('restaurant.viewAny')"
:href="route('admin.restaurants.index')"
:active="route().current('admin.restaurants.index')">Restaurants
</NavLink>
<NavLink
v-if="can('product.viewAny') && can('category.viewAny')"
:href="route('vendor.menu')"
:active="route().current('vendor.menu')">Restaurant menu
</NavLink>

We log in as Vendor, and if we add a few DB records for categories/products (manually for now, via SQL client), the visual result is this: