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,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Local mirror of the platform identity, keyed by public_id (OIDC sub).
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->uuid('public_id')->unique();
$table->string('name')->nullable();
$table->string('email')->unique();
$table->string('avatar_url')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
@@ -0,0 +1,87 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Ladill Queue core schema multi-branch queue management.
*
* Hierarchy: owner_ref Organization Branch Department
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('queue_organizations', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->string('name');
$table->string('slug')->index();
$table->string('logo_path')->nullable();
$table->string('timezone')->default('UTC');
$table->json('settings')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['owner_ref', 'slug']);
});
Schema::create('queue_branches', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->string('name');
$table->string('code')->nullable();
$table->string('address')->nullable();
$table->string('phone')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
Schema::create('queue_departments', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->string('name');
$table->string('type')->default('general');
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
Schema::create('queue_members', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->string('user_ref')->index();
$table->string('role');
$table->foreignId('branch_id')->nullable()->constrained('queue_branches')->nullOnDelete();
$table->timestamps();
$table->unique(['organization_id', 'user_ref']);
});
Schema::create('queue_audit_logs', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->nullable()->constrained('queue_organizations')->nullOnDelete();
$table->string('actor_ref')->nullable();
$table->string('action');
$table->string('subject_type')->nullable();
$table->unsignedBigInteger('subject_id')->nullable();
$table->json('metadata')->nullable();
$table->string('ip_address')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['owner_ref', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('queue_audit_logs');
Schema::dropIfExists('queue_members');
Schema::dropIfExists('queue_departments');
Schema::dropIfExists('queue_branches');
Schema::dropIfExists('queue_organizations');
}
};
@@ -0,0 +1,297 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Ladill Queue domain schema queues, tickets, counters, displays, workflows.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('queue_service_queues', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->foreignId('department_id')->nullable()->constrained('queue_departments')->nullOnDelete();
$table->string('name');
$table->text('description')->nullable();
$table->string('color', 20)->default('#4f46e5');
$table->string('prefix', 10)->default('A');
$table->string('strategy')->default('fifo'); // fifo, priority, appointment, vip, emergency, skill, dynamic, overflow
$table->unsignedInteger('max_capacity')->nullable();
$table->unsignedInteger('avg_service_seconds')->default(300);
$table->json('operating_hours')->nullable();
$table->json('priority_rules')->nullable();
$table->json('settings')->nullable();
$table->boolean('is_active')->default(true);
$table->boolean('is_paused')->default(false);
$table->unsignedBigInteger('ticket_sequence')->default(0);
$table->timestamps();
$table->softDeletes();
$table->index(['owner_ref', 'branch_id', 'is_active']);
});
Schema::create('queue_counters', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->foreignId('department_id')->nullable()->constrained('queue_departments')->nullOnDelete();
$table->string('name');
$table->string('code')->nullable();
$table->string('status')->default('offline'); // available, busy, offline, paused
$table->json('settings')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
Schema::create('queue_counter_service_queue', function (Blueprint $table) {
$table->id();
$table->foreignId('counter_id')->constrained('queue_counters')->cascadeOnDelete();
$table->foreignId('service_queue_id')->constrained('queue_service_queues')->cascadeOnDelete();
$table->unsignedSmallInteger('priority')->default(0);
$table->unique(['counter_id', 'service_queue_id']);
});
Schema::create('queue_counter_assignments', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('counter_id')->constrained('queue_counters')->cascadeOnDelete();
$table->string('staff_ref')->index();
$table->timestamp('assigned_at')->useCurrent();
$table->timestamp('released_at')->nullable();
$table->timestamps();
$table->index(['counter_id', 'released_at']);
});
Schema::create('queue_staff_sessions', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->foreignId('counter_id')->nullable()->constrained('queue_counters')->nullOnDelete();
$table->string('staff_ref')->index();
$table->timestamp('started_at');
$table->timestamp('ended_at')->nullable();
$table->json('stats')->nullable();
$table->timestamps();
});
Schema::create('queue_tickets', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->foreignId('service_queue_id')->constrained('queue_service_queues')->cascadeOnDelete();
$table->foreignId('counter_id')->nullable()->constrained('queue_counters')->nullOnDelete();
$table->string('ticket_number')->index();
$table->string('status')->default('waiting'); // waiting, called, serving, on_hold, completed, no_show, cancelled, transferred
$table->string('priority')->default('walk_in'); // emergency, vip, elderly, disabled, appointment, walk_in
$table->unsignedSmallInteger('position')->nullable();
$table->string('customer_name')->nullable();
$table->string('customer_phone')->nullable();
$table->string('customer_email')->nullable();
$table->string('source')->default('kiosk'); // kiosk, reception, mobile, web, api, appointment
$table->string('qr_token')->nullable()->unique();
$table->string('barcode')->nullable();
$table->unsignedInteger('estimated_wait_seconds')->nullable();
$table->timestamp('issued_at');
$table->timestamp('called_at')->nullable();
$table->timestamp('serving_started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['owner_ref', 'service_queue_id', 'status']);
$table->index(['owner_ref', 'branch_id', 'status', 'issued_at']);
});
Schema::create('queue_ticket_events', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('ticket_id')->constrained('queue_tickets')->cascadeOnDelete();
$table->string('event'); // issued, called, recalled, skipped, transferred, held, completed, no_show, cancelled
$table->string('actor_ref')->nullable();
$table->foreignId('counter_id')->nullable()->constrained('queue_counters')->nullOnDelete();
$table->json('metadata')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['ticket_id', 'created_at']);
});
Schema::create('queue_ticket_transfers', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('ticket_id')->constrained('queue_tickets')->cascadeOnDelete();
$table->foreignId('from_service_queue_id')->constrained('queue_service_queues');
$table->foreignId('to_service_queue_id')->constrained('queue_service_queues');
$table->foreignId('from_counter_id')->nullable()->constrained('queue_counters')->nullOnDelete();
$table->string('reason')->nullable();
$table->string('transferred_by')->nullable();
$table->timestamp('transferred_at')->useCurrent();
});
Schema::create('queue_queue_rules', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('service_queue_id')->constrained('queue_service_queues')->cascadeOnDelete();
$table->string('rule_type'); // overflow, routing, priority_boost, capacity
$table->json('conditions')->nullable();
$table->json('actions')->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('queue_workflows', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->nullable()->constrained('queue_branches')->nullOnDelete();
$table->string('name');
$table->text('description')->nullable();
$table->string('industry_template')->nullable();
$table->boolean('is_active')->default(true);
$table->json('settings')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('queue_workflow_steps', function (Blueprint $table) {
$table->id();
$table->foreignId('workflow_id')->constrained('queue_workflows')->cascadeOnDelete();
$table->foreignId('service_queue_id')->nullable()->constrained('queue_service_queues')->nullOnDelete();
$table->string('name');
$table->unsignedSmallInteger('sort_order')->default(0);
$table->json('settings')->nullable();
$table->timestamps();
});
Schema::create('queue_appointments', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->foreignId('service_queue_id')->nullable()->constrained('queue_service_queues')->nullOnDelete();
$table->foreignId('ticket_id')->nullable()->constrained('queue_tickets')->nullOnDelete();
$table->string('customer_name');
$table->string('customer_phone')->nullable();
$table->string('customer_email')->nullable();
$table->string('status')->default('scheduled'); // scheduled, checked_in, completed, cancelled, no_show
$table->string('mode')->default('hybrid'); // appointment_only, walk_in_only, hybrid
$table->timestamp('scheduled_at');
$table->timestamp('checked_in_at')->nullable();
$table->string('reference')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['owner_ref', 'branch_id', 'scheduled_at']);
});
Schema::create('queue_service_sessions', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('ticket_id')->constrained('queue_tickets')->cascadeOnDelete();
$table->foreignId('counter_id')->constrained('queue_counters')->cascadeOnDelete();
$table->string('staff_ref')->nullable();
$table->timestamp('started_at');
$table->timestamp('ended_at')->nullable();
$table->unsignedInteger('duration_seconds')->nullable();
$table->timestamps();
});
Schema::create('queue_display_screens', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->constrained('queue_branches')->cascadeOnDelete();
$table->string('name');
$table->string('layout')->default('standard'); // standard, compact, split, media
$table->string('access_token')->unique();
$table->json('service_queue_ids')->nullable();
$table->json('settings')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamp('last_seen_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('queue_devices', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->nullable()->constrained('queue_branches')->nullOnDelete();
$table->string('name');
$table->string('type'); // kiosk, tablet, printer, scanner, display, feedback_tablet
$table->string('status')->default('offline');
$table->string('device_token')->nullable()->unique();
$table->json('config')->nullable();
$table->timestamp('last_online_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('queue_voice_announcements', function (Blueprint $table) {
$table->id();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('branch_id')->nullable()->constrained('queue_branches')->nullOnDelete();
$table->foreignId('ticket_id')->nullable()->constrained('queue_tickets')->nullOnDelete();
$table->string('locale', 10)->default('en');
$table->text('message');
$table->string('voice')->nullable();
$table->unsignedTinyInteger('volume')->default(80);
$table->unsignedTinyInteger('repeat_count')->default(1);
$table->string('status')->default('pending'); // pending, played, failed
$table->timestamp('played_at')->nullable();
$table->timestamps();
});
Schema::create('queue_customer_feedback', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('owner_ref')->index();
$table->foreignId('organization_id')->constrained('queue_organizations')->cascadeOnDelete();
$table->foreignId('ticket_id')->constrained('queue_tickets')->cascadeOnDelete();
$table->foreignId('counter_id')->nullable()->constrained('queue_counters')->nullOnDelete();
$table->unsignedTinyInteger('rating')->nullable();
$table->unsignedTinyInteger('service_quality')->nullable();
$table->unsignedTinyInteger('wait_experience')->nullable();
$table->unsignedTinyInteger('staff_professionalism')->nullable();
$table->text('comments')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('queue_customer_feedback');
Schema::dropIfExists('queue_voice_announcements');
Schema::dropIfExists('queue_devices');
Schema::dropIfExists('queue_display_screens');
Schema::dropIfExists('queue_service_sessions');
Schema::dropIfExists('queue_appointments');
Schema::dropIfExists('queue_workflow_steps');
Schema::dropIfExists('queue_workflows');
Schema::dropIfExists('queue_queue_rules');
Schema::dropIfExists('queue_ticket_transfers');
Schema::dropIfExists('queue_ticket_events');
Schema::dropIfExists('queue_tickets');
Schema::dropIfExists('queue_staff_sessions');
Schema::dropIfExists('queue_counter_assignments');
Schema::dropIfExists('queue_counter_service_queue');
Schema::dropIfExists('queue_counters');
Schema::dropIfExists('queue_service_queues');
}
};
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
}
};