Initial Ladill Queue release — enterprise QMS standalone app.
Deploy Ladill Queue / deploy (push) Successful in 56s
Deploy Ladill Queue / deploy (push) Successful in 56s
Phases 1–6: tickets, counters, displays, appointments, workflows, rules, analytics, reports, feedback, admin, device API, and Gitea deploy workflow for queue.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
@props(['title' => 'Ladill Queue', 'heading' => null])
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>{{ $title }} · Ladill Queue</title>
|
||||
@include('partials.favicon')
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="h-full font-sans antialiased bg-slate-100 text-slate-900" x-data="{ sidebarOpen: false }">
|
||||
@include('partials.boot-splash')
|
||||
<div class="min-h-screen max-lg:min-h-dvh">
|
||||
<div x-show="sidebarOpen" x-cloak class="fixed inset-0 z-30 bg-black/50 lg:hidden" @click="sidebarOpen = false"></div>
|
||||
<aside x-data="{ ready: false }" x-init="requestAnimationFrame(() => ready = true)"
|
||||
class="fixed inset-y-0 left-0 z-40 w-64 -translate-x-full transform lg:translate-x-0"
|
||||
:class="{ '!translate-x-0': sidebarOpen, 'transition-transform': ready }">
|
||||
@include('partials.sidebar')
|
||||
</aside>
|
||||
<div class="flex min-h-screen flex-col min-w-0 lg:pl-64">
|
||||
@include('partials.topbar', ['heading' => $heading ?? $title])
|
||||
<main class="flex-1 p-4 pb-24 sm:p-6 lg:pb-6">
|
||||
@include('partials.flash')
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@auth
|
||||
@php
|
||||
$navUser = auth()->user();
|
||||
$navInitials = collect(explode(' ', trim((string) $navUser?->name)))
|
||||
->filter()->take(2)
|
||||
->map(fn ($part) => strtoupper(substr($part, 0, 1)))
|
||||
->implode('');
|
||||
$navAvatarUrl = $navUser && method_exists($navUser, 'avatarUrl')
|
||||
? $navUser->avatarUrl()
|
||||
: ($navUser?->avatar_url ?? null);
|
||||
@endphp
|
||||
@include('partials.mobile-bottom-nav', [
|
||||
'homeUrl' => route('frontdesk.dashboard'),
|
||||
'homeActive' => request()->routeIs('frontdesk.dashboard'),
|
||||
'searchUrl' => route('frontdesk.visitors.index'),
|
||||
'searchActive' => request()->routeIs('frontdesk.visitors.*'),
|
||||
'notificationsUrl' => route('notifications.index'),
|
||||
'notificationsActive' => request()->routeIs('notifications.*'),
|
||||
'unreadUrl' => route('notifications.unread'),
|
||||
'profileActive' => false,
|
||||
'profileName' => $navUser?->name ?? '',
|
||||
'profileSubtitle' => $navUser?->email ?? '',
|
||||
'profileMenuItems' => \App\Support\UserProfileMenu::items($navUser),
|
||||
'avatarUrl' => $navAvatarUrl,
|
||||
'initials' => $navInitials !== '' ? $navInitials : 'U',
|
||||
])
|
||||
@endauth
|
||||
@include('partials.wallet-topup-modal', ['openOnLoad' => (bool) session('topup_required')])
|
||||
@include('partials.afia')
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
@props(['color' => 'slate'])
|
||||
@php
|
||||
$map = [
|
||||
'slate' => 'bg-slate-100 text-slate-700',
|
||||
'indigo' => 'bg-indigo-100 text-indigo-700',
|
||||
'green' => 'bg-green-100 text-green-700',
|
||||
'red' => 'bg-red-100 text-red-700',
|
||||
'amber' => 'bg-amber-100 text-amber-700',
|
||||
'blue' => 'bg-blue-100 text-blue-700',
|
||||
];
|
||||
$classes = $map[$color] ?? $map['slate'];
|
||||
@endphp
|
||||
<span {{ $attributes->merge(['class' => "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium $classes"]) }}>
|
||||
{{ $slot }}
|
||||
</span>
|
||||
@@ -0,0 +1,33 @@
|
||||
@props([
|
||||
'href' => null,
|
||||
'type' => 'button',
|
||||
'label' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$tag = $href ? 'a' : 'button';
|
||||
$ariaLabel = $label ?? trim(preg_replace('/\s+/', ' ', strip_tags((string) $slot)));
|
||||
@endphp
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($href) href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
aria-label="{{ $ariaLabel }}"
|
||||
title="{{ $ariaLabel }}"
|
||||
{{ $attributes->class(['btn-fab h-10 w-10 lg:hidden']) }}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
|
||||
</svg>
|
||||
</{{ $tag }}>
|
||||
|
||||
<{{ $tag }}
|
||||
@if ($href) href="{{ $href }}" @endif
|
||||
@if ($tag === 'button') type="{{ $type }}" @endif
|
||||
{{ $attributes->class(['btn-primary hidden lg:inline-flex']) }}
|
||||
>
|
||||
<svg class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
|
||||
</svg>
|
||||
{{ $slot }}
|
||||
</{{ $tag }}>
|
||||
@@ -0,0 +1,37 @@
|
||||
@props([
|
||||
'name',
|
||||
'label' => null,
|
||||
'type' => 'text',
|
||||
'value' => null,
|
||||
'options' => [],
|
||||
'placeholder' => '',
|
||||
'required' => false,
|
||||
'rows' => 4,
|
||||
])
|
||||
@php
|
||||
$label = $label ?? \Illuminate\Support\Str::headline($name);
|
||||
$current = old($name, $value);
|
||||
$base = 'mt-1 block w-full rounded-xl border-slate-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500';
|
||||
@endphp
|
||||
<div {{ $attributes->only('class') }}>
|
||||
<label for="{{ $name }}" class="block text-sm font-medium text-slate-700">
|
||||
{{ $label }} @if($required)<span class="text-red-500">*</span>@endif
|
||||
</label>
|
||||
|
||||
@if ($type === 'textarea')
|
||||
<textarea id="{{ $name }}" name="{{ $name }}" rows="{{ $rows }}" placeholder="{{ $placeholder }}"
|
||||
class="{{ $base }}">{{ $current }}</textarea>
|
||||
@elseif ($type === 'select')
|
||||
<select id="{{ $name }}" name="{{ $name }}" class="{{ $base }}">
|
||||
@foreach ($options as $optValue => $optLabel)
|
||||
<option value="{{ $optValue }}" @selected((string) $current === (string) $optValue)>{{ $optLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input type="{{ $type }}" id="{{ $name }}" name="{{ $name }}" value="{{ $current }}"
|
||||
placeholder="{{ $placeholder }}" @if($type==='number') step="0.01" min="0" @endif
|
||||
class="{{ $base }}">
|
||||
@endif
|
||||
|
||||
@error($name)<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
@props([
|
||||
'name',
|
||||
'show' => false,
|
||||
'maxWidth' => '2xl'
|
||||
])
|
||||
|
||||
@php
|
||||
$maxWidth = [
|
||||
'sm' => 'sm:max-w-sm',
|
||||
'md' => 'sm:max-w-md',
|
||||
'lg' => 'sm:max-w-lg',
|
||||
'xl' => 'sm:max-w-xl',
|
||||
'2xl' => 'sm:max-w-2xl',
|
||||
][$maxWidth];
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
show: @js($show),
|
||||
focusables() {
|
||||
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
||||
return [...$el.querySelectorAll(selector)]
|
||||
.filter(el => ! el.hasAttribute('disabled'))
|
||||
},
|
||||
firstFocusable() { return this.focusables()[0] },
|
||||
lastFocusable() { return this.focusables().slice(-1)[0] },
|
||||
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
||||
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
||||
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
||||
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
||||
}"
|
||||
x-init="$watch('show', value => {
|
||||
if (value) {
|
||||
document.body.classList.add('overflow-y-hidden');
|
||||
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
|
||||
} else {
|
||||
setTimeout(() => { if (!show) document.body.classList.remove('overflow-y-hidden'); }, 200);
|
||||
}
|
||||
})"
|
||||
x-on:open-modal.window="String($event.detail) === @js($name) ? show = true : null"
|
||||
data-modal-name="{{ $name }}"
|
||||
x-on:close-modal.window="String($event.detail) === @js($name) ? show = false : null"
|
||||
x-on:close.stop="show = false"
|
||||
x-on:keydown.escape.window="show = false"
|
||||
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
||||
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
||||
x-show="show"
|
||||
x-cloak
|
||||
x-transition:enter="transition-opacity ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition-opacity ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center sm:justify-center"
|
||||
style="background: rgba(0,0,0,0.45); backdrop-filter: blur(3px);"
|
||||
>
|
||||
{{-- Backdrop click to close --}}
|
||||
<div class="absolute inset-0" x-on:click="show = false" data-close-modal="{{ $name }}"></div>
|
||||
|
||||
{{-- Panel: bottom-sheet on mobile, centered card on sm+ --}}
|
||||
<div
|
||||
x-show="show"
|
||||
class="relative z-10 w-full max-h-[90vh] overflow-y-auto rounded-t-3xl bg-white shadow-2xl sm:mb-6 sm:max-h-[85vh] sm:rounded-2xl {{ $maxWidth }} sm:border sm:border-gray-200"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
|
||||
x-transition:enter-end="translate-y-0 sm:opacity-100 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="translate-y-0 sm:opacity-100 sm:scale-100"
|
||||
x-transition:leave-end="translate-y-full sm:translate-y-0 sm:opacity-0 sm:scale-95"
|
||||
x-on:click.stop
|
||||
>
|
||||
{{-- Drag handle (mobile only) --}}
|
||||
<div class="flex justify-center pb-1 pt-3 sm:hidden">
|
||||
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
|
||||
{{-- Close button (desktop only) --}}
|
||||
<button @click="show = false" data-close-modal="{{ $name }}" type="button"
|
||||
class="absolute right-3 top-3 z-10 hidden h-8 w-8 items-center justify-center rounded-full text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 sm:flex">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user