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
+60
View File
@@ -0,0 +1,60 @@
<?php
use App\Http\Controllers\Api\AnalyticsController;
use App\Http\Controllers\Api\AppointmentController;
use App\Http\Controllers\Api\BranchController;
use App\Http\Controllers\Api\CounterController;
use App\Http\Controllers\Api\DeviceHeartbeatController;
use App\Http\Controllers\Api\PublicQueueController;
use App\Http\Controllers\Api\QueueRuleController;
use App\Http\Controllers\Api\ReportController;
use App\Http\Controllers\Api\ServiceEventController;
use App\Http\Controllers\Api\ServiceQueueController;
use App\Http\Controllers\Api\TicketController;
use App\Http\Controllers\Api\WorkflowController;
use Illuminate\Support\Facades\Route;
Route::get('/health', fn () => response()->json(['status' => 'ok', 'app' => 'queue']));
Route::post('/devices/heartbeat', DeviceHeartbeatController::class)->name('api.devices.heartbeat');
Route::post('/service-events', ServiceEventController::class)->name('api.service-events');
Route::prefix('v1/public')->group(function () {
Route::post('/join', [PublicQueueController::class, 'join'])->name('api.public.join');
Route::get('/tickets/{qrToken}', [PublicQueueController::class, 'show'])->name('api.public.ticket');
Route::post('/tickets/{qrToken}/delay', [PublicQueueController::class, 'delay'])->name('api.public.ticket.delay');
Route::post('/tickets/{qrToken}/cancel', [PublicQueueController::class, 'cancel'])->name('api.public.ticket.cancel');
});
Route::middleware(['auth:sanctum', 'qms.setup'])->prefix('v1')->group(function () {
Route::get('/queues', [ServiceQueueController::class, 'index'])->name('api.queues.index');
Route::get('/queues/{serviceQueue}/waiting', [ServiceQueueController::class, 'waiting'])->name('api.queues.waiting');
Route::post('/queues/{serviceQueue}/pause', [ServiceQueueController::class, 'pause'])->name('api.queues.pause');
Route::post('/queues/{serviceQueue}/resume', [ServiceQueueController::class, 'resume'])->name('api.queues.resume');
Route::get('/tickets', [TicketController::class, 'index'])->name('api.tickets.index');
Route::post('/tickets', [TicketController::class, 'store'])->name('api.tickets.store');
Route::get('/tickets/{ticket}', [TicketController::class, 'show'])->name('api.tickets.show');
Route::post('/tickets/{ticket}/action', [TicketController::class, 'action'])->name('api.tickets.action');
Route::post('/queues/{serviceQueue}/call-next', [TicketController::class, 'callNext'])->name('api.queues.call-next');
Route::get('/counters', [CounterController::class, 'index'])->name('api.counters.index');
Route::post('/counters', [CounterController::class, 'store'])->name('api.counters.store');
Route::get('/appointments', [AppointmentController::class, 'index'])->name('api.appointments.index');
Route::post('/appointments', [AppointmentController::class, 'store'])->name('api.appointments.store');
Route::post('/appointments/{appointment}/check-in', [AppointmentController::class, 'checkIn'])->name('api.appointments.check-in');
Route::get('/workflows', [WorkflowController::class, 'index'])->name('api.workflows.index');
Route::get('/workflows/{workflow}', [WorkflowController::class, 'show'])->name('api.workflows.show');
Route::get('/queues/{serviceQueue}/rules', [QueueRuleController::class, 'index'])->name('api.rules.index');
Route::post('/queues/{serviceQueue}/rules', [QueueRuleController::class, 'store'])->name('api.rules.store');
Route::get('/analytics/overview', [AnalyticsController::class, 'overview'])->name('api.analytics.overview');
Route::get('/reports/{type}', [ReportController::class, 'show'])->name('api.reports.show');
Route::get('/branches', [BranchController::class, 'index'])->name('api.branches.index');
Route::post('/branches', [BranchController::class, 'store'])->name('api.branches.store');
});
+12
View File
@@ -0,0 +1,12 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::command('qms:mark-devices-offline')->everyFiveMinutes();
Schedule::command('qms:send-appointment-reminders')->everyFifteenMinutes();
+168
View File
@@ -0,0 +1,168 @@
<?php
use App\Http\Controllers\Auth\SsoLoginController;
use App\Http\Controllers\NotificationController;
use App\Http\Controllers\Qms\AnalyticsController;
use App\Http\Controllers\Qms\AppointmentController;
use App\Http\Controllers\Qms\AuditLogController;
use App\Http\Controllers\Qms\BranchController;
use App\Http\Controllers\Qms\ConsoleController;
use App\Http\Controllers\Qms\CounterController;
use App\Http\Controllers\Qms\DashboardController;
use App\Http\Controllers\Qms\DepartmentController;
use App\Http\Controllers\Qms\DeviceController;
use App\Http\Controllers\Qms\DisplayPublicController;
use App\Http\Controllers\Qms\DisplayScreenController;
use App\Http\Controllers\Qms\FeedbackAdminController;
use App\Http\Controllers\Qms\FeedbackController;
use App\Http\Controllers\Qms\KioskDeviceController;
use App\Http\Controllers\Qms\MemberController;
use App\Http\Controllers\Qms\MobileQueueController;
use App\Http\Controllers\Qms\OnboardingController;
use App\Http\Controllers\Qms\QueueBoardController;
use App\Http\Controllers\Qms\QueueRuleController;
use App\Http\Controllers\Qms\ReportController;
use App\Http\Controllers\Qms\ServiceQueueController;
use App\Http\Controllers\Qms\SettingsController;
use App\Http\Controllers\Qms\TicketController;
use App\Http\Controllers\Qms\WorkflowController;
use Illuminate\Support\Facades\Route;
Route::get('/', fn () => auth()->check()
? redirect()->route('qms.dashboard')
: redirect()->route('sso.connect'))->name('qms.root');
Route::get('/login', [SsoLoginController::class, 'connect'])->name('login');
Route::get('/sso/connect', [SsoLoginController::class, 'connect'])->name('sso.connect');
Route::get('/sso/callback', [SsoLoginController::class, 'callback'])->name('sso.callback');
Route::get('/sso/error', [SsoLoginController::class, 'failed'])->name('sso.failed');
Route::post('/logout', [SsoLoginController::class, 'logout'])->name('logout');
Route::get('/sso/logout-bridge', [SsoLoginController::class, 'logoutBridge'])->name('sso.logout-bridge');
Route::get('/sso/logout-frontchannel', [SsoLoginController::class, 'frontchannelLogout'])->name('sso.logout-frontchannel');
Route::get('/sso/platform-signed-out', [SsoLoginController::class, 'platformSignedOut'])->name('sso.platform-signed-out');
Route::get('/signed-out', fn () => auth()->check() ? redirect()->route('qms.dashboard') : view('auth.signed-out'))->name('qms.signed-out');
// Public: digital display
Route::get('/display/{token}', [DisplayPublicController::class, 'show'])->name('qms.display.public');
Route::get('/display/{token}/data', [DisplayPublicController::class, 'data'])->name('qms.display.data');
// Public: mobile queue
Route::get('/join', [MobileQueueController::class, 'joinForm'])->name('qms.mobile.join');
Route::post('/join', [MobileQueueController::class, 'join'])->name('qms.mobile.join.store');
Route::get('/m/{qrToken}', [MobileQueueController::class, 'show'])->name('qms.mobile.show');
Route::get('/m/{qrToken}/data', [MobileQueueController::class, 'data'])->name('qms.mobile.data');
Route::post('/m/{qrToken}/delay', [MobileQueueController::class, 'delay'])->name('qms.mobile.delay');
Route::post('/m/{qrToken}/cancel', [MobileQueueController::class, 'cancel'])->name('qms.mobile.cancel');
// Public: customer feedback
Route::get('/feedback/{qrToken}', [FeedbackController::class, 'show'])->name('qms.feedback.show');
Route::post('/feedback/{qrToken}', [FeedbackController::class, 'store'])->name('qms.feedback.store');
// Public: kiosk device
Route::middleware(['qms.device:kiosk', 'throttle:60,1'])->prefix('kiosk/d')->group(function () {
Route::get('/{token}', [KioskDeviceController::class, 'show'])->name('qms.kiosk.device');
Route::post('/{token}/issue', [KioskDeviceController::class, 'issue'])->name('qms.kiosk.device.issue');
});
Route::middleware(['auth', 'platform.session'])->group(function () {
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
Route::get('/notifications/unread', [NotificationController::class, 'unread'])->name('notifications.unread');
Route::post('/notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.mark-read');
Route::post('/notifications/mark-all-read', [NotificationController::class, 'markAllAsRead'])->name('notifications.mark-all-read');
Route::get('/onboarding', [OnboardingController::class, 'show'])->name('qms.onboarding.show');
Route::post('/onboarding', [OnboardingController::class, 'store'])->name('qms.onboarding.store');
Route::middleware(['qms.setup'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('qms.dashboard');
Route::get('/board', [QueueBoardController::class, 'index'])->name('qms.board.index');
Route::get('/queues', [ServiceQueueController::class, 'index'])->name('qms.queues.index');
Route::get('/queues/create', [ServiceQueueController::class, 'create'])->name('qms.queues.create');
Route::post('/queues', [ServiceQueueController::class, 'store'])->name('qms.queues.store');
Route::get('/queues/{serviceQueue}', [ServiceQueueController::class, 'show'])->name('qms.queues.show');
Route::get('/queues/{serviceQueue}/edit', [ServiceQueueController::class, 'edit'])->name('qms.queues.edit');
Route::put('/queues/{serviceQueue}', [ServiceQueueController::class, 'update'])->name('qms.queues.update');
Route::post('/queues/{serviceQueue}/pause', [ServiceQueueController::class, 'pause'])->name('qms.queues.pause');
Route::post('/queues/{serviceQueue}/resume', [ServiceQueueController::class, 'resume'])->name('qms.queues.resume');
Route::get('/tickets', [TicketController::class, 'index'])->name('qms.tickets.index');
Route::get('/tickets/create', [TicketController::class, 'create'])->name('qms.tickets.create');
Route::post('/tickets', [TicketController::class, 'store'])->name('qms.tickets.store');
Route::get('/tickets/{ticket}', [TicketController::class, 'show'])->name('qms.tickets.show');
Route::get('/tickets/{ticket}/print', [TicketController::class, 'print'])->name('qms.tickets.print');
Route::get('/counters', [CounterController::class, 'index'])->name('qms.counters.index');
Route::get('/counters/create', [CounterController::class, 'create'])->name('qms.counters.create');
Route::post('/counters', [CounterController::class, 'store'])->name('qms.counters.store');
Route::get('/counters/{counter}', [CounterController::class, 'show'])->name('qms.counters.show');
Route::get('/counters/{counter}/edit', [CounterController::class, 'edit'])->name('qms.counters.edit');
Route::put('/counters/{counter}', [CounterController::class, 'update'])->name('qms.counters.update');
Route::get('/console/{counter}', [ConsoleController::class, 'show'])->name('qms.console.show');
Route::post('/console/{counter}/action', [ConsoleController::class, 'action'])->name('qms.console.action');
Route::get('/displays', [DisplayScreenController::class, 'index'])->name('qms.displays.index');
Route::get('/displays/create', [DisplayScreenController::class, 'create'])->name('qms.displays.create');
Route::post('/displays', [DisplayScreenController::class, 'store'])->name('qms.displays.store');
Route::get('/displays/{display}', [DisplayScreenController::class, 'show'])->name('qms.displays.show');
Route::get('/displays/{display}/edit', [DisplayScreenController::class, 'edit'])->name('qms.displays.edit');
Route::put('/displays/{display}', [DisplayScreenController::class, 'update'])->name('qms.displays.update');
Route::get('/devices', [DeviceController::class, 'index'])->name('qms.devices.index');
Route::get('/devices/create', [DeviceController::class, 'create'])->name('qms.devices.create');
Route::post('/devices', [DeviceController::class, 'store'])->name('qms.devices.store');
Route::post('/devices/{device}/regenerate-token', [DeviceController::class, 'regenerateToken'])->name('qms.devices.regenerate-token');
Route::get('/appointments', [AppointmentController::class, 'index'])->name('qms.appointments.index');
Route::get('/appointments/create', [AppointmentController::class, 'create'])->name('qms.appointments.create');
Route::post('/appointments', [AppointmentController::class, 'store'])->name('qms.appointments.store');
Route::get('/appointments/{appointment}/edit', [AppointmentController::class, 'edit'])->name('qms.appointments.edit');
Route::put('/appointments/{appointment}', [AppointmentController::class, 'update'])->name('qms.appointments.update');
Route::post('/appointments/{appointment}/check-in', [AppointmentController::class, 'checkIn'])->name('qms.appointments.check-in');
Route::post('/appointments/{appointment}/cancel', [AppointmentController::class, 'cancel'])->name('qms.appointments.cancel');
Route::get('/workflows', [WorkflowController::class, 'index'])->name('qms.workflows.index');
Route::get('/workflows/create', [WorkflowController::class, 'create'])->name('qms.workflows.create');
Route::post('/workflows', [WorkflowController::class, 'store'])->name('qms.workflows.store');
Route::get('/workflows/{workflow}', [WorkflowController::class, 'show'])->name('qms.workflows.show');
Route::get('/workflows/{workflow}/edit', [WorkflowController::class, 'edit'])->name('qms.workflows.edit');
Route::put('/workflows/{workflow}', [WorkflowController::class, 'update'])->name('qms.workflows.update');
Route::delete('/workflows/{workflow}', [WorkflowController::class, 'destroy'])->name('qms.workflows.destroy');
Route::get('/queues/{serviceQueue}/rules', [QueueRuleController::class, 'index'])->name('qms.rules.index');
Route::post('/queues/{serviceQueue}/rules', [QueueRuleController::class, 'store'])->name('qms.rules.store');
Route::delete('/queues/{serviceQueue}/rules/{rule}', [QueueRuleController::class, 'destroy'])->name('qms.rules.destroy');
Route::get('/analytics', [AnalyticsController::class, 'index'])->name('qms.analytics.index');
Route::get('/reports', [ReportController::class, 'index'])->name('qms.reports.index');
Route::get('/reports/{type}', [ReportController::class, 'show'])->name('qms.reports.show');
Route::get('/reports/{type}/export', [ReportController::class, 'export'])->name('qms.reports.export');
Route::get('/feedback', [FeedbackAdminController::class, 'index'])->name('qms.feedback.admin');
Route::get('/admin/branches', [BranchController::class, 'index'])->name('qms.branches.index');
Route::get('/admin/branches/create', [BranchController::class, 'create'])->name('qms.branches.create');
Route::post('/admin/branches', [BranchController::class, 'store'])->name('qms.branches.store');
Route::get('/admin/branches/{branch}/edit', [BranchController::class, 'edit'])->name('qms.branches.edit');
Route::put('/admin/branches/{branch}', [BranchController::class, 'update'])->name('qms.branches.update');
Route::get('/admin/departments', [DepartmentController::class, 'index'])->name('qms.departments.index');
Route::get('/admin/departments/create', [DepartmentController::class, 'create'])->name('qms.departments.create');
Route::post('/admin/departments', [DepartmentController::class, 'store'])->name('qms.departments.store');
Route::get('/admin/departments/{department}/edit', [DepartmentController::class, 'edit'])->name('qms.departments.edit');
Route::put('/admin/departments/{department}', [DepartmentController::class, 'update'])->name('qms.departments.update');
Route::delete('/admin/departments/{department}', [DepartmentController::class, 'destroy'])->name('qms.departments.destroy');
Route::get('/admin/members', [MemberController::class, 'index'])->name('qms.members.index');
Route::get('/admin/members/create', [MemberController::class, 'create'])->name('qms.members.create');
Route::post('/admin/members', [MemberController::class, 'store'])->name('qms.members.store');
Route::delete('/admin/members/{member}', [MemberController::class, 'destroy'])->name('qms.members.destroy');
Route::get('/admin/audit', [AuditLogController::class, 'index'])->name('qms.audit.index');
Route::get('/admin/audit/export', [AuditLogController::class, 'export'])->name('qms.audit.export');
Route::get('/settings', [SettingsController::class, 'edit'])->name('qms.settings.edit');
Route::put('/settings', [SettingsController::class, 'update'])->name('qms.settings.update');
});
});