Move Branches and Team into Settings as Pro features.
Deploy Ladill Frontdesk / deploy (push) Successful in 48s
Deploy Ladill Frontdesk / deploy (push) Successful in 48s
Gate multi-branch and team management behind paid plans, nest their routes under Settings, and remove them from the sidebar Administration section.
This commit is contained in:
@@ -3,19 +3,36 @@
|
|||||||
namespace App\Http\Controllers\Frontdesk;
|
namespace App\Http\Controllers\Frontdesk;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature;
|
||||||
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
||||||
use App\Models\Branch;
|
use App\Models\Branch;
|
||||||
|
use App\Services\Frontdesk\PlanService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class BranchController extends Controller
|
class BranchController extends Controller
|
||||||
{
|
{
|
||||||
|
use RequiresPlanFeature;
|
||||||
use ScopesToAccount;
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
private const FEATURE = 'branches';
|
||||||
|
|
||||||
|
private const UPGRADE_MESSAGE = 'Multi-branch management requires Frontdesk Pro.';
|
||||||
|
|
||||||
public function index(Request $request): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.branches.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);
|
$organization = $this->organization($request);
|
||||||
|
|
||||||
$branches = Branch::owned($this->ownerRef($request))
|
$branches = Branch::owned($this->ownerRef($request))
|
||||||
@@ -36,14 +53,29 @@ class BranchController extends Controller
|
|||||||
public function create(Request $request): View
|
public function create(Request $request): View
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
$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)]);
|
return view('frontdesk.admin.branches.create', ['organization' => $this->organization($request)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$organization = $this->organization($request);
|
$organization = $this->organization($request);
|
||||||
$plans = app(\App\Services\Frontdesk\PlanService::class);
|
$plans = app(PlanService::class);
|
||||||
|
|
||||||
$currentCount = Branch::owned($this->ownerRef($request))
|
$currentCount = Branch::owned($this->ownerRef($request))
|
||||||
->where('organization_id', $organization->id)
|
->where('organization_id', $organization->id)
|
||||||
@@ -75,6 +107,15 @@ class BranchController extends Controller
|
|||||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||||
$this->authorizeOwner($request, $branch);
|
$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'));
|
return view('frontdesk.admin.branches.edit', compact('branch'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +124,10 @@ class BranchController extends Controller
|
|||||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||||
$this->authorizeOwner($request, $branch);
|
$this->authorizeOwner($request, $branch);
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'code' => ['nullable', 'string', 'max:50'],
|
'code' => ['nullable', 'string', 'max:50'],
|
||||||
@@ -93,7 +138,7 @@ class BranchController extends Controller
|
|||||||
|
|
||||||
$activating = $request->boolean('is_active') && ! $branch->is_active;
|
$activating = $request->boolean('is_active') && ! $branch->is_active;
|
||||||
if ($activating) {
|
if ($activating) {
|
||||||
$plans = app(\App\Services\Frontdesk\PlanService::class);
|
$plans = app(PlanService::class);
|
||||||
$organization = $this->organization($request);
|
$organization = $this->organization($request);
|
||||||
if (! $plans->canActivateBranch($organization)) {
|
if (! $plans->canActivateBranch($organization)) {
|
||||||
return back()->withInput()->with('error', $plans->branchLimitMessage($organization));
|
return back()->withInput()->with('error', $plans->branchLimitMessage($organization));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Frontdesk;
|
namespace App\Http\Controllers\Frontdesk;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature;
|
||||||
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
||||||
use App\Models\Branch;
|
use App\Models\Branch;
|
||||||
use App\Models\Building;
|
use App\Models\Building;
|
||||||
@@ -12,13 +13,27 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class BuildingController extends Controller
|
class BuildingController extends Controller
|
||||||
{
|
{
|
||||||
|
use RequiresPlanFeature;
|
||||||
use ScopesToAccount;
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
private const FEATURE = 'branches';
|
||||||
|
|
||||||
|
private const UPGRADE_MESSAGE = 'Multi-branch management requires Frontdesk Pro.';
|
||||||
|
|
||||||
public function index(Request $request, Branch $branch): View
|
public function index(Request $request, Branch $branch): View
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.branches.view');
|
$this->authorizeAbility($request, 'admin.branches.view');
|
||||||
$this->authorizeOwner($request, $branch);
|
$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();
|
$buildings = $branch->buildings()->withCount('receptionDesks')->orderBy('name')->get();
|
||||||
|
|
||||||
return view('frontdesk.admin.buildings.index', compact('branch', 'buildings'));
|
return view('frontdesk.admin.buildings.index', compact('branch', 'buildings'));
|
||||||
@@ -29,6 +44,10 @@ class BuildingController extends Controller
|
|||||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||||
$this->authorizeOwner($request, $branch);
|
$this->authorizeOwner($request, $branch);
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'floor_count' => ['nullable', 'string', 'max:20'],
|
'floor_count' => ['nullable', 'string', 'max:20'],
|
||||||
@@ -49,6 +68,10 @@ class BuildingController extends Controller
|
|||||||
$this->authorizeOwner($request, $building);
|
$this->authorizeOwner($request, $building);
|
||||||
abort_unless($building->branch_id === $branch->id, 404);
|
abort_unless($building->branch_id === $branch->id, 404);
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$building->delete();
|
$building->delete();
|
||||||
|
|
||||||
return back()->with('success', 'Building removed.');
|
return back()->with('success', 'Building removed.');
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Frontdesk\Concerns;
|
||||||
|
|
||||||
|
use App\Services\Frontdesk\PlanService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
trait RequiresPlanFeature
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* For page views: return an upgrade screen when the org lacks the feature.
|
||||||
|
*/
|
||||||
|
protected function proFeatureUpgradeView(
|
||||||
|
Request $request,
|
||||||
|
string $feature,
|
||||||
|
string $title,
|
||||||
|
string $description,
|
||||||
|
): ?View {
|
||||||
|
$organization = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Frontdesk;
|
namespace App\Http\Controllers\Frontdesk;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature;
|
||||||
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
||||||
use App\Models\Branch;
|
use App\Models\Branch;
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
@@ -13,11 +14,26 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class MemberController extends Controller
|
class MemberController extends Controller
|
||||||
{
|
{
|
||||||
|
use RequiresPlanFeature;
|
||||||
use ScopesToAccount;
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
private const FEATURE = 'team';
|
||||||
|
|
||||||
|
private const UPGRADE_MESSAGE = 'Team management requires Frontdesk Pro.';
|
||||||
|
|
||||||
public function index(Request $request): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.members.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);
|
$organization = $this->organization($request);
|
||||||
|
|
||||||
$members = Member::owned($this->ownerRef($request))
|
$members = Member::owned($this->ownerRef($request))
|
||||||
@@ -45,6 +61,16 @@ class MemberController extends Controller
|
|||||||
public function create(Request $request, IdentityTeamClient $identity): View
|
public function create(Request $request, IdentityTeamClient $identity): View
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.members.manage');
|
$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);
|
$organization = $this->organization($request);
|
||||||
|
|
||||||
$branches = Branch::owned($this->ownerRef($request))
|
$branches = Branch::owned($this->ownerRef($request))
|
||||||
@@ -71,6 +97,11 @@ class MemberController extends Controller
|
|||||||
public function store(Request $request, IdentityTeamClient $identity): RedirectResponse
|
public function store(Request $request, IdentityTeamClient $identity): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.members.manage');
|
$this->authorizeAbility($request, 'admin.members.manage');
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$organization = $this->organization($request);
|
$organization = $this->organization($request);
|
||||||
$owner = $this->ownerRef($request);
|
$owner = $this->ownerRef($request);
|
||||||
|
|
||||||
@@ -119,6 +150,10 @@ class MemberController extends Controller
|
|||||||
$this->authorizeAbility($request, 'admin.members.manage');
|
$this->authorizeAbility($request, 'admin.members.manage');
|
||||||
$this->authorizeOwner($request, $member);
|
$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.');
|
abort_if($member->user_ref === $this->ownerRef($request), 422, 'You cannot remove yourself.');
|
||||||
|
|
||||||
$member->delete();
|
$member->delete();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Frontdesk;
|
namespace App\Http\Controllers\Frontdesk;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Controllers\Frontdesk\Concerns\RequiresPlanFeature;
|
||||||
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
use App\Http\Controllers\Frontdesk\Concerns\ScopesToAccount;
|
||||||
use App\Models\Building;
|
use App\Models\Building;
|
||||||
use App\Models\ReceptionDesk;
|
use App\Models\ReceptionDesk;
|
||||||
@@ -12,13 +13,27 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class ReceptionDeskController extends Controller
|
class ReceptionDeskController extends Controller
|
||||||
{
|
{
|
||||||
|
use RequiresPlanFeature;
|
||||||
use ScopesToAccount;
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
private const FEATURE = 'branches';
|
||||||
|
|
||||||
|
private const UPGRADE_MESSAGE = 'Multi-branch management requires Frontdesk Pro.';
|
||||||
|
|
||||||
public function index(Request $request, Building $building): View
|
public function index(Request $request, Building $building): View
|
||||||
{
|
{
|
||||||
$this->authorizeAbility($request, 'admin.desks.view');
|
$this->authorizeAbility($request, 'admin.desks.view');
|
||||||
$this->authorizeOwner($request, $building);
|
$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();
|
$desks = $building->receptionDesks()->orderBy('name')->get();
|
||||||
|
|
||||||
return view('frontdesk.admin.desks.index', compact('building', 'desks'));
|
return view('frontdesk.admin.desks.index', compact('building', 'desks'));
|
||||||
@@ -29,6 +44,10 @@ class ReceptionDeskController extends Controller
|
|||||||
$this->authorizeAbility($request, 'admin.branches.manage');
|
$this->authorizeAbility($request, 'admin.branches.manage');
|
||||||
$this->authorizeOwner($request, $building);
|
$this->authorizeOwner($request, $building);
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'location' => ['nullable', 'string', 'max:255'],
|
'location' => ['nullable', 'string', 'max:255'],
|
||||||
@@ -50,6 +69,10 @@ class ReceptionDeskController extends Controller
|
|||||||
$this->authorizeOwner($request, $desk);
|
$this->authorizeOwner($request, $desk);
|
||||||
abort_unless($desk->building_id === $building->id, 404);
|
abort_unless($desk->building_id === $building->id, 404);
|
||||||
|
|
||||||
|
if ($deny = $this->denyWithoutFeature($request, self::FEATURE, self::UPGRADE_MESSAGE)) {
|
||||||
|
return $deny;
|
||||||
|
}
|
||||||
|
|
||||||
$desk->delete();
|
$desk->delete();
|
||||||
|
|
||||||
return back()->with('success', 'Reception desk removed.');
|
return back()->with('success', 'Reception desk removed.');
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class SettingsController extends Controller
|
|||||||
$monthlyUsage = $usage->forOrganization($organization);
|
$monthlyUsage = $usage->forOrganization($organization);
|
||||||
$plan = $plans->plan($organization);
|
$plan = $plans->plan($organization);
|
||||||
$freeEmailAllowance = $plans->freeEmailsPerMonth($organization);
|
$freeEmailAllowance = $plans->freeEmailsPerMonth($organization);
|
||||||
|
$member = $this->member($request);
|
||||||
|
$permissions = app(FrontdeskPermissions::class);
|
||||||
|
|
||||||
return view('frontdesk.settings.edit', [
|
return view('frontdesk.settings.edit', [
|
||||||
'organization' => $organization,
|
'organization' => $organization,
|
||||||
@@ -45,6 +47,10 @@ class SettingsController extends Controller
|
|||||||
'isPro' => $plans->isPro($organization),
|
'isPro' => $plans->isPro($organization),
|
||||||
'isEnterprise' => $plans->isEnterprise($organization),
|
'isEnterprise' => $plans->isEnterprise($organization),
|
||||||
'hasPaidPlan' => $plans->hasPaidPlan($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),
|
'billedBranches' => $plans->billedBranches($organization),
|
||||||
'activeBranchCount' => $plans->activeBranchCount($organization),
|
'activeBranchCount' => $plans->activeBranchCount($organization),
|
||||||
'proPriceMinor' => $plans->proPricePerBranchMinor(),
|
'proPriceMinor' => $plans->proPricePerBranchMinor(),
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ return [
|
|||||||
'hosts',
|
'hosts',
|
||||||
'badges',
|
'badges',
|
||||||
'watchlist',
|
'watchlist',
|
||||||
|
'branches',
|
||||||
|
'team',
|
||||||
'integrations',
|
'integrations',
|
||||||
'webhooks',
|
'webhooks',
|
||||||
'scheduled_reports',
|
'scheduled_reports',
|
||||||
@@ -178,6 +180,8 @@ return [
|
|||||||
'hosts',
|
'hosts',
|
||||||
'badges',
|
'badges',
|
||||||
'watchlist',
|
'watchlist',
|
||||||
|
'branches',
|
||||||
|
'team',
|
||||||
'integrations',
|
'integrations',
|
||||||
'webhooks',
|
'webhooks',
|
||||||
'scheduled_reports',
|
'scheduled_reports',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<x-app-layout title="Add branch">
|
<x-app-layout title="Add branch">
|
||||||
<div class="mx-auto max-w-lg">
|
<div class="mx-auto max-w-lg">
|
||||||
<h1 class="text-xl font-semibold text-slate-900">Add branch</h1>
|
<a href="{{ route('frontdesk.branches.index') }}" class="text-sm text-slate-500 hover:text-slate-800">← Branches</a>
|
||||||
|
<h1 class="mt-2 text-xl font-semibold text-slate-900">Add branch</h1>
|
||||||
<form method="POST" action="{{ route('frontdesk.branches.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
<form method="POST" action="{{ route('frontdesk.branches.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||||
@csrf
|
@csrf
|
||||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<x-app-layout title="Edit branch">
|
<x-app-layout title="Edit branch">
|
||||||
<div class="mx-auto max-w-lg">
|
<div class="mx-auto max-w-lg">
|
||||||
<h1 class="text-xl font-semibold text-slate-900">Edit branch</h1>
|
<a href="{{ route('frontdesk.branches.index') }}" class="text-sm text-slate-500 hover:text-slate-800">← Branches</a>
|
||||||
|
<h1 class="mt-2 text-xl font-semibold text-slate-900">Edit branch</h1>
|
||||||
<form method="POST" action="{{ route('frontdesk.branches.update', $branch) }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
<form method="POST" action="{{ route('frontdesk.branches.update', $branch) }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" value="{{ old('name', $branch->name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
<div><label class="block text-sm font-medium">Name</label><input type="text" name="name" value="{{ old('name', $branch->name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm"></div>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
<x-app-layout title="Branches">
|
<x-app-layout title="Branches">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<a href="{{ route('frontdesk.settings') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-800">← Settings</a>
|
||||||
<x-frontdesk.page-hero
|
<x-frontdesk.page-hero
|
||||||
badge="Locations · Buildings · Reception desks"
|
badge="Settings · Locations · Buildings"
|
||||||
title="Branches"
|
title="Branches"
|
||||||
description="Organize your sites, buildings, and reception desks so visits and devices stay scoped to the right location."
|
description="Organize your sites, buildings, and reception desks so visits and devices stay scoped to the right location."
|
||||||
:stats="[
|
:stats="[
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<x-app-layout title="Add member">
|
<x-app-layout title="Add member">
|
||||||
<div class="mx-auto max-w-lg">
|
<div class="mx-auto max-w-lg">
|
||||||
<h1 class="text-xl font-semibold text-slate-900">Invite team member</h1>
|
<a href="{{ route('frontdesk.members.index') }}" class="text-sm text-slate-500 hover:text-slate-800">← Team</a>
|
||||||
|
<h1 class="mt-2 text-xl font-semibold text-slate-900">Invite team member</h1>
|
||||||
|
|
||||||
<form method="POST" x-data action="{{ route('frontdesk.members.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
<form method="POST" x-data action="{{ route('frontdesk.members.store') }}" class="mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||||
@csrf
|
@csrf
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<x-app-layout title="Team">
|
<x-app-layout title="Team">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
<a href="{{ route('frontdesk.settings') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-800">← Settings</a>
|
||||||
<x-frontdesk.page-hero
|
<x-frontdesk.page-hero
|
||||||
badge="Roles · Access · Branch scope"
|
badge="Settings · Roles · Access"
|
||||||
title="Team"
|
title="Team"
|
||||||
description="Invite colleagues, assign Frontdesk roles, and limit branch access for reception and security staff."
|
description="Invite colleagues, assign Frontdesk roles, and limit branch access for reception and security staff."
|
||||||
:stats="[
|
:stats="[
|
||||||
|
|||||||
@@ -72,11 +72,12 @@
|
|||||||
<div class="flex flex-col rounded-2xl border border-slate-200 bg-white p-6 {{ $planKey === 'free' ? 'ring-2 ring-indigo-500' : '' }}">
|
<div class="flex flex-col rounded-2xl border border-slate-200 bg-white p-6 {{ $planKey === 'free' ? 'ring-2 ring-indigo-500' : '' }}">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Free</p>
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Free</p>
|
||||||
<p class="mt-2 text-3xl font-bold text-slate-900">GHS 0</p>
|
<p class="mt-2 text-3xl font-bold text-slate-900">GHS 0</p>
|
||||||
<p class="mt-1 text-sm text-slate-500">1 branch · 1 kiosk</p>
|
<p class="mt-1 text-sm text-slate-500">1 branch · 1 kiosk · owner only</p>
|
||||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
||||||
<li>Core check-in & badges</li>
|
<li>Core check-in & badges</li>
|
||||||
<li>100 host emails / month</li>
|
<li>100 host emails / month</li>
|
||||||
<li>Watchlist & hosts</li>
|
<li>Watchlist & hosts</li>
|
||||||
|
<li class="text-slate-400">Multi-branch & team (Pro)</li>
|
||||||
</ul>
|
</ul>
|
||||||
@if ($planKey === 'free')
|
@if ($planKey === 'free')
|
||||||
<p class="mt-6 text-sm font-medium text-indigo-700">Current plan</p>
|
<p class="mt-6 text-sm font-medium text-indigo-700">Current plan</p>
|
||||||
@@ -97,6 +98,8 @@
|
|||||||
= {{ $currency }} <span x-text="fmt(proTotal())"></span>/mo
|
= {{ $currency }} <span x-text="fmt(proTotal())"></span>/mo
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
<ul class="mt-5 flex-1 space-y-2 text-sm text-slate-700">
|
||||||
|
<li>Multi-branch locations & buildings</li>
|
||||||
|
<li>Team roles & invitations</li>
|
||||||
<li>Unlimited host email alerts</li>
|
<li>Unlimited host email alerts</li>
|
||||||
<li>Webhooks & iCal feeds</li>
|
<li>Webhooks & iCal feeds</li>
|
||||||
<li>Scheduled report exports</li>
|
<li>Scheduled report exports</li>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
({{ $activeBranchCount }} active).
|
({{ $activeBranchCount }} active).
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<span class="font-medium text-slate-900">Free</span> — one branch, one kiosk, core check-in and badges. Host alerts billed after {{ number_format($freeEmailAllowance) }} free emails per month.
|
<span class="font-medium text-slate-900">Free</span> — one branch (setup only), one kiosk, owner-only access. Multi-branch and team require Pro. Host alerts billed after {{ number_format($freeEmailAllowance) }} free emails per month.
|
||||||
@endif
|
@endif
|
||||||
</p>
|
</p>
|
||||||
@if (! $hasPaidPlan)
|
@if (! $hasPaidPlan)
|
||||||
@@ -47,6 +47,45 @@
|
|||||||
</x-settings.card>
|
</x-settings.card>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if ($canViewBranches || $canViewTeam)
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
|
@if ($canViewBranches)
|
||||||
|
<a href="{{ route('frontdesk.branches.index') }}"
|
||||||
|
class="group rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow-md">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-slate-900 group-hover:text-indigo-700">Branches</p>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Locations, buildings, and reception desks.</p>
|
||||||
|
</div>
|
||||||
|
@if (! $hasBranchesFeature)
|
||||||
|
<span class="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">Pro</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs font-medium text-indigo-600">
|
||||||
|
{{ $hasBranchesFeature ? 'Manage branches →' : 'Upgrade to unlock →' }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@if ($canViewTeam)
|
||||||
|
<a href="{{ route('frontdesk.members.index') }}"
|
||||||
|
class="group rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 hover:shadow-md">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-slate-900 group-hover:text-indigo-700">Team</p>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Invite colleagues and assign Frontdesk roles.</p>
|
||||||
|
</div>
|
||||||
|
@if (! $hasTeamFeature)
|
||||||
|
<span class="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">Pro</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-xs font-medium text-indigo-600">
|
||||||
|
{{ $hasTeamFeature ? 'Manage team →' : 'Upgrade to unlock →' }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<form method="POST" action="{{ route('frontdesk.settings.update') }}" enctype="multipart/form-data" class="space-y-6">
|
<form method="POST" action="{{ route('frontdesk.settings.update') }}" enctype="multipart/form-data" class="space-y-6">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<x-app-layout :title="$title">
|
||||||
|
<div class="mx-auto max-w-lg">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">Settings</p>
|
||||||
|
<h1 class="mt-1 text-xl font-semibold text-slate-900">{{ $title }}</h1>
|
||||||
|
<p class="mt-2 text-sm text-slate-600">{{ $description }}</p>
|
||||||
|
|
||||||
|
<div class="mt-6 rounded-2xl border border-indigo-200 bg-indigo-50 p-6">
|
||||||
|
<h2 class="font-semibold text-indigo-900">Upgrade to Pro</h2>
|
||||||
|
<p class="mt-2 text-sm text-indigo-800">
|
||||||
|
GHS {{ number_format($proPriceMinor / 100, 0) }} / branch / month from your Ladill wallet —
|
||||||
|
multi-branch locations, team roles, integrations, and more.
|
||||||
|
</p>
|
||||||
|
<a href="{{ route('frontdesk.pro.index') }}" class="btn-primary mt-4 inline-flex">View Pro plans</a>
|
||||||
|
<p class="mt-3 text-xs text-indigo-700">
|
||||||
|
<a href="{{ route('frontdesk.wallet') }}" class="underline">Top up your wallet</a> if you need more balance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ route('frontdesk.settings') }}" class="mt-6 inline-block text-sm text-slate-600 hover:text-slate-900">← Back to settings</a>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -98,15 +98,13 @@
|
|||||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />'];
|
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminNav = [];
|
$settingsActive = request()->routeIs('frontdesk.settings')
|
||||||
if ($permissions->can($member, 'admin.branches.view')) {
|
|| request()->routeIs('frontdesk.branches.*')
|
||||||
$adminNav[] = ['name' => 'Branches', 'route' => route('frontdesk.branches.index'), 'active' => request()->routeIs('frontdesk.branches.*') || request()->routeIs('frontdesk.buildings.*') || request()->routeIs('frontdesk.desks.*'),
|
|| request()->routeIs('frontdesk.buildings.*')
|
||||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m-1.5 3h1.5m3-9H15m-1.5 3H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21" />'];
|
|| request()->routeIs('frontdesk.desks.*')
|
||||||
}
|
|| request()->routeIs('frontdesk.members.*')
|
||||||
if ($permissions->can($member, 'admin.members.view')) {
|
|| request()->routeIs('frontdesk.integrations.*')
|
||||||
$adminNav[] = ['name' => 'Team', 'route' => route('frontdesk.members.index'), 'active' => request()->routeIs('frontdesk.members.*'),
|
|| request()->routeIs('frontdesk.settings.badge*');
|
||||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />'];
|
|
||||||
}
|
|
||||||
@endphp
|
@endphp
|
||||||
<nav class="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
<nav class="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||||
@foreach($nav as $item)
|
@foreach($nav as $item)
|
||||||
@@ -116,7 +114,7 @@
|
|||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
@if (count($adminNav) > 0 || count($complianceNav) > 0)
|
@if (count($complianceNav) > 0)
|
||||||
<p class="mb-1 mt-4 px-3 text-[10px] font-semibold uppercase tracking-wider text-slate-400">Compliance</p>
|
<p class="mb-1 mt-4 px-3 text-[10px] font-semibold uppercase tracking-wider text-slate-400">Compliance</p>
|
||||||
@foreach ($complianceNav as $item)
|
@foreach ($complianceNav as $item)
|
||||||
<a href="{{ $item['route'] }}" class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $item['active'] ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
<a href="{{ $item['route'] }}" class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $item['active'] ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
||||||
@@ -125,22 +123,12 @@
|
|||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if (count($adminNav) > 0)
|
|
||||||
<p class="mb-1 mt-4 px-3 text-[10px] font-semibold uppercase tracking-wider text-slate-400">Administration</p>
|
|
||||||
@foreach ($adminNav as $item)
|
|
||||||
<a href="{{ $item['route'] }}" class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $item['active'] ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
|
||||||
<svg class="{{ $item['iconSizeClass'] ?? 'h-[18px] w-[18px]' }} shrink-0 {{ $item['active'] ? 'text-indigo-600' : 'text-slate-400' }}" viewBox="{{ $item['iconViewBox'] ?? '0 0 24 24' }}" fill="none" stroke="currentColor" stroke-width="{{ $item['iconStrokeWidth'] ?? 1.5 }}">{!! $item['icon'] !!}</svg>
|
|
||||||
<span>{{ $item['name'] }}</span>
|
|
||||||
</a>
|
|
||||||
@endforeach
|
|
||||||
@endif
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="shrink-0 border-t border-slate-200 px-3 py-3">
|
<div class="shrink-0 border-t border-slate-200 px-3 py-3">
|
||||||
<a href="{{ route('frontdesk.settings') }}"
|
<a href="{{ route('frontdesk.settings') }}"
|
||||||
class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ request()->routeIs('frontdesk.settings') && ! request()->routeIs('frontdesk.pro.*') ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ $settingsActive ? 'bg-indigo-50 text-indigo-700 font-semibold' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900' }}">
|
||||||
<svg class="h-[18px] w-[18px] shrink-0 {{ request()->routeIs('frontdesk.settings') && ! request()->routeIs('frontdesk.pro.*') ? 'text-indigo-600' : 'text-slate-400' }}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<svg class="h-[18px] w-[18px] shrink-0 {{ $settingsActive ? 'text-indigo-600' : 'text-slate-400' }}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.241.437-.613.43-.992a6.932 6.932 0 0 1 0-.255c.007-.378-.138-.75-.43-.991l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.281Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.241.437-.613.43-.992a6.932 6.932 0 0 1 0-.255c.007-.378-.138-.75-.43-.991l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.281Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="flex-1 truncate">Settings</span>
|
<span class="flex-1 truncate">Settings</span>
|
||||||
|
|||||||
+20
-15
@@ -191,24 +191,29 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
|||||||
// Legacy alias — defaults branches to active count when omitted.
|
// Legacy alias — defaults branches to active count when omitted.
|
||||||
Route::post('/settings/upgrade', [ProController::class, 'subscribe'])->name('frontdesk.settings.upgrade');
|
Route::post('/settings/upgrade', [ProController::class, 'subscribe'])->name('frontdesk.settings.upgrade');
|
||||||
|
|
||||||
Route::get('/branches', [BranchController::class, 'index'])->name('frontdesk.branches.index');
|
// Branches & Team live under Settings and require a paid plan.
|
||||||
Route::get('/branches/create', [BranchController::class, 'create'])->name('frontdesk.branches.create');
|
Route::get('/settings/branches', [BranchController::class, 'index'])->name('frontdesk.branches.index');
|
||||||
Route::post('/branches', [BranchController::class, 'store'])->name('frontdesk.branches.store');
|
Route::get('/settings/branches/create', [BranchController::class, 'create'])->name('frontdesk.branches.create');
|
||||||
Route::get('/branches/{branch}/edit', [BranchController::class, 'edit'])->name('frontdesk.branches.edit');
|
Route::post('/settings/branches', [BranchController::class, 'store'])->name('frontdesk.branches.store');
|
||||||
Route::put('/branches/{branch}', [BranchController::class, 'update'])->name('frontdesk.branches.update');
|
Route::get('/settings/branches/{branch}/edit', [BranchController::class, 'edit'])->name('frontdesk.branches.edit');
|
||||||
|
Route::put('/settings/branches/{branch}', [BranchController::class, 'update'])->name('frontdesk.branches.update');
|
||||||
|
|
||||||
Route::get('/branches/{branch}/buildings', [BuildingController::class, 'index'])->name('frontdesk.buildings.index');
|
Route::get('/settings/branches/{branch}/buildings', [BuildingController::class, 'index'])->name('frontdesk.buildings.index');
|
||||||
Route::post('/branches/{branch}/buildings', [BuildingController::class, 'store'])->name('frontdesk.buildings.store');
|
Route::post('/settings/branches/{branch}/buildings', [BuildingController::class, 'store'])->name('frontdesk.buildings.store');
|
||||||
Route::delete('/branches/{branch}/buildings/{building}', [BuildingController::class, 'destroy'])->name('frontdesk.buildings.destroy');
|
Route::delete('/settings/branches/{branch}/buildings/{building}', [BuildingController::class, 'destroy'])->name('frontdesk.buildings.destroy');
|
||||||
|
|
||||||
Route::get('/buildings/{building}/desks', [ReceptionDeskController::class, 'index'])->name('frontdesk.desks.index');
|
Route::get('/settings/buildings/{building}/desks', [ReceptionDeskController::class, 'index'])->name('frontdesk.desks.index');
|
||||||
Route::post('/buildings/{building}/desks', [ReceptionDeskController::class, 'store'])->name('frontdesk.desks.store');
|
Route::post('/settings/buildings/{building}/desks', [ReceptionDeskController::class, 'store'])->name('frontdesk.desks.store');
|
||||||
Route::delete('/buildings/{building}/desks/{desk}', [ReceptionDeskController::class, 'destroy'])->name('frontdesk.desks.destroy');
|
Route::delete('/settings/buildings/{building}/desks/{desk}', [ReceptionDeskController::class, 'destroy'])->name('frontdesk.desks.destroy');
|
||||||
|
|
||||||
Route::get('/members', [MemberController::class, 'index'])->name('frontdesk.members.index');
|
Route::get('/settings/team', [MemberController::class, 'index'])->name('frontdesk.members.index');
|
||||||
Route::get('/members/create', [MemberController::class, 'create'])->name('frontdesk.members.create');
|
Route::get('/settings/team/create', [MemberController::class, 'create'])->name('frontdesk.members.create');
|
||||||
Route::post('/members', [MemberController::class, 'store'])->name('frontdesk.members.store');
|
Route::post('/settings/team', [MemberController::class, 'store'])->name('frontdesk.members.store');
|
||||||
Route::delete('/members/{member}', [MemberController::class, 'destroy'])->name('frontdesk.members.destroy');
|
Route::delete('/settings/team/{member}', [MemberController::class, 'destroy'])->name('frontdesk.members.destroy');
|
||||||
|
|
||||||
|
// Legacy path redirects.
|
||||||
|
Route::redirect('/branches', '/settings/branches');
|
||||||
|
Route::redirect('/members', '/settings/team');
|
||||||
|
|
||||||
Route::get('/wallet', fn () => redirect()->away(ladill_account_url('/wallet')))->name('frontdesk.wallet');
|
Route::get('/wallet', fn () => redirect()->away(ladill_account_url('/wallet')))->name('frontdesk.wallet');
|
||||||
Route::get('/wallet/balance', [WalletController::class, 'balance'])->name('frontdesk.wallet.balance');
|
Route::get('/wallet/balance', [WalletController::class, 'balance'])->name('frontdesk.wallet.balance');
|
||||||
|
|||||||
@@ -63,8 +63,16 @@ class FrontdeskPhase2Test extends TestCase
|
|||||||
$this->assertDatabaseHas('frontdesk_members', ['role' => 'org_admin']);
|
$this->assertDatabaseHas('frontdesk_members', ['role' => 'org_admin']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_org_admin_can_create_branch(): void
|
public function test_org_admin_can_create_branch_on_pro(): void
|
||||||
{
|
{
|
||||||
|
$this->organization->update([
|
||||||
|
'settings' => array_merge($this->organization->settings ?? [], [
|
||||||
|
'plan' => 'pro',
|
||||||
|
'billed_branches' => 3,
|
||||||
|
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
$this->actingAs($this->admin)
|
$this->actingAs($this->admin)
|
||||||
->post(route('frontdesk.branches.store'), [
|
->post(route('frontdesk.branches.store'), [
|
||||||
'name' => 'East Wing',
|
'name' => 'East Wing',
|
||||||
@@ -75,8 +83,31 @@ class FrontdeskPhase2Test extends TestCase
|
|||||||
$this->assertDatabaseHas('frontdesk_branches', ['name' => 'East Wing']);
|
$this->assertDatabaseHas('frontdesk_branches', ['name' => 'East Wing']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_org_admin_can_add_team_member(): void
|
public function test_free_plan_cannot_manage_branches(): void
|
||||||
{
|
{
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->get(route('frontdesk.branches.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Upgrade to Pro');
|
||||||
|
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->post(route('frontdesk.branches.store'), [
|
||||||
|
'name' => 'East Wing',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('frontdesk.pro.index'))
|
||||||
|
->assertSessionHas('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_org_admin_can_add_team_member_on_pro(): void
|
||||||
|
{
|
||||||
|
$this->organization->update([
|
||||||
|
'settings' => array_merge($this->organization->settings ?? [], [
|
||||||
|
'plan' => 'pro',
|
||||||
|
'billed_branches' => 1,
|
||||||
|
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
$branch = Branch::create([
|
$branch = Branch::create([
|
||||||
'owner_ref' => $this->admin->public_id,
|
'owner_ref' => $this->admin->public_id,
|
||||||
'organization_id' => $this->organization->id,
|
'organization_id' => $this->organization->id,
|
||||||
@@ -84,20 +115,43 @@ class FrontdeskPhase2Test extends TestCase
|
|||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->mock(\App\Services\Identity\IdentityTeamClient::class, function ($mock) {
|
||||||
|
$mock->shouldReceive('inviteAppMember')->once();
|
||||||
|
});
|
||||||
|
|
||||||
$this->actingAs($this->admin)
|
$this->actingAs($this->admin)
|
||||||
->post(route('frontdesk.members.store'), [
|
->post(route('frontdesk.members.store'), [
|
||||||
'user_ref' => 'receptionist-001',
|
'email' => 'receptionist@example.com',
|
||||||
'role' => 'receptionist',
|
'role' => 'receptionist',
|
||||||
'branch_id' => $branch->id,
|
'branch_id' => $branch->id,
|
||||||
])
|
])
|
||||||
->assertRedirect(route('frontdesk.members.index'));
|
->assertRedirect(route('frontdesk.members.index'));
|
||||||
|
|
||||||
$this->assertDatabaseHas('frontdesk_members', [
|
$this->assertDatabaseHas('frontdesk_members', [
|
||||||
'user_ref' => 'receptionist-001',
|
'user_ref' => 'receptionist@example.com',
|
||||||
'role' => 'receptionist',
|
'role' => 'receptionist',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_free_plan_cannot_manage_team(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->get(route('frontdesk.members.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Upgrade to Pro');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_settings_links_to_branches_and_team(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->get(route('frontdesk.settings'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Branches')
|
||||||
|
->assertSee('Team')
|
||||||
|
->assertSee(route('frontdesk.branches.index'), false)
|
||||||
|
->assertSee(route('frontdesk.members.index'), false);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_receptionist_cannot_manage_branches(): void
|
public function test_receptionist_cannot_manage_branches(): void
|
||||||
{
|
{
|
||||||
$receptionist = User::create([
|
$receptionist = User::create([
|
||||||
|
|||||||
@@ -459,4 +459,27 @@ class FrontdeskProTest extends TestCase
|
|||||||
|
|
||||||
$this->assertDatabaseHas('frontdesk_branches', ['name' => 'Second Branch']);
|
$this->assertDatabaseHas('frontdesk_branches', ['name' => 'Second Branch']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_pro_can_open_branches_and_team_settings(): void
|
||||||
|
{
|
||||||
|
$this->organization->update([
|
||||||
|
'settings' => array_merge($this->organization->settings ?? [], [
|
||||||
|
'plan' => 'pro',
|
||||||
|
'billed_branches' => 1,
|
||||||
|
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('frontdesk.branches.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Branches')
|
||||||
|
->assertDontSee('Upgrade to Pro');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('frontdesk.members.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Team')
|
||||||
|
->assertDontSee('Upgrade to Pro');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user