The final feature related to the Teams is for the Clinic Owner to choose the current team if they have multiple clinics.
This choice will be visible in the top-right corner of the Laravel Breeze design, like this:
Now, the code.
First, the permission of WHO can change the team? We add a method to the Policy:
app/Policies/TeamPolicy.php
class TeamPolicy{ // ... public function changeTeam(User $user): bool { return $user->hasPermissionTo(Permission::SWITCH_TEAM); }}
Next is the Controller method, where we will use that changeTeam
immediately. We will add that method into the same TeamController
:
app/Http/Controllers/TeamController.php:
use App\Enums\Role;use Symfony\Component\HttpFoundation\Response; class TeamController extends Controller{ public function changeCurrentTeam(int $teamId) { Gate::authorize('changeTeam', Team::class); $team = auth()->user()->teams()->findOrFail($teamId); if (! auth()->user()->belongsToTeam($team)) { abort(Response::HTTP_FORBIDDEN); } // Change team auth()->user()->update(['current_team_id' => $team->id]); setPermissionsTeamId($team->id); auth()->user()->unsetRelation('roles')->unsetRelation('permissions'); return redirect(route('dashboard'), Response::HTTP_SEE_OTHER); }}
Next, we add the Route for that method.
routes/web.php
Route::middleware('auth')->group(function () { Route::resource('teams', TeamController::class)->only(['index', 'create', 'store']); Route::get('team/change/{teamId}', [TeamController::class, 'changeCurrentTeam']) ->name('team.change'); });
Finally, the front-end link to that Route. We just mimic the front-end code of the Profile dropdown menu.
resources/views/layouts/navigation.blade.php
<!-- Settings Dropdown --><div class="hidden sm:flex sm:items-center sm:ms-6"> @can(\App\Enums\Permission::SWITCH_TEAM) <x-dropdown align="right" width="48"> <x-slot name="trigger"> <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"> <div>{{ Auth::user()->currentTeam->name }}</div> <div class="ms-1"> <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg> </div> </button> </x-slot> <x-slot name="content"> @if (Auth::user()->teams()->count() > 1) @foreach (auth()->user()->teams as $team) <x-dropdown-link :href="route('team.change', $team->id)" @class(['font-bold' => auth()->user()->current_team_id == $team->id])> {{ $team->name }} </x-dropdown-link> @endforeach @else <span class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out"> No other teams </span> @endif </x-slot> </x-dropdown> @endcan <x-dropdown align="right" width="48"> ... (identical Breeze dropdown for "Edit Profile" and "Logout") </x-dropdown></div>
Now, the tests. We will add a few methods to the existing TeamTest.
tests/Feature/TeamTest.php:
use App\Models\User;use App\Models\Team;use App\Enums\Role as RoleEnum;use Spatie\Permission\Models\Role as RoleModel;use function Pest\Laravel\actingAs; it('allows clinic owner to change team', function () { $clinicOwner = User::factory()->clinicOwner()->create(); $secondTeam = Team::factory()->create(); $clinicOwner->teams()->attach($secondTeam->id, [ 'role_id' => RoleModel::where('name', RoleEnum::ClinicOwner->value)->first()->id, 'model_type' => $clinicOwner->getMorphClass(), ]); actingAs($clinicOwner) ->get(route('team.change', $secondTeam->id)); expect($clinicOwner->refresh()->current_team_id)->toBe($secondTeam->id);}); it('does not allow user to change team if user is not in the team', function () { $clinicOwner = User::factory()->clinicOwner()->create(); $secondTeam = Team::factory()->create(); actingAs($clinicOwner) ->get(route('team.change', $secondTeam->id)) ->assertNotFound(); expect($clinicOwner->refresh()->current_team_id)->toBe($clinicOwner->current_team_id);}); it('does not allow to change team for user without switch team permissions', function (User $user) { $team = Team::factory()->create(); actingAs($user) ->get(route('team.change', $team->id)) ->assertForbidden();})->with([ fn () => User::factory()->masterAdmin()->create(), fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->staff()->create(), fn () => User::factory()->doctor()->create(), fn () => User::factory()->patient()->create(),]);
The full result of the TeamTest:
While preparing this project, it was annoying to log out and log in with different users constantly. So, there's a Laravel package spatie/laravel-login-link that makes this task much easier!
The result is this: instead of entering email/password, we just need to click one of the links above the form:
To achieve that, we need to do this:
composer require spatie/laravel-login-linkphp artisan vendor:publish --tag="login-link-config"
And then we added a few links to the login page:
resources/views/auth/login.blade.php
<x-guest-layout> @env('local') <div class="space-y-2 mb-4"> <x-login-link email="master@admin.com" label="Login as master admin"/> <x-login-link email="owner@clinic.com" label="Login as clinic owner"/> <x-login-link email="admin@clinic.com" label="Login as clinic admin"/> <x-login-link email="staff@clinic.com" label="Login as staff"/> <x-login-link email="user@clinic.com" label="Login as patient"/> </div> @endenv ... The rest of the login form
And that's it!
Notice: Ensure these links are seen only in the local environment. Otherwise, it may be a security issue.
Great, we have the Team/Clinic management! Now, let's go to the User level.