Move Devices under Settings like Branches and Team.
Deploy Ladill POS / deploy (push) Successful in 29s

Drop Devices from the main sidebar, surface a Devices card on the Settings
page, and nest device routes under /settings/devices with legacy redirects.
This commit is contained in:
isaacclad
2026-07-15 23:56:03 +00:00
parent 29f2c96c2b
commit 4546c2ab8c
8 changed files with 212 additions and 155 deletions
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Pos;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Controllers\Pos\Concerns\ScopesToAccount; use App\Http\Controllers\Pos\Concerns\ScopesToAccount;
use App\Models\PosDevice;
use App\Models\PosLocation; use App\Models\PosLocation;
use App\Models\PosMember; use App\Models\PosMember;
use App\Models\PaymentGatewaySetting; use App\Models\PaymentGatewaySetting;
@@ -41,9 +42,14 @@ class SettingsController extends Controller
? PosLocation::owned($owner)->orderBy('name')->get() ? PosLocation::owned($owner)->orderBy('name')->get()
: PosLocation::owned($owner)->whereKey($scope)->orderBy('name')->get(); : PosLocation::owned($owner)->whereKey($scope)->orderBy('name')->get();
$devicesQuery = PosDevice::owned($owner)->with('location')->orderBy('name');
$this->scopeToLocation($request, $devicesQuery);
return view('pos.settings', [ return view('pos.settings', [
'location' => $location, 'location' => $location,
'branches' => $branches, 'branches' => $branches,
'devices' => $devicesQuery->limit(10)->get(),
'deviceTypes' => config('pos.device_types', []),
'members' => PosMember::owned($owner)->with('location')->orderBy('created_at')->get(), 'members' => PosMember::owned($owner)->with('location')->orderBy('created_at')->get(),
'roles' => config('pos.roles', []), 'roles' => config('pos.roles', []),
'hasMultiLocation' => $this->subscriptions->canUseMultiLocation($account), 'hasMultiLocation' => $this->subscriptions->canUseMultiLocation($account),
+5 -4
View File
@@ -14,8 +14,6 @@
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />'], 'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />'],
['name' => 'Sales', 'route' => route('pos.sales.index'), 'active' => request()->routeIs('pos.sales.*'), ['name' => 'Sales', 'route' => route('pos.sales.index'), 'active' => request()->routeIs('pos.sales.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />'], 'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />'],
['name' => 'Devices', 'route' => route('pos.devices.index'), 'active' => request()->routeIs('pos.devices*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z" />'],
]; ];
// Restaurant mode adds the floor + kitchen display, right after Register. // Restaurant mode adds the floor + kitchen display, right after Register.
@@ -40,9 +38,12 @@
</nav> </nav>
<div class="shrink-0 border-t border-slate-100 px-3 py-3"> <div class="shrink-0 border-t border-slate-100 px-3 py-3">
@php
$settingsActive = request()->routeIs('pos.settings*', 'pos.devices.*', 'pos.branches.*', 'pos.team.*');
@endphp
<a href="{{ route('pos.settings') }}" <a href="{{ route('pos.settings') }}"
class="group flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] transition {{ request()->routeIs('pos.settings*') ? '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('pos.settings*') ? '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-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-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.28Z" /><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-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-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.28Z" /><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>
+51 -44
View File
@@ -1,51 +1,58 @@
<x-app-layout title="Add device"> <x-app-layout title="Add device">
<div class="mx-auto max-w-lg space-y-6"> <x-settings.page description="Name the hardware and link it to a branch when needed.">
<div> <div class="mb-4 flex flex-wrap items-center gap-1 text-sm text-slate-500">
<a href="{{ route('pos.devices.index') }}" class="text-sm font-medium text-slate-500 hover:text-slate-800"> Devices</a> <a href="{{ route('pos.settings') }}" class="hover:text-slate-800">Settings</a>
<h1 class="mt-2 text-2xl font-semibold text-slate-900">Register device</h1> <span class="px-1">/</span>
<p class="mt-1 text-sm text-slate-500">Name the hardware and link it to a branch when needed.</p> <a href="{{ route('pos.devices.index') }}" class="hover:text-slate-800">Devices</a>
<span class="px-1">/</span>
<span class="font-medium text-slate-900">Add</span>
</div> </div>
@if (session('error')) @if (session('error'))
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">{{ session('error') }}</div> <div class="mb-4 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">{{ session('error') }}</div>
@endif @endif
<form method="POST" action="{{ route('pos.devices.store') }}" <x-settings.card title="Register device">
class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6" <form method="POST" action="{{ route('pos.devices.store') }}"
x-data="{ type: @js(old('type', 'register')), hints: @js($hints) }"> class="space-y-4"
@csrf x-data="{ type: @js(old('type', 'register')), hints: @js($hints) }">
<div> @csrf
<label class="block text-sm font-medium text-slate-700">Name</label> <div>
<input type="text" name="name" value="{{ old('name') }}" required placeholder="Front till · Customer screen" <label class="block text-sm font-medium text-slate-700">Name</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <input type="text" name="name" value="{{ old('name') }}" required placeholder="Front till · Customer screen"
@error('name') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div> @error('name') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror
<div> </div>
<label class="block text-sm font-medium text-slate-700">Type</label> <div>
<select name="type" required x-model="type" <label class="block text-sm font-medium text-slate-700">Type</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="type" required x-model="type"
@foreach ($deviceTypes as $key => $label) class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="{{ $key }}">{{ $label }}</option> @foreach ($deviceTypes as $key => $label)
@endforeach <option value="{{ $key }}">{{ $label }}</option>
</select> @endforeach
<p class="mt-2 text-xs text-slate-500" x-text="hints[type] || ''"></p> </select>
@error('type') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror <p class="mt-2 text-xs text-slate-500" x-text="hints[type] || ''"></p>
</div> @error('type') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror
<div> </div>
<label class="block text-sm font-medium text-slate-700">Branch</label> <div>
<select name="location_id" <label class="block text-sm font-medium text-slate-700">Branch</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="location_id"
<option value=""> Optional / all branches </option> class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach ($locations as $location) <option value=""> Optional / all branches </option>
<option value="{{ $location->id }}" @selected((string) old('location_id', $defaultLocationId) === (string) $location->id)> @foreach ($locations as $location)
{{ $location->name }} <option value="{{ $location->id }}" @selected((string) old('location_id', $defaultLocationId) === (string) $location->id)>
</option> {{ $location->name }}
@endforeach </option>
</select> @endforeach
<p class="mt-1 text-xs text-slate-400">Required for customer and kitchen displays.</p> </select>
@error('location_id') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror <p class="mt-1 text-xs text-slate-400">Required for customer and kitchen displays.</p>
</div> @error('location_id') <p class="mt-1 text-xs text-rose-600">{{ $message }}</p> @enderror
<button type="submit" class="btn-primary w-full">Register device</button> </div>
</form> <div class="flex flex-wrap gap-3 pt-1">
</div> <a href="{{ route('pos.settings') }}#devices" class="inline-flex items-center justify-center rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50">Cancel</a>
<button type="submit" class="btn-primary">Register device</button>
</div>
</form>
</x-settings.card>
</x-settings.page>
</x-app-layout> </x-app-layout>
+88 -83
View File
@@ -1,96 +1,101 @@
<x-app-layout title="Edit device"> <x-app-layout title="Edit device">
<div class="mx-auto max-w-lg space-y-6"> <x-settings.page description="{{ ($deviceTypes[$device->type] ?? $device->type) }} · {{ $device->location?->name ?? 'No branch' }}">
<div> <div class="mb-4 flex flex-wrap items-center gap-1 text-sm text-slate-500">
<a href="{{ route('pos.devices.index') }}" class="text-sm font-medium text-slate-500 hover:text-slate-800"> Devices</a> <a href="{{ route('pos.settings') }}" class="hover:text-slate-800">Settings</a>
<h1 class="mt-2 text-2xl font-semibold text-slate-900">Edit device</h1> <span class="px-1">/</span>
<p class="mt-1 text-sm text-slate-500">{{ $deviceTypes[$device->type] ?? $device->type }} · {{ $device->location?->name ?? 'No branch' }}</p> <a href="{{ route('pos.devices.index') }}" class="hover:text-slate-800">Devices</a>
<span class="px-1">/</span>
<span class="font-medium text-slate-900">{{ $device->name }}</span>
</div> </div>
@if (session('success')) @if (session('success'))
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div> <div class="mb-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif @endif
<form method="POST" action="{{ route('pos.devices.update', $device) }}" <x-settings.card title="Edit {{ $device->name }}">
class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6" <form method="POST" action="{{ route('pos.devices.update', $device) }}"
x-data="{ type: @js(old('type', $device->type)), hints: @js($hints) }"> class="space-y-4"
@csrf x-data="{ type: @js(old('type', $device->type)), hints: @js($hints) }">
@method('PUT') @csrf
<div> @method('PUT')
<label class="block text-sm font-medium text-slate-700">Name</label> <div>
<input type="text" name="name" value="{{ old('name', $device->name) }}" required <label class="block text-sm font-medium text-slate-700">Name</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <input type="text" name="name" value="{{ old('name', $device->name) }}" required
</div> class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<div> </div>
<label class="block text-sm font-medium text-slate-700">Type</label> <div>
<select name="type" required x-model="type" <label class="block text-sm font-medium text-slate-700">Type</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="type" required x-model="type"
@foreach ($deviceTypes as $key => $label) class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="{{ $key }}">{{ $label }}</option> @foreach ($deviceTypes as $key => $label)
@endforeach <option value="{{ $key }}">{{ $label }}</option>
</select> @endforeach
<p class="mt-2 text-xs text-slate-500" x-text="hints[type] || ''"></p> </select>
</div> <p class="mt-2 text-xs text-slate-500" x-text="hints[type] || ''"></p>
<div> </div>
<label class="block text-sm font-medium text-slate-700">Status</label> <div>
<select name="status" <label class="block text-sm font-medium text-slate-700">Status</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="status"
@foreach (['online', 'offline', 'maintenance'] as $status) class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="{{ $status }}" @selected(old('status', $device->status) === $status)>{{ ucfirst($status) }}</option> @foreach (['online', 'offline', 'maintenance'] as $status)
@endforeach <option value="{{ $status }}" @selected(old('status', $device->status) === $status)>{{ ucfirst($status) }}</option>
</select> @endforeach
</div> </select>
<div> </div>
<label class="block text-sm font-medium text-slate-700">Branch</label> <div>
<select name="location_id" <label class="block text-sm font-medium text-slate-700">Branch</label>
class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="location_id"
<option value=""> Optional / all branches </option> class="mt-1.5 w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@foreach ($locations as $location) <option value=""> Optional / all branches </option>
<option value="{{ $location->id }}" @selected((string) old('location_id', $device->location_id) === (string) $location->id)> @foreach ($locations as $location)
{{ $location->name }} <option value="{{ $location->id }}" @selected((string) old('location_id', $device->location_id) === (string) $location->id)>
</option> {{ $location->name }}
@endforeach </option>
</select> @endforeach
</div> </select>
@if ($device->device_token)
<div class="rounded-xl border border-slate-100 bg-slate-50 p-4">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Device token</p>
<code class="mt-2 block break-all text-xs text-slate-700">{{ $device->device_token }}</code>
@if ($openUrl)
<a href="{{ $openUrl }}" target="_blank" rel="noopener"
class="mt-3 inline-flex text-sm font-semibold text-indigo-600 hover:text-indigo-800">
Open device URL
</a>
@endif
<p class="mt-2 text-xs text-slate-500">Last online: {{ $device->last_online_at?->diffForHumans() ?? 'Never' }}</p>
</div> </div>
@endif
<div class="flex gap-3 pt-1"> @if ($device->device_token)
<a href="{{ route('pos.devices.index') }}" class="inline-flex flex-1 items-center justify-center rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a> <div class="rounded-xl border border-slate-100 bg-slate-50 p-4">
<button type="submit" class="btn-primary flex-1">Save</button> <p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Device token</p>
</div> <code class="mt-2 block break-all text-xs text-slate-700">{{ $device->device_token }}</code>
</form> @if ($openUrl)
<a href="{{ $openUrl }}" target="_blank" rel="noopener"
class="mt-3 inline-flex text-sm font-semibold text-indigo-600 hover:text-indigo-800">
Open device URL
</a>
@endif
<p class="mt-2 text-xs text-slate-500">Last online: {{ $device->last_online_at?->diffForHumans() ?? 'Never' }}</p>
</div>
@endif
<div class="space-y-3 rounded-2xl border border-rose-100 bg-white p-5"> <div class="flex flex-wrap gap-3 pt-1">
<p class="text-sm font-semibold text-slate-900">Device actions</p> <a href="{{ route('pos.settings') }}#devices" class="inline-flex items-center justify-center rounded-xl border border-slate-200 px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
@if ($device->device_token) <button type="submit" class="btn-primary">Save</button>
<form method="POST" action="{{ route('pos.devices.regenerate-token', $device) }}" </div>
onsubmit="return confirm('Regenerate token? Existing display sessions will stop until reopened.');"> </form>
</x-settings.card>
<x-settings.card title="Device actions" class="mt-6">
<div class="space-y-3">
@if ($device->device_token)
<form method="POST" action="{{ route('pos.devices.regenerate-token', $device) }}"
onsubmit="return confirm('Regenerate token? Existing display sessions will stop until reopened.');">
@csrf
<button type="submit" class="w-full rounded-xl border border-amber-200 bg-amber-50 px-4 py-2.5 text-sm font-medium text-amber-900 hover:bg-amber-100 sm:w-auto">
Regenerate token
</button>
</form>
@endif
<form method="POST" action="{{ route('pos.devices.destroy', $device) }}"
onsubmit="return confirm('Remove this device?');">
@csrf @csrf
<button type="submit" class="w-full rounded-xl border border-amber-200 bg-amber-50 px-4 py-2.5 text-sm font-medium text-amber-900 hover:bg-amber-100"> @method('DELETE')
Regenerate token <button type="submit" class="w-full rounded-xl border border-rose-200 bg-rose-50 px-4 py-2.5 text-sm font-medium text-rose-800 hover:bg-rose-100 sm:w-auto">
Remove device
</button> </button>
</form> </form>
@endif </div>
<form method="POST" action="{{ route('pos.devices.destroy', $device) }}" </x-settings.card>
onsubmit="return confirm('Remove this device?');"> </x-settings.page>
@csrf
@method('DELETE')
<button type="submit" class="w-full rounded-xl border border-rose-200 bg-rose-50 px-4 py-2.5 text-sm font-medium text-rose-800 hover:bg-rose-100">
Remove device
</button>
</form>
</div>
</div>
</x-app-layout> </x-app-layout>
+13 -16
View File
@@ -1,15 +1,15 @@
<x-app-layout title="Devices"> <x-app-layout title="Devices">
<div class="mx-auto max-w-5xl space-y-6"> <x-settings.page description="Register tills, customer screens, kitchen displays, and printers for each branch.">
<div class="flex flex-wrap items-end justify-between gap-4"> <div class="mb-4 flex flex-wrap items-center justify-between gap-3 text-sm">
<div> <div class="flex flex-wrap items-center gap-1 text-slate-500">
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Hardware</p> <a href="{{ route('pos.settings') }}" class="hover:text-slate-800">Settings</a>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Devices</h1> <span class="px-1">/</span>
<p class="mt-1 text-sm text-slate-500">Register tills, customer screens, kitchen displays, and printers for each branch.</p> <span class="font-medium text-slate-900">Devices</span>
</div> </div>
<a href="{{ route('pos.devices.create') }}" class="btn-primary">Add device</a> <a href="{{ route('pos.devices.create') }}" class="btn-primary">Add device</a>
</div> </div>
<div class="grid gap-3 sm:grid-cols-3"> <div class="mb-6 grid gap-3 sm:grid-cols-3">
<div class="rounded-2xl border border-slate-200 bg-white px-5 py-4"> <div class="rounded-2xl border border-slate-200 bg-white px-5 py-4">
<p class="text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($heroStats['total']) }}</p> <p class="text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($heroStats['total']) }}</p>
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-slate-500">Devices</p> <p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-slate-500">Devices</p>
@@ -25,16 +25,13 @@
</div> </div>
@if (session('success')) @if (session('success'))
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div> <div class="mb-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">{{ session('success') }}</div>
@endif @endif
@if (session('error')) @if (session('error'))
<div class="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">{{ session('error') }}</div> <div class="mb-4 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">{{ session('error') }}</div>
@endif @endif
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white"> <x-settings.card title="All devices" description="Hardware registered to this POS account.">
<div class="border-b border-slate-100 px-6 py-4">
<h2 class="text-sm font-semibold text-slate-900">All devices</h2>
</div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100 text-sm"> <table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500"> <thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
@@ -75,8 +72,8 @@
</table> </table>
</div> </div>
@if ($devices->hasPages()) @if ($devices->hasPages())
<div class="border-t border-slate-100 px-5 py-3">{{ $devices->links() }}</div> <div class="border-t border-slate-100 px-1 pt-3">{{ $devices->links() }}</div>
@endif @endif
</div> </x-settings.card>
</div> </x-settings.page>
</x-app-layout> </x-app-layout>
+3 -1
View File
@@ -1,5 +1,5 @@
<x-app-layout title="Settings"> <x-app-layout title="Settings">
<x-settings.page description="Configure this branch, manage locations, and invite team members."> <x-settings.page description="Configure this branch, manage locations, devices, and invite team members.">
<x-settings.card title="This branch" description="Register name, currency, service style, and receipt options for {{ $location->name }}."> <x-settings.card title="This branch" description="Register name, currency, service style, and receipt options for {{ $location->name }}.">
<form method="POST" action="{{ route('pos.settings.update') }}" enctype="multipart/form-data" class="space-y-4"> <form method="POST" action="{{ route('pos.settings.update') }}" enctype="multipart/form-data" class="space-y-4">
@csrf @csrf
@@ -179,6 +179,8 @@
<div class="mt-6 space-y-6"> <div class="mt-6 space-y-6">
@include('pos.settings._branches-section') @include('pos.settings._branches-section')
@include('pos.settings._devices-section')
@if (! empty($canManageTeam)) @if (! empty($canManageTeam))
@include('pos.settings._team-section') @include('pos.settings._team-section')
@endif @endif
@@ -0,0 +1,35 @@
<x-settings.card id="devices" title="Devices" description="Register tills, customer screens, kitchen displays, and printers for each branch.">
@if ($devices->isNotEmpty())
<ul class="divide-y divide-slate-100">
@foreach ($devices as $device)
<li class="flex items-center justify-between gap-4 py-3 first:pt-0 last:pb-0">
<div class="min-w-0">
<p class="font-medium text-slate-900">
{{ $device->name }}
<span class="ml-2 inline-flex rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase {{ $device->isOnline() ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500' }}">
{{ $device->isOnline() ? 'Online' : ucfirst($device->status) }}
</span>
</p>
<p class="mt-0.5 text-xs text-slate-500">
{{ $deviceTypes[$device->type] ?? $device->type }}
· {{ $device->location?->name ?? 'All branches' }}
@if ($device->last_online_at)
· last online {{ $device->last_online_at->diffForHumans() }}
@endif
</p>
</div>
<a href="{{ route('pos.devices.edit', $device) }}" class="shrink-0 text-sm font-medium text-indigo-600 hover:text-indigo-800">Manage</a>
</li>
@endforeach
</ul>
@else
<p class="text-sm text-slate-500">No devices yet. Add a register, customer display, kitchen screen, or printer.</p>
@endif
<div class="mt-4 flex flex-wrap items-center gap-3 border-t border-slate-100 pt-4">
<a href="{{ route('pos.devices.create') }}" class="btn-primary">Add device</a>
@if ($devices->isNotEmpty())
<a href="{{ route('pos.devices.index') }}" class="text-sm font-medium text-slate-600 hover:text-slate-900">View all devices</a>
@endif
</div>
</x-settings.card>
+11 -7
View File
@@ -153,13 +153,17 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
Route::get('/settings', [SettingsController::class, 'index'])->name('pos.settings'); Route::get('/settings', [SettingsController::class, 'index'])->name('pos.settings');
Route::put('/settings', [SettingsController::class, 'update'])->name('pos.settings.update'); Route::put('/settings', [SettingsController::class, 'update'])->name('pos.settings.update');
Route::get('/devices', [DeviceController::class, 'index'])->name('pos.devices.index'); Route::get('/settings/devices', [DeviceController::class, 'index'])->name('pos.devices.index');
Route::get('/devices/create', [DeviceController::class, 'create'])->name('pos.devices.create'); Route::get('/settings/devices/create', [DeviceController::class, 'create'])->name('pos.devices.create');
Route::post('/devices', [DeviceController::class, 'store'])->name('pos.devices.store'); Route::post('/settings/devices', [DeviceController::class, 'store'])->name('pos.devices.store');
Route::get('/devices/{device}/edit', [DeviceController::class, 'edit'])->name('pos.devices.edit'); Route::get('/settings/devices/{device}/edit', [DeviceController::class, 'edit'])->name('pos.devices.edit');
Route::put('/devices/{device}', [DeviceController::class, 'update'])->name('pos.devices.update'); Route::put('/settings/devices/{device}', [DeviceController::class, 'update'])->name('pos.devices.update');
Route::delete('/devices/{device}', [DeviceController::class, 'destroy'])->name('pos.devices.destroy'); Route::delete('/settings/devices/{device}', [DeviceController::class, 'destroy'])->name('pos.devices.destroy');
Route::post('/devices/{device}/regenerate-token', [DeviceController::class, 'regenerateToken'])->name('pos.devices.regenerate-token'); Route::post('/settings/devices/{device}/regenerate-token', [DeviceController::class, 'regenerateToken'])->name('pos.devices.regenerate-token');
// Legacy /devices URLs → settings
Route::redirect('/devices', '/settings/devices');
Route::redirect('/devices/create', '/settings/devices/create');
Route::get('/devices/{device}/edit', fn (string $device) => redirect("/settings/devices/{$device}/edit"));
Route::get('/settings/branches', [LocationController::class, 'index'])->name('pos.branches.index'); Route::get('/settings/branches', [LocationController::class, 'index'])->name('pos.branches.index');
Route::get('/settings/branches/create', [LocationController::class, 'create'])->name('pos.branches.create'); Route::get('/settings/branches/create', [LocationController::class, 'create'])->name('pos.branches.create');