Management UI
CI / ci (push) Canceled after 0s

This commit is contained in:
Stefan Lazic committed 2026-09-05 09:11:24 +01:00
1 parent cd6abeca6e
commit f11404c574
29 files changed
+876 -89

No files matched your search

@@ -0,0 +1,20 @@
<x-layouts.public :business="$business">
<main class="mx-auto flex min-h-screen max-w-md flex-col justify-center px-6 py-16">
<p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">{{ $business->name }}</p>
<h1 class="mt-3 text-4xl font-black tracking-tight">Finish your owner setup</h1>
<p class="mt-4 text-sm leading-6 text-muted">Choose a password for {{ $invitation->staff->user->email }} to access the business workspace.</p>
@if ($errors->any())
<div class="mt-6 rounded-tile border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('owner.invite.accept', $invitation->token) }}" class="mt-8 space-y-5">
@csrf
<label class="block text-sm font-bold">Password
<input name="password" type="password" minlength="8" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<label class="block text-sm font-bold">Confirm password
<input name="password_confirmation" type="password" minlength="8" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<x-ui.button type="submit" class="w-full">Open owner workspace</x-ui.button>
</form>
</main>
</x-layouts.public>
@@ -0,0 +1,20 @@
<x-layouts.public :business="null">
<main class="mx-auto flex min-h-screen max-w-md flex-col justify-center px-6 py-16">
<p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">NextChair Manager</p>
<h1 class="mt-3 text-4xl font-black tracking-tight">Platform sign in</h1>
<p class="mt-4 text-sm leading-6 text-muted">Sign in with the platform administrator account to manage businesses.</p>
@if ($errors->any())
<div class="mt-6 rounded-tile border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('manager.login.store') }}" class="mt-8 space-y-5">
@csrf
<label class="block text-sm font-bold">Email
<input name="email" value="{{ old('email') }}" type="email" required autofocus class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<label class="block text-sm font-bold">Password
<input name="password" type="password" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<x-ui.button type="submit" class="w-full">Sign in</x-ui.button>
</form>
</main>
</x-layouts.public>
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Manager · {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen bg-stone text-ink">
<header class="border-b border-line bg-white">
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 py-5 sm:px-8 lg:px-12">
<a href="{{ route('manager.dashboard') }}" class="text-sm font-black tracking-tight">NextChair <span class="font-normal text-muted">Manager</span></a>
<div class="flex items-center gap-4 text-xs font-bold uppercase tracking-widest">
<span class="hidden text-muted sm:inline">{{ auth()->user()->name }}</span>
<form method="POST" action="{{ route('auth.signout') }}">@csrf<button class="text-muted hover:text-ink">Sign out</button></form>
</div>
</div>
</header>
{{ $slot }}
</body>
</html>
+26 -1
View File
@@ -1 +1,26 @@
<x-layouts.public :business="null"><main class="mx-auto flex min-h-screen max-w-xl flex-col justify-center px-5 py-16 lg:px-10"><p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">NextChair setup</p><h1 class="mt-3 text-4xl font-black tracking-tight">Create your business</h1><p class="mt-4 text-sm leading-6 text-muted">Set up the first business owner and start configuring your booking workspace.</p>@if (session('status'))<div class="mt-6 rounded-tile bg-accent p-4 text-sm font-bold text-ink">{{ session('status') }}</div><a href="{{ route('auth.signin') }}" class="mt-6 text-sm font-bold text-accent-foreground underline">Sign in</a>@else @if ($errors->any())<div class="mt-6 rounded-tile border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ $errors->first() }}</div>@endif<form method="POST" action="{{ route('install.store') }}" class="mt-8 space-y-5">@csrf<label class="block text-sm font-bold">Business name<input name="businessName" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label><label class="block text-sm font-bold">Business slug<input name="slug" required placeholder="my-barbershop" class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label><label class="block text-sm font-bold">Domain <span class="font-normal text-muted">(optional)</span><input name="domain" placeholder="book.example.com" class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label><div class="grid gap-5 sm:grid-cols-2"><label class="block text-sm font-bold">Owner name<input name="adminName" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label><label class="block text-sm font-bold">Owner email<input name="adminEmail" type="email" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label></div><label class="block text-sm font-bold">Password<input name="password" type="password" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label><label class="block text-sm font-bold">Confirm password<input name="password_confirmation" type="password" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3"></label><x-ui.button type="submit" class="w-full">Finish setup</x-ui.button></form>@endif</main></x-layouts.public>
<x-layouts.public :business="null">
<main class="mx-auto flex min-h-screen max-w-xl flex-col justify-center px-5 py-16 lg:px-10">
<p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">NextChair setup</p>
<h1 class="mt-3 text-4xl font-black tracking-tight">Create the platform administrator</h1>
<p class="mt-4 text-sm leading-6 text-muted">This first account manages the NextChair platform. Businesses and their owners are created later from the protected Manager area.</p>
@if ($errors->any())
<div class="mt-6 rounded-tile border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('install.store') }}" class="mt-8 space-y-5">
@csrf
<label class="block text-sm font-bold">Your name
<input name="adminName" value="{{ old('adminName') }}" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<label class="block text-sm font-bold">Email
<input name="adminEmail" value="{{ old('adminEmail') }}" type="email" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<label class="block text-sm font-bold">Password
<input name="password" type="password" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<label class="block text-sm font-bold">Confirm password
<input name="password_confirmation" type="password" required class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">
</label>
<x-ui.button type="submit" class="w-full">Finish platform setup</x-ui.button>
</form>
</main>
</x-layouts.public>
@@ -0,0 +1,22 @@
<x-layouts.manager>
<main class="mx-auto max-w-4xl px-5 py-10 sm:px-8 lg:px-12 lg:py-14">
<a href="{{ route('manager.dashboard') }}" class="text-xs font-bold uppercase tracking-widest text-muted">← Businesses</a>
<div class="mt-10"><p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">New tenant</p><h1 class="mt-3 text-4xl font-black tracking-tight">Create a business</h1><p class="mt-3 max-w-2xl text-sm leading-6 text-muted">Provision the tenant, its defaults, and the owner’s secure setup invitation together.</p></div>
@if ($errors->any())<div class="mt-8 rounded-tile border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ $errors->first() }}</div>@endif
<form method="POST" action="{{ route('manager.businesses.store') }}" class="mt-8 space-y-8">
@csrf
<section class="rounded-jumbo border border-line bg-white p-6 sm:p-8"><h2 class="text-lg font-black">Business details</h2><div class="mt-6 grid gap-5 sm:grid-cols-2">
<label class="block text-sm font-bold sm:col-span-2">Business name<input name="name" value="{{ old('name') }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label>
<label class="block text-sm font-bold">Short name <span class="font-normal text-muted">(optional)</span><input name="shortName" value="{{ old('shortName') }}" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label>
<label class="block text-sm font-bold">Slug<input name="slug" value="{{ old('slug') }}" placeholder="blake-heritage" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label>
<label class="block text-sm font-bold sm:col-span-2">Primary domain<input name="domain" value="{{ old('domain') }}" placeholder="book.example.com" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"><span class="mt-2 block text-xs font-normal text-muted">This hostname routes public and owner requests to this tenant.</span></label>
<label class="block text-sm font-bold">Business email<input name="email" value="{{ old('email') }}" type="email" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label>
<label class="block text-sm font-bold">Phone<input name="phone" value="{{ old('phone') }}" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label>
<label class="block text-sm font-bold">Timezone<select name="timezone" class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">@foreach (DateTimeZone::listIdentifiers() as $timezone)<option value="{{ $timezone }}" @selected(old('timezone', 'Europe/London') === $timezone)>{{ $timezone }}</option>@endforeach</select></label>
<label class="block text-sm font-bold">Currency<input name="currency" value="{{ old('currency', 'GBP') }}" maxlength="3" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label>
</div></section>
<section class="rounded-jumbo border border-line bg-white p-6 sm:p-8"><h2 class="text-lg font-black">Business owner</h2><p class="mt-2 text-sm text-muted">The owner receives a one-time link to choose their password.</p><div class="mt-6 grid gap-5 sm:grid-cols-2"><label class="block text-sm font-bold">Owner name<input name="ownerName" value="{{ old('ownerName') }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold">Owner email<input name="ownerEmail" value="{{ old('ownerEmail') }}" type="email" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label></div></section>
<div class="flex justify-end"><x-ui.button type="submit">Provision business</x-ui.button></div>
</form>
</main>
</x-layouts.manager>
@@ -0,0 +1,8 @@
<x-layouts.manager>
<main class="mx-auto max-w-4xl px-5 py-10 sm:px-8 lg:px-12 lg:py-14">
<a href="{{ route('manager.businesses.show', $business) }}" class="text-xs font-bold uppercase tracking-widest text-muted">← {{ $business->name }}</a>
<div class="mt-10"><p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">Tenant settings</p><h1 class="mt-3 text-4xl font-black tracking-tight">Edit business</h1></div>
@if ($errors->any())<div class="mt-8 rounded-tile border border-red-200 bg-red-50 p-4 text-sm text-red-700">{{ $errors->first() }}</div>@endif
<form method="POST" action="{{ route('manager.businesses.update', $business) }}" class="mt-8 rounded-jumbo border border-line bg-white p-6 sm:p-8">@csrf @method('PUT')<div class="grid gap-5 sm:grid-cols-2"><label class="block text-sm font-bold sm:col-span-2">Business name<input name="name" value="{{ old('name', $business->name) }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold">Short name<input name="shortName" value="{{ old('shortName', $business->shortName) }}" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold">Slug<input name="slug" value="{{ old('slug', $business->slug) }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold sm:col-span-2">Primary domain<input name="domain" value="{{ old('domain', $business->primaryDomain?->hostname) }}" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold">Business email<input name="email" value="{{ old('email', $business->email) }}" type="email" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold">Phone<input name="phone" value="{{ old('phone', $business->phone) }}" class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label><label class="block text-sm font-bold">Timezone<select name="timezone" class="mt-2 w-full rounded-tile border border-line bg-white px-4 py-3">@foreach (DateTimeZone::listIdentifiers() as $timezone)<option value="{{ $timezone }}" @selected(old('timezone', $business->timezone) === $timezone)>{{ $timezone }}</option>@endforeach</select></label><label class="block text-sm font-bold">Currency<input name="currency" value="{{ old('currency', $business->currency) }}" maxlength="3" required class="mt-2 w-full rounded-tile border border-line px-4 py-3"></label></div><div class="mt-8 flex justify-end"><x-ui.button type="submit">Save changes</x-ui.button></div></form>
</main>
</x-layouts.manager>
@@ -0,0 +1,10 @@
<x-layouts.manager>
<main class="mx-auto max-w-6xl px-5 py-10 sm:px-8 lg:px-12 lg:py-14">
<a href="{{ route('manager.dashboard') }}" class="text-xs font-bold uppercase tracking-widest text-muted">← Businesses</a>
<div class="mt-10 flex flex-col justify-between gap-5 sm:flex-row sm:items-end"><div><p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">Business tenant</p><h1 class="mt-3 text-4xl font-black tracking-tight">{{ $business->name }}</h1><p class="mt-3 text-sm text-muted">{{ $business->slug }}</p></div><x-ui.button variant="secondary" href="{{ route('manager.businesses.edit', $business) }}">Edit business</x-ui.button></div>
@if (session('status'))<div class="mt-8 rounded-tile bg-accent p-4 text-sm font-bold text-ink">{{ session('status') }}</div>@endif
@if (session('owner_invitation_url'))<div class="mt-4 rounded-jumbo border border-amber-200 bg-amber-50 p-6"><p class="text-xs font-bold uppercase tracking-widest text-amber-900">Owner setup link</p><p class="mt-2 text-sm text-amber-900">Share this link securely with the business owner. It expires after seven days and can only be used once.</p><code class="mt-4 block break-all rounded-tile bg-white p-3 text-xs text-ink">{{ session('owner_invitation_url') }}</code></div>@endif
<section class="mt-10 grid gap-4 sm:grid-cols-3"><x-ui.card class="p-5"><p class="text-[10px] font-bold uppercase tracking-widest text-muted">Primary domain</p><p class="mt-3 font-black">{{ $business->primaryDomain?->hostname ?: 'Not configured' }}</p></x-ui.card><x-ui.card class="p-5"><p class="text-[10px] font-bold uppercase tracking-widest text-muted">Timezone</p><p class="mt-3 font-black">{{ $business->timezone }}</p></x-ui.card><x-ui.card class="p-5"><p class="text-[10px] font-bold uppercase tracking-widest text-muted">Defaults</p><p class="mt-3 font-black">{{ $business->hours->count() }} days · {{ $business->staff->count() }} staff</p></x-ui.card></section>
<div class="mt-10 grid gap-8 lg:grid-cols-2"><section><h2 class="text-lg font-black">Domains</h2><div class="mt-4 space-y-3">@foreach ($business->domains as $domain)<div class="flex items-center justify-between rounded-tile border border-line bg-white p-4"><div><p class="font-bold">{{ $domain->hostname }}</p><p class="mt-1 text-xs text-muted">{{ $domain->isPrimary ? 'Primary' : 'Additional' }}</p></div><x-ui.badge tone="{{ $domain->isActive ? 'accent' : 'default' }}">{{ $domain->isActive ? 'Active' : 'Inactive' }}</x-ui.badge></div>@endforeach</div></section><section><h2 class="text-lg font-black">Staff and owners</h2><div class="mt-4 space-y-3">@foreach ($business->staff as $member)<div class="rounded-tile border border-line bg-white p-4"><p class="font-bold">{{ $member->user?->name ?: $member->user?->email }}</p><p class="mt-1 text-xs text-muted">{{ $member->role?->name }} · {{ $member->user?->email }}</p></div>@endforeach</div></section></div>
</main>
</x-layouts.manager>
@@ -0,0 +1,39 @@
<x-layouts.manager>
<main class="mx-auto max-w-7xl px-5 py-10 sm:px-8 lg:px-12 lg:py-14">
<div class="flex flex-col justify-between gap-5 sm:flex-row sm:items-end">
<div>
<p class="text-[10px] font-bold uppercase tracking-[0.3em] text-accent-foreground">Platform operations</p>
<h1 class="mt-3 text-4xl font-black tracking-tight">Businesses</h1>
<p class="mt-3 text-sm text-muted">Create and maintain isolated NextChair tenants.</p>
</div>
<x-ui.button href="{{ route('manager.businesses.create') }}">Create business</x-ui.button>
</div>
@if (session('status'))
<div class="mt-8 rounded-tile bg-accent p-4 text-sm font-bold text-ink">{{ session('status') }}</div>
@endif
<section class="mt-10 grid gap-4 sm:grid-cols-3">
<x-ui.card class="p-5"><p class="text-[10px] font-bold uppercase tracking-widest text-muted">Businesses</p><p class="mt-3 text-3xl font-black">{{ $businessCount }}</p></x-ui.card>
<x-ui.card class="p-5"><p class="text-[10px] font-bold uppercase tracking-widest text-muted">Tenant model</p><p class="mt-3 text-lg font-black">Domain routed</p></x-ui.card>
<x-ui.card class="p-5"><p class="text-[10px] font-bold uppercase tracking-widest text-muted">Provisioning</p><p class="mt-3 text-lg font-black">Owner invited</p></x-ui.card>
</section>
<section class="mt-10 space-y-3">
@forelse ($businesses as $business)
@php($owner = $business->staff->first(fn ($member) => $member->role?->name === 'Owner'))
<a href="{{ route('manager.businesses.show', $business) }}" class="block rounded-jumbo border border-line bg-white p-6 transition hover:border-ink">
<div class="flex flex-col justify-between gap-5 sm:flex-row sm:items-center">
<div>
<div class="flex flex-wrap items-center gap-3"><h2 class="text-xl font-black">{{ $business->name }}</h2><x-ui.badge tone="accent">Active</x-ui.badge></div>
<p class="mt-2 text-sm text-muted">{{ $business->primaryDomain?->hostname ?: 'No primary domain' }} · {{ $business->slug }}</p>
</div>
<div class="text-left text-sm sm:text-right"><p class="font-bold">{{ $owner?->user?->email ?: 'Owner setup pending' }}</p><p class="mt-1 text-xs text-muted">{{ $business->staff_count }} staff · {{ $business->domains_count }} domain{{ $business->domains_count === 1 ? '' : 's' }}</p></div>
</div>
</a>
@empty
<div class="rounded-jumbo border border-dashed border-line bg-white p-12 text-center"><p class="text-lg font-bold">No businesses yet</p><p class="mt-2 text-sm text-muted">Create the first tenant to begin onboarding an owner.</p><a href="{{ route('manager.businesses.create') }}" class="mt-5 inline-block text-xs font-bold uppercase tracking-widest underline">Create a business</a></div>
@endforelse
</section>
</main>
</x-layouts.manager>