Initial Ladill Queue release — enterprise QMS standalone app.
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:
isaacclad
2026-06-29 20:19:52 +00:00
co-authored by Cursor
commit cca98eefd2
297 changed files with 27263 additions and 0 deletions
@@ -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>