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:
staff
is created and has no passwordThis is what our form will look like.
And the invitation email will look like this.
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
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);
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.
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(), ],
Now let's create a notification email for the final requirement.
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();}