From 5526a10342ec1ef3dcda1a6677f81c9dde105fa4 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Thu, 16 Jul 2026 08:13:42 +0000 Subject: [PATCH] Move Branches and Team into Settings as Pro features. Gate multi-branch and team management behind paid plans, nest their routes under Settings, and remove them from the sidebar Administration section. --- .../Frontdesk/BranchController.php | 49 ++++++++++++++- .../Frontdesk/BuildingController.php | 23 +++++++ .../Concerns/RequiresPlanFeature.php | 53 ++++++++++++++++ .../Frontdesk/MemberController.php | 35 +++++++++++ .../Frontdesk/ReceptionDeskController.php | 23 +++++++ .../Frontdesk/SettingsController.php | 6 ++ config/frontdesk.php | 4 ++ .../frontdesk/admin/branches/create.blade.php | 3 +- .../frontdesk/admin/branches/edit.blade.php | 3 +- .../frontdesk/admin/branches/index.blade.php | 3 +- .../frontdesk/admin/members/create.blade.php | 3 +- .../frontdesk/admin/members/index.blade.php | 3 +- resources/views/frontdesk/pro/index.blade.php | 5 +- .../views/frontdesk/settings/edit.blade.php | 41 +++++++++++- .../frontdesk/settings/pro-feature.blade.php | 21 +++++++ resources/views/partials/sidebar.blade.php | 32 +++------- routes/web.php | 35 ++++++----- tests/Feature/FrontdeskPhase2Test.php | 62 +++++++++++++++++-- tests/Feature/FrontdeskProTest.php | 23 +++++++ 19 files changed, 377 insertions(+), 50 deletions(-) create mode 100644 app/Http/Controllers/Frontdesk/Concerns/RequiresPlanFeature.php create mode 100644 resources/views/frontdesk/settings/pro-feature.blade.php diff --git a/app/Http/Controllers/Frontdesk/BranchController.php b/app/Http/Controllers/Frontdesk/BranchController.php index 8bedcf5..3702b3a 100644 --- a/app/Http/Controllers/Frontdesk/BranchController.php +++ b/app/Http/Controllers/Frontdesk/BranchController.php @@ -3,19 +3,36 @@ namespace App\Http\Controllers\Frontdesk; use App\Http\Controllers\Controller; +use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature; use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount; use App\Models\Branch; +use App\Services\Frontdesk\PlanService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; class BranchController extends Controller { + use RequiresPlanFeature; use ScopesToAccount; + private const FEATURE = 'branches'; + + private const UPGRADE_MESSAGE = 'Multi-branch management requires Frontdesk Pro.'; + public function index(Request $request): View { $this->authorizeAbility($request, 'admin.branches.view'); + + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Branches', + 'Manage locations, buildings, and reception desks with Frontdesk Pro. Free plans include one branch created during setup.', + )) { + return $upgrade; + } + $organization = $this->organization($request); $branches = Branch::owned($this->ownerRef($request)) @@ -36,14 +53,29 @@ class BranchController extends Controller public function create(Request $request): View { $this->authorizeAbility($request, 'admin.branches.manage'); + + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Branches', + 'Add and manage multiple locations with Frontdesk Pro.', + )) { + return $upgrade; + } + return view('frontdesk.admin.branches.create', ['organization' => $this->organization($request)]); } public function store(Request $request): RedirectResponse { $this->authorizeAbility($request, 'admin.branches.manage'); + + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $organization = $this->organization($request); - $plans = app(\App\Services\Frontdesk\PlanService::class); + $plans = app(PlanService::class); $currentCount = Branch::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) @@ -75,6 +107,15 @@ class BranchController extends Controller $this->authorizeAbility($request, 'admin.branches.manage'); $this->authorizeOwner($request, $branch); + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Branches', + 'Edit branch details with Frontdesk Pro.', + )) { + return $upgrade; + } + return view('frontdesk.admin.branches.edit', compact('branch')); } @@ -83,6 +124,10 @@ class BranchController extends Controller $this->authorizeAbility($request, 'admin.branches.manage'); $this->authorizeOwner($request, $branch); + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'code' => ['nullable', 'string', 'max:50'], @@ -93,7 +138,7 @@ class BranchController extends Controller $activating = $request->boolean('is_active') && ! $branch->is_active; if ($activating) { - $plans = app(\App\Services\Frontdesk\PlanService::class); + $plans = app(PlanService::class); $organization = $this->organization($request); if (! $plans->canActivateBranch($organization)) { return back()->withInput()->with('error', $plans->branchLimitMessage($organization)); diff --git a/app/Http/Controllers/Frontdesk/BuildingController.php b/app/Http/Controllers/Frontdesk/BuildingController.php index 7e483c1..c7b478b 100644 --- a/app/Http/Controllers/Frontdesk/BuildingController.php +++ b/app/Http/Controllers/Frontdesk/BuildingController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Frontdesk; use App\Http\Controllers\Controller; +use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature; use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount; use App\Models\Branch; use App\Models\Building; @@ -12,13 +13,27 @@ use Illuminate\View\View; class BuildingController extends Controller { + use RequiresPlanFeature; use ScopesToAccount; + private const FEATURE = 'branches'; + + private const UPGRADE_MESSAGE = 'Multi-branch management requires Frontdesk Pro.'; + public function index(Request $request, Branch $branch): View { $this->authorizeAbility($request, 'admin.branches.view'); $this->authorizeOwner($request, $branch); + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Branches', + 'Manage buildings and reception desks with Frontdesk Pro.', + )) { + return $upgrade; + } + $buildings = $branch->buildings()->withCount('receptionDesks')->orderBy('name')->get(); return view('frontdesk.admin.buildings.index', compact('branch', 'buildings')); @@ -29,6 +44,10 @@ class BuildingController extends Controller $this->authorizeAbility($request, 'admin.branches.manage'); $this->authorizeOwner($request, $branch); + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'floor_count' => ['nullable', 'string', 'max:20'], @@ -49,6 +68,10 @@ class BuildingController extends Controller $this->authorizeOwner($request, $building); abort_unless($building->branch_id === $branch->id, 404); + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $building->delete(); return back()->with('success', 'Building removed.'); diff --git a/app/Http/Controllers/Frontdesk/Concerns/RequiresPlanFeature.php b/app/Http/Controllers/Frontdesk/Concerns/RequiresPlanFeature.php new file mode 100644 index 0000000..25d6cf8 --- /dev/null +++ b/app/Http/Controllers/Frontdesk/Concerns/RequiresPlanFeature.php @@ -0,0 +1,53 @@ +organization($request); + $plans = app(PlanService::class); + + if ($plans->hasFeature($organization, $feature)) { + return null; + } + + return view('frontdesk.settings.pro-feature', [ + 'organization' => $organization, + 'title' => $title, + 'description' => $description, + 'proPriceMinor' => $plans->proPricePerBranchMinor(), + ]); + } + + /** + * For mutations: redirect free-plan orgs to the plans page. + */ + protected function denyWithoutFeature( + Request $request, + string $feature, + string $message, + ): ?RedirectResponse { + $organization = $this->organization($request); + if (app(PlanService::class)->hasFeature($organization, $feature)) { + return null; + } + + return redirect() + ->route('frontdesk.pro.index') + ->with('error', $message); + } +} diff --git a/app/Http/Controllers/Frontdesk/MemberController.php b/app/Http/Controllers/Frontdesk/MemberController.php index 1faa82c..bfe3305 100644 --- a/app/Http/Controllers/Frontdesk/MemberController.php +++ b/app/Http/Controllers/Frontdesk/MemberController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Frontdesk; use App\Http\Controllers\Controller; +use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature; use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount; use App\Models\Branch; use App\Models\Member; @@ -13,11 +14,26 @@ use Illuminate\View\View; class MemberController extends Controller { + use RequiresPlanFeature; use ScopesToAccount; + private const FEATURE = 'team'; + + private const UPGRADE_MESSAGE = 'Team management requires Frontdesk Pro.'; + public function index(Request $request): View { $this->authorizeAbility($request, 'admin.members.view'); + + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Team', + 'Invite colleagues, assign roles, and scope access by branch with Frontdesk Pro.', + )) { + return $upgrade; + } + $organization = $this->organization($request); $members = Member::owned($this->ownerRef($request)) @@ -45,6 +61,16 @@ class MemberController extends Controller public function create(Request $request, IdentityTeamClient $identity): View { $this->authorizeAbility($request, 'admin.members.manage'); + + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Team', + 'Invite team members with Frontdesk Pro.', + )) { + return $upgrade; + } + $organization = $this->organization($request); $branches = Branch::owned($this->ownerRef($request)) @@ -71,6 +97,11 @@ class MemberController extends Controller public function store(Request $request, IdentityTeamClient $identity): RedirectResponse { $this->authorizeAbility($request, 'admin.members.manage'); + + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $organization = $this->organization($request); $owner = $this->ownerRef($request); @@ -119,6 +150,10 @@ class MemberController extends Controller $this->authorizeAbility($request, 'admin.members.manage'); $this->authorizeOwner($request, $member); + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + abort_if($member->user_ref === $this->ownerRef($request), 422, 'You cannot remove yourself.'); $member->delete(); diff --git a/app/Http/Controllers/Frontdesk/ReceptionDeskController.php b/app/Http/Controllers/Frontdesk/ReceptionDeskController.php index 071093e..cf6b731 100644 --- a/app/Http/Controllers/Frontdesk/ReceptionDeskController.php +++ b/app/Http/Controllers/Frontdesk/ReceptionDeskController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Frontdesk; use App\Http\Controllers\Controller; +use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature; use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount; use App\Models\Building; use App\Models\ReceptionDesk; @@ -12,13 +13,27 @@ use Illuminate\View\View; class ReceptionDeskController extends Controller { + use RequiresPlanFeature; use ScopesToAccount; + private const FEATURE = 'branches'; + + private const UPGRADE_MESSAGE = 'Multi-branch management requires Frontdesk Pro.'; + public function index(Request $request, Building $building): View { $this->authorizeAbility($request, 'admin.desks.view'); $this->authorizeOwner($request, $building); + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Branches', + 'Manage reception desks with Frontdesk Pro.', + )) { + return $upgrade; + } + $desks = $building->receptionDesks()->orderBy('name')->get(); return view('frontdesk.admin.desks.index', compact('building', 'desks')); @@ -29,6 +44,10 @@ class ReceptionDeskController extends Controller $this->authorizeAbility($request, 'admin.branches.manage'); $this->authorizeOwner($request, $building); + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'location' => ['nullable', 'string', 'max:255'], @@ -50,6 +69,10 @@ class ReceptionDeskController extends Controller $this->authorizeOwner($request, $desk); abort_unless($desk->building_id === $building->id, 404); + if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) { + return $deny; + } + $desk->delete(); return back()->with('success', 'Reception desk removed.'); diff --git a/app/Http/Controllers/Frontdesk/SettingsController.php b/app/Http/Controllers/Frontdesk/SettingsController.php index 42e1606..80cd972 100644 --- a/app/Http/Controllers/Frontdesk/SettingsController.php +++ b/app/Http/Controllers/Frontdesk/SettingsController.php @@ -32,6 +32,8 @@ class SettingsController extends Controller $monthlyUsage = $usage->forOrganization($organization); $plan = $plans->plan($organization); $freeEmailAllowance = $plans->freeEmailsPerMonth($organization); + $member = $this->member($request); + $permissions = app(FrontdeskPermissions::class); return view('frontdesk.settings.edit', [ 'organization' => $organization, @@ -45,6 +47,10 @@ class SettingsController extends Controller 'isPro' => $plans->isPro($organization), 'isEnterprise' => $plans->isEnterprise($organization), 'hasPaidPlan' => $plans->hasPaidPlan($organization), + 'hasBranchesFeature' => $plans->hasFeature($organization, 'branches'), + 'hasTeamFeature' => $plans->hasFeature($organization, 'team'), + 'canViewBranches' => $permissions->can($member, 'admin.branches.view'), + 'canViewTeam' => $permissions->can($member, 'admin.members.view'), 'billedBranches' => $plans->billedBranches($organization), 'activeBranchCount' => $plans->activeBranchCount($organization), 'proPriceMinor' => $plans->proPricePerBranchMinor(), diff --git a/config/frontdesk.php b/config/frontdesk.php index e83c083..b8a828d 100644 --- a/config/frontdesk.php +++ b/config/frontdesk.php @@ -160,6 +160,8 @@ return [ 'hosts', 'badges', 'watchlist', + 'branches', + 'team', 'integrations', 'webhooks', 'scheduled_reports', @@ -178,6 +180,8 @@ return [ 'hosts', 'badges', 'watchlist', + 'branches', + 'team', 'integrations', 'webhooks', 'scheduled_reports', diff --git a/resources/views/frontdesk/admin/branches/create.blade.php b/resources/views/frontdesk/admin/branches/create.blade.php index ebacecf..1718a95 100644 --- a/resources/views/frontdesk/admin/branches/create.blade.php +++ b/resources/views/frontdesk/admin/branches/create.blade.php @@ -1,6 +1,7 @@
-

Add branch

+ ← Branches +

Add branch

@csrf
diff --git a/resources/views/frontdesk/admin/branches/edit.blade.php b/resources/views/frontdesk/admin/branches/edit.blade.php index 3d9a625..ff6299b 100644 --- a/resources/views/frontdesk/admin/branches/edit.blade.php +++ b/resources/views/frontdesk/admin/branches/edit.blade.php @@ -1,6 +1,7 @@
-

Edit branch

+ ← Branches +

Edit branch

@csrf @method('PUT')
diff --git a/resources/views/frontdesk/admin/branches/index.blade.php b/resources/views/frontdesk/admin/branches/index.blade.php index 9165f4c..e6a0a3d 100644 --- a/resources/views/frontdesk/admin/branches/index.blade.php +++ b/resources/views/frontdesk/admin/branches/index.blade.php @@ -7,8 +7,9 @@
+ ← Settings
-

Invite team member

+ ← Team +

Invite team member

@csrf diff --git a/resources/views/frontdesk/admin/members/index.blade.php b/resources/views/frontdesk/admin/members/index.blade.php index 624e857..eaa621a 100644 --- a/resources/views/frontdesk/admin/members/index.blade.php +++ b/resources/views/frontdesk/admin/members/index.blade.php @@ -1,7 +1,8 @@
+ ← Settings

Free

GHS 0

-

1 branch · 1 kiosk

+

1 branch · 1 kiosk · owner only

  • Core check-in & badges
  • 100 host emails / month
  • Watchlist & hosts
  • +
  • Multi-branch & team (Pro)
@if ($planKey === 'free')

Current plan

@@ -97,6 +98,8 @@ = {{ $currency }} /mo