diff --git a/app/Http/Controllers/Qms/BranchController.php b/app/Http/Controllers/Qms/BranchController.php index d064ebf..ababe14 100644 --- a/app/Http/Controllers/Qms/BranchController.php +++ b/app/Http/Controllers/Qms/BranchController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Qms; use App\Http\Controllers\Controller; +use App\Http\Controllers\Qms\Concerns\RequiresPlanFeature; use App\Http\Controllers\Qms\Concerns\ScopesToAccount; use App\Models\Branch; use App\Services\Qms\AuditLogger; @@ -13,11 +14,26 @@ use Illuminate\View\View; class BranchController extends Controller { + use RequiresPlanFeature; use ScopesToAccount; + private const FEATURE = 'branches'; + + private const UPGRADE_MESSAGE = 'Multi-branch management requires Queue Pro.'; + public function index(Request $request): View { $this->authorizeAbility($request, 'admin.branches.view'); + + if ($upgrade = $this->proFeatureUpgradeView( + $request, + self::FEATURE, + 'Branches', + 'Manage locations and sites with Queue Pro. Free plans include one branch created during setup.', + )) { + return $upgrade; + } + $organization = $this->organization($request); $branches = Branch::owned($this->ownerRef($request)) @@ -26,19 +42,39 @@ class BranchController extends Controller ->orderBy('name') ->get(); - return view('qms.admin.branches.index', compact('branches', 'organization')); + $heroStats = [ + 'total' => $branches->count(), + 'active' => $branches->where('is_active', true)->count(), + 'departments' => $branches->sum('departments_count'), + ]; + + return view('qms.admin.branches.index', compact('branches', 'organization', 'heroStats')); } 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 Queue Pro.', + )) { + return $upgrade; + } + return view('qms.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); $owner = $this->ownerRef($request); @@ -74,6 +110,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 Queue Pro.', + )) { + return $upgrade; + } + return view('qms.admin.branches.edit', compact('branch')); } @@ -82,6 +127,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'], diff --git a/app/Http/Controllers/Qms/Concerns/RequiresPlanFeature.php b/app/Http/Controllers/Qms/Concerns/RequiresPlanFeature.php new file mode 100644 index 0000000..11dd3fc --- /dev/null +++ b/app/Http/Controllers/Qms/Concerns/RequiresPlanFeature.php @@ -0,0 +1,53 @@ +organization($request); + $plans = app(PlanService::class); + + if ($plans->hasFeature($organization, $feature)) { + return null; + } + + return view('qms.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('qms.pro.index') + ->with('error', $message); + } +} diff --git a/app/Http/Controllers/Qms/MemberController.php b/app/Http/Controllers/Qms/MemberController.php index 9d1c073..6fd0d0b 100644 --- a/app/Http/Controllers/Qms/MemberController.php +++ b/app/Http/Controllers/Qms/MemberController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Qms; use App\Http\Controllers\Controller; +use App\Http\Controllers\Qms\Concerns\RequiresPlanFeature; use App\Http\Controllers\Qms\Concerns\ScopesToAccount; use App\Models\Branch; use App\Models\Member; @@ -14,11 +15,26 @@ use Illuminate\View\View; class MemberController extends Controller { + use RequiresPlanFeature; use ScopesToAccount; + private const FEATURE = 'team'; + + private const UPGRADE_MESSAGE = 'Team management requires Queue 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 Queue Pro.', + )) { + return $upgrade; + } + $organization = $this->organization($request); $members = Member::owned($this->ownerRef($request)) @@ -46,6 +62,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 colleagues and assign Queue roles with Pro.', + )) { + return $upgrade; + } + $organization = $this->organization($request); $branches = Branch::owned($this->ownerRef($request)) @@ -72,6 +98,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); @@ -122,6 +153,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.'); $memberId = $member->id; diff --git a/app/Http/Controllers/Qms/SettingsController.php b/app/Http/Controllers/Qms/SettingsController.php index 3b7e834..4c59e86 100644 --- a/app/Http/Controllers/Qms/SettingsController.php +++ b/app/Http/Controllers/Qms/SettingsController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Http\Controllers\Qms\Concerns\ScopesToAccount; use App\Models\Branch; use App\Services\Qms\AuditLogger; +use App\Services\Qms\PlanService; use App\Services\Qms\QmsPermissions; use App\Support\OrganizationBranding; use Illuminate\Http\RedirectResponse; @@ -16,11 +17,13 @@ class SettingsController extends Controller { use ScopesToAccount; - public function edit(Request $request): View + public function edit(Request $request, PlanService $plans): View { $this->authorizeAbility($request, 'settings.view'); $organization = $this->organization($request); - $canManage = app(QmsPermissions::class)->can($this->member($request), 'settings.manage'); + $member = $this->member($request); + $permissions = app(QmsPermissions::class); + $canManage = $permissions->can($member, 'settings.manage'); $branchCount = Branch::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) @@ -32,6 +35,15 @@ class SettingsController extends Controller 'branchCount' => $branchCount, 'appointmentModes' => config('qms.appointment_modes'), 'industries' => config('qms.industry_templates'), + 'hasPaidPlan' => $plans->hasPaidPlan($organization), + 'isPro' => $plans->isPro($organization), + 'isEnterprise' => $plans->isEnterprise($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'), + 'proPriceMinor' => $plans->proPricePerBranchMinor(), + 'activeBranchCount' => $plans->activeBranchCount($organization), ]); } diff --git a/app/Services/Qms/PlanService.php b/app/Services/Qms/PlanService.php index cd4eaab..d821d78 100644 --- a/app/Services/Qms/PlanService.php +++ b/app/Services/Qms/PlanService.php @@ -42,6 +42,13 @@ class PlanService return in_array($this->planKey($organization), ['pro', 'enterprise'], true); } + public function hasFeature(Organization $organization, string $feature): bool + { + $features = config('qms.plans.'.$this->planKey($organization).'.features', []); + + return in_array($feature, $features, true); + } + public function activeBranchCount(Organization $organization): int { return Branch::query() diff --git a/config/qms.php b/config/qms.php index 02f5516..76c60e8 100644 --- a/config/qms.php +++ b/config/qms.php @@ -180,6 +180,12 @@ return [ 'price_minor' => 0, 'max_branches' => 1, 'max_queues' => 5, + 'features' => [ + 'queues', + 'tickets', + 'counters', + 'displays', + ], ], 'pro' => [ 'label' => 'Pro', @@ -189,12 +195,35 @@ return [ ), 'max_branches' => null, 'max_queues' => null, + 'features' => [ + 'queues', + 'tickets', + 'counters', + 'displays', + 'branches', + 'team', + 'routing', + 'integrations', + 'reports', + ], ], 'enterprise' => [ 'label' => 'Enterprise', 'price_minor_per_branch' => (int) env('QUEUE_ENTERPRISE_PRICE_PER_BRANCH_MINOR', 449000), 'max_branches' => null, 'max_queues' => null, + 'features' => [ + 'queues', + 'tickets', + 'counters', + 'displays', + 'branches', + 'team', + 'routing', + 'integrations', + 'reports', + 'sla', + ], ], ], diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 8c4c767..96165af 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -51,24 +51,19 @@ } $adminNav = []; - if ($permissions->can($member, 'admin.branches.view')) { - $adminNav[] = ['name' => 'Branches', 'route' => route('qms.branches.index'), 'active' => request()->routeIs('qms.branches.*'), - 'icon' => '']; - } if ($permissions->can($member, 'admin.departments.view')) { $adminNav[] = ['name' => 'Departments', 'route' => route('qms.departments.index'), 'active' => request()->routeIs('qms.departments.*'), 'icon' => '']; } - if ($permissions->can($member, 'admin.members.view')) { - $adminNav[] = ['name' => 'Team', 'route' => route('qms.members.index'), 'active' => request()->routeIs('qms.members.*'), - 'icon' => '']; - } if ($permissions->can($member, 'audit.view')) { $adminNav[] = ['name' => 'Audit log', 'route' => route('qms.audit.index'), 'active' => request()->routeIs('qms.audit.*'), 'icon' => '']; } - $settingsActive = request()->routeIs('qms.settings.*') && ! request()->routeIs('qms.pro.*'); + $settingsActive = (request()->routeIs('qms.settings.*') + || request()->routeIs('qms.branches.*') + || request()->routeIs('qms.members.*')) + && ! request()->routeIs('qms.pro.*'); @endphp