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

Add Restaurant Staff Members

Before we can manage orders, we need the ability to add staff members to the system. This lesson will create a new view with a form for vendors to add staff members.

Our plan for this lesson is as follows:

  • vendor adds new staff member
  • new user with the role staff is created and has no password
  • newly created staff member gets an invitation email
  • staff member creates a password via the link and then can log in

This is what our form will look like.

Staff Management Add New Member

And the invitation email will look like this.

Staff Member Invitation Email


Create Role And Relationship For Staff Members

Add a new restaurant_id column to the users table. Staff members will belong to a restaurant.

php artisan make:migration add_restaurant_id_column_to_users_table

And update the Migration.

database/migrations/2023_07_20_143658_add_restaurant_id_column_to_users_table.php

public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignIdFor(Restaurant::class)
->after('id')
->nullable()
->constrained();
});
}

Add the user.create permission to the vendor role and create the staff role in the database by updating RoleSeeder.

database/seeders/RoleSeeder.php

public function run(): void
{
$this->createAdminRole();
$this->createVendorRole();
$this->createCustomerRole();
$this->createStaffRole();
}
 
// ...
 
protected function createVendorRole(): void
{
$permissions = Permission::query()
->orWhere('name', 'like', 'category.%')
->orWhere('name', 'like', 'product.%')
->orWhereIn('name', [
'user.create',
]);
 
$this->createRole(RoleName::VENDOR, $permissions->pluck('id'));
}
 
//
 
public function createStaffRole()
{
$this->createRole(RoleName::STAFF, collect());
}

Define the staff() relationship in the Restaurant Model.

app/Models/Restaurant.php

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Restaurant extends Model
{
// ...
 
public function staff(): HasMany
{
return $this->hasMany(User::class);
}
 
//...

Do not forget to run the migrate:fresh command.

php artisan migrate:fresh --seed

Controller And Routes

Let's create the StoreStaffMemberRequest FormRequest class and define validation rules.

app/Http/Requests/Vendor/StoreStaffMemberRequest.php

namespace App\Http\Requests\Vendor;
 
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
 
class StoreStaffMemberRequest extends FormRequest
{
public function authorize(): bool
{
return Gate::allows('user.create');
}
 
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:' . User::class],
];
}
}

Then create a new StaffMemberController for a vendor.

php artisan make:controller Vendor/StaffMemberController

We will have an index() method to display the management view and store() to add staff members to the system.

app/Http/Controllers/Vendor/StaffMemberController.php

namespace App\Http\Controllers\Vendor;
 
use App\Enums\RoleName;
use App\Http\Controllers\Controller;
use App\Http\Requests\Vendor\StoreStaffMemberRequest;
use App\Models\Role;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
 
class StaffMemberController extends Controller
{
public function index(): Response
{
return Inertia::render('Vendor/Staff/Show');
}
 
public function store(StoreStaffMemberRequest $request): RedirectResponse
{
$restaurant = $request->user()->restaurant;
$attributes = $request->validated();
 
DB::transaction(function () use ($attributes, $restaurant) {
$user = $restaurant->staff()->create([
'name' => $attributes['name'],
'email' => $attributes['email'],
'password' => '',
]);
 
$user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first());
});
 
return back();
}
}

Notice that we created a new staff member without a password because staff members will get an invitation email and activate their accounts by creating a password. Same way as we did with vendor users at the beginning of this course.

Add a new resource to the vendor.php routes file.

routes/vendor.php

use App\Http\Controllers\Vendor\CategoryController;
use App\Http\Controllers\Vendor\MenuController;
use App\Http\Controllers\Vendor\ProductController;
use App\Http\Controllers\Vendor\StaffMemberController;
use Illuminate\Support\Facades\Route;
 
// ...
 
Route::get('menu', [MenuController::class, 'index'])->name('menu');
Route::resource('categories', CategoryController::class);
Route::resource('products', ProductController::class);
Route::resource('staff-members', StaffMemberController::class);

Create Views

We will have a parent Show.vue page and will include a form to create a staff member as a component. We can see this implementation pattern on the Profile page by Laravel Breeze.

We are doing this because we plan to add another component to manage added staff members later. This way, we have a well-organized structure of a separate functionality on the same page.

Let's create a Show.vue page.

resources/js/Pages/Vendor/Staff/Show.vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import { Head } from '@inertiajs/vue3'
import AddStaffMemberForm from '@/Pages/Vendor/Staff/Partials/AddStaffMemberForm.vue'
</script>
 
<template>
<Head title="Staff Management" />
 
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Staff Management</h2>
</template>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div v-if="can('user.create')" class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<AddStaffMemberForm />
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

And then create the AddStaffMemberForm component.

resources/js/Pages/Vendor/Staff/Partials/AddStaffMemberForm.vue

<script setup>
import { useForm } from '@inertiajs/vue3'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import InputError from '@/Components/InputError.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
 
const form = useForm({
name: '',
email: ''
})
 
const addMember = () => {
form.post(route('vendor.staff-members.store'), {
preserveScroll: true,
onSuccess: () => form.reset()
})
}
</script>
 
<template>
<section class="max-w-xl">
<header>
<h2 class="text-lg font-medium text-gray-900">Add Restaurant Staff Member</h2>
 
<p class="mt-1 text-sm text-gray-600"></p>
</header>
 
<form @submit.prevent="addMember" class="mt-6 space-y-6">
<div class="form-group">
<InputLabel for="name" value="Name" />
 
<TextInput id="name" v-model="form.name" type="text" :disabled="form.processing" />
 
<InputError :message="form.errors.name" />
</div>
 
<div class="form-group">
<InputLabel for="email" value="Email" />
 
<TextInput
id="email"
v-model="form.email"
type="email"
autocomplete="email"
:disabled="form.processing"
/>
 
<InputError :message="form.errors.email" />
</div>
 
<div class="flex items-center gap-4">
<PrimaryButton :disabled="form.processing">Add</PrimaryButton>
 
<Transition
enter-from-class="opacity-0"
leave-to-class="opacity-0"
class="transition ease-in-out"
>
<p v-if="form.recentlySuccessful" class="text-sm text-gray-600">Added.</p>
</Transition>
</div>
</form>
</section>
</template>

Since we have no redirect to a different page after a successful post request, we clear the form details using the form.reset() method.

const addMember = () => {
form.post(route('vendor.staff-members.store'), {
preserveScroll: true,
onSuccess: () => form.reset()
})
}

We can use the form.recentlySuccessful property to display a message after a successful post request.

<template>
<!-- ... -->
<p v-if="form.recentlySuccessful" class="text-sm text-gray-600">Added.</p>
<!-- ... -->
</template>

And this is how it will look.

Staff Management Member Added

Then, add a <NavLink> to the Staff Management page in the AuthenticatedLayout. Since admin also has user.create permission we add additional check if authenticated user has vendor role. Admin should not be able to manage staff members.

resources/js/Layouts/AuthenticatedLayout.vue

<template>
<!-- ... -->
My Orders
</NavLink>
<NavLink
v-if="can('user.create') && $page.props.auth.is_vendor"
:href="route('vendor.staff-members.index')"
:active="route().current('vendor.staff-members.index')"
>
Staff Management
</NavLink>
</div>
</div>
<!-- ... -->
</template>

And add is_vendor property to the shared data.

app/Http/Middleware/HandleInertiaRequests.php

'auth' => [
'user' => $request->user(),
'permissions' => $request->user()?->permissions() ?? [],
'is_vendor' => $request->user()?->isVendor(),
],

Staff Member Email Notification

Now let's create a notification email for the final requirement.

Staff Member Invitation Email

The make:notification Artisan command allows us quickly scaffold the Notification.

php artisan make:notification RestaurantStaffInvitation --markdown=mail.restaurant.staff-invitation

Update the RestaurantStaffInvitation class as follows.

app/Notifications/RestaurantStaffInvitation.php

namespace App\Notifications;
 
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Password;
 
class RestaurantStaffInvitation extends Notification
{
use Queueable;
 
/**
* Create a new notification instance.
*/
public function __construct(public string $restaurantName)
{
}
 
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
 
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject(__('You have been invited to :restaurant staff members on :app', [
'restaurant' => $this->restaurantName,
'app' => config('app.name'),
]))
->markdown('mail.restaurant.staff-invitation', [
'setUrl' => $this->resetUrl($notifiable),
'restaurant' => $this->restaurantName,
'requestNewUrl' => route('password.request'),
]);
}
 
protected function resetUrl($notifiable)
{
return url(route('password.reset', [
'token' => Password::createToken($notifiable),
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
 
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

And the markdown template for the Notification.

resources/views/mail/restaurant/staff-invitation.blade.php

<x-mail::message>
# Staff Account created for {{ $restaurant }}
 
{{ __('This email is being sent to you as a notification that an account for :restaurant has been created for you at :app.', [
'restaurant' => $restaurant,
'app' => config('app.name'),
]) }}
 
{{ __('Please note that new accounts do not have a pre-set password. Therefore, it is necessary for you to set your password manually.') }}
 
<x-mail::button :url="$setUrl">
Set Password
</x-mail::button>
 
{{ __('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]) }}
 
{{ __('In the event that the link has expired, you have the option to request a new one.') }}
 
<x-mail::button :url="$requestNewUrl">
Request New Link
</x-mail::button>
 
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

Finally, update the store() method in the StaffMemberController to fire that Notification.

app/Http/Controllers/Vendor/StaffMemberController.php

use App\Notifications\RestaurantStaffInvitation;
 
// ...
 
public function store(StoreStaffMemberRequest $request): RedirectResponse
{
$restaurant = $request->user()->restaurant;
$attributes = $request->validated();
 
DB::transaction(function () use ($attributes, $restaurant) {
$member = DB::transaction(function () use ($attributes, $restaurant) {
$user = $restaurant->staff()->create([
'name' => $attributes['name'],
'email' => $attributes['email'],
'password' => '',
]);
 
$user->roles()->sync(Role::where('name', RoleName::STAFF->value)->first());
 
return $user;
});
 
$member->notify(new RestaurantStaffInvitation($restaurant->name));
 
return back();
}