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
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AuditLog extends Model
{
use BelongsToOwner;
protected $table = 'queue_audit_logs';
public $timestamps = false;
protected $fillable = [
'owner_ref', 'organization_id', 'actor_ref', 'action',
'subject_type', 'subject_id', 'metadata', 'ip_address', 'created_at',
];
protected function casts(): array
{
return [
'metadata' => 'array',
'created_at' => 'datetime',
];
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public static function record(
string $ownerRef,
string $action,
?int $organizationId = null,
?string $actorRef = null,
?string $subjectType = null,
?int $subjectId = null,
?array $metadata = null,
): self {
return static::create([
'owner_ref' => $ownerRef,
'organization_id' => $organizationId,
'actor_ref' => $actorRef,
'action' => $action,
'subject_type' => $subjectType,
'subject_id' => $subjectId,
'metadata' => $metadata,
'ip_address' => request()?->ip(),
'created_at' => now(),
]);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Branch extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_branches';
protected $fillable = [
'owner_ref', 'organization_id', 'name', 'code', 'address', 'phone', 'is_active',
];
protected function casts(): array
{
return ['is_active' => 'boolean'];
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function departments(): HasMany
{
return $this->hasMany(Department::class, 'branch_id');
}
public function serviceQueues(): HasMany
{
return $this->hasMany(ServiceQueue::class, 'branch_id');
}
public function counters(): HasMany
{
return $this->hasMany(Counter::class, 'branch_id');
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Builder;
trait BelongsToOwner
{
public function scopeOwned(Builder $query, string $ownerRef): Builder
{
return $query->where($this->getTable().'.owner_ref', $ownerRef);
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Counter extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_counters';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'department_id',
'name', 'code', 'status', 'settings', 'is_active',
];
protected function casts(): array
{
return [
'settings' => 'array',
'is_active' => 'boolean',
];
}
protected static function booted(): void
{
static::creating(function (self $counter) {
$counter->uuid ??= (string) Str::uuid();
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
public function department(): BelongsTo
{
return $this->belongsTo(Department::class, 'department_id');
}
public function serviceQueues(): BelongsToMany
{
return $this->belongsToMany(ServiceQueue::class, 'queue_counter_service_queue', 'counter_id', 'service_queue_id')
->withPivot('priority');
}
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'counter_id');
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class CustomerFeedback extends Model
{
use BelongsToOwner;
protected $table = 'queue_customer_feedback';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'ticket_id', 'counter_id',
'rating', 'service_quality', 'wait_experience', 'staff_professionalism', 'comments',
];
protected static function booted(): void
{
static::creating(function (self $model) {
$model->uuid ??= (string) Str::uuid();
});
}
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
public function counter(): BelongsTo
{
return $this->belongsTo(Counter::class, 'counter_id');
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Department extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_departments';
protected $fillable = [
'owner_ref', 'branch_id', 'name', 'type', 'is_active',
];
protected function casts(): array
{
return ['is_active' => 'boolean'];
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Device extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_devices';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id',
'name', 'type', 'status', 'device_token', 'config', 'last_online_at',
];
protected function casts(): array
{
return [
'config' => 'array',
'last_online_at' => 'datetime',
];
}
protected static function booted(): void
{
static::creating(function (self $device) {
$device->uuid ??= (string) Str::uuid();
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class DisplayScreen extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_display_screens';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id',
'name', 'layout', 'access_token', 'service_queue_ids', 'settings', 'is_active', 'last_seen_at',
];
protected function casts(): array
{
return [
'service_queue_ids' => 'array',
'settings' => 'array',
'is_active' => 'boolean',
'last_seen_at' => 'datetime',
];
}
protected static function booted(): void
{
static::creating(function (self $screen) {
$screen->uuid ??= (string) Str::uuid();
$screen->access_token ??= Str::random(48);
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Member extends Model
{
use BelongsToOwner;
protected $table = 'queue_members';
protected $fillable = ['owner_ref', 'organization_id', 'user_ref', 'role', 'branch_id'];
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
public function hasRole(string ...$roles): bool
{
return in_array($this->role, $roles, true);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Organization extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_organizations';
protected $fillable = [
'owner_ref', 'name', 'slug', 'logo_path', 'timezone', 'settings',
];
protected function casts(): array
{
return ['settings' => 'array'];
}
public function branches(): HasMany
{
return $this->hasMany(Branch::class, 'organization_id');
}
public function members(): HasMany
{
return $this->hasMany(Member::class, 'organization_id');
}
public function serviceQueues(): HasMany
{
return $this->hasMany(ServiceQueue::class, 'organization_id');
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class QueueAppointment extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_appointments';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'service_queue_id', 'ticket_id',
'customer_name', 'customer_phone', 'customer_email', 'status', 'mode',
'scheduled_at', 'checked_in_at', 'reference', 'metadata',
];
protected function casts(): array
{
return [
'scheduled_at' => 'datetime',
'checked_in_at' => 'datetime',
'metadata' => 'array',
];
}
protected static function booted(): void
{
static::creating(function (self $model) {
$model->uuid ??= (string) Str::uuid();
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
public function serviceQueue(): BelongsTo
{
return $this->belongsTo(ServiceQueue::class, 'service_queue_id');
}
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class QueueRule extends Model
{
use BelongsToOwner;
protected $table = 'queue_queue_rules';
protected $fillable = [
'owner_ref', 'service_queue_id', 'rule_type', 'conditions', 'actions', 'sort_order', 'is_active',
];
protected function casts(): array
{
return [
'conditions' => 'array',
'actions' => 'array',
'is_active' => 'boolean',
];
}
public function serviceQueue(): BelongsTo
{
return $this->belongsTo(ServiceQueue::class, 'service_queue_id');
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class ServiceQueue extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_service_queues';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'department_id',
'name', 'description', 'color', 'prefix', 'strategy',
'max_capacity', 'avg_service_seconds', 'operating_hours', 'priority_rules',
'settings', 'is_active', 'is_paused', 'ticket_sequence',
];
protected function casts(): array
{
return [
'operating_hours' => 'array',
'priority_rules' => 'array',
'settings' => 'array',
'is_active' => 'boolean',
'is_paused' => 'boolean',
];
}
protected static function booted(): void
{
static::creating(function (self $queue) {
$queue->uuid ??= (string) Str::uuid();
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
public function department(): BelongsTo
{
return $this->belongsTo(Department::class, 'department_id');
}
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'service_queue_id');
}
public function counters(): BelongsToMany
{
return $this->belongsToMany(Counter::class, 'queue_counter_service_queue', 'service_queue_id', 'counter_id')
->withPivot('priority');
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServiceSession extends Model
{
use BelongsToOwner;
protected $table = 'queue_service_sessions';
protected $fillable = [
'owner_ref', 'ticket_id', 'counter_id', 'staff_ref',
'started_at', 'ended_at', 'duration_seconds',
];
protected function casts(): array
{
return [
'started_at' => 'datetime',
'ended_at' => 'datetime',
];
}
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
public function counter(): BelongsTo
{
return $this->belongsTo(Counter::class, 'counter_id');
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Ticket extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_tickets';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id', 'service_queue_id', 'counter_id',
'ticket_number', 'status', 'priority', 'position', 'customer_name', 'customer_phone',
'customer_email', 'source', 'qr_token', 'barcode', 'estimated_wait_seconds',
'issued_at', 'called_at', 'serving_started_at', 'completed_at', 'metadata',
];
protected function casts(): array
{
return [
'metadata' => 'array',
'issued_at' => 'datetime',
'called_at' => 'datetime',
'serving_started_at' => 'datetime',
'completed_at' => 'datetime',
];
}
protected static function booted(): void
{
static::creating(function (self $ticket) {
$ticket->uuid ??= (string) Str::uuid();
$ticket->qr_token ??= Str::random(32);
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
public function serviceQueue(): BelongsTo
{
return $this->belongsTo(ServiceQueue::class, 'service_queue_id');
}
public function counter(): BelongsTo
{
return $this->belongsTo(Counter::class, 'counter_id');
}
public function events(): HasMany
{
return $this->hasMany(TicketEvent::class, 'ticket_id');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketEvent extends Model
{
use BelongsToOwner;
protected $table = 'queue_ticket_events';
public $timestamps = false;
protected $fillable = [
'owner_ref', 'ticket_id', 'event', 'actor_ref', 'counter_id', 'metadata', 'created_at',
];
protected function casts(): array
{
return [
'metadata' => 'array',
'created_at' => 'datetime',
];
}
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
public function counter(): BelongsTo
{
return $this->belongsTo(Counter::class, 'counter_id');
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* Thin local mirror of the platform identity (auth.ladill.com owns users).
* Keyed by the OIDC `sub` (public_id); every CRM record is scoped to it.
*/
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = ['public_id', 'name', 'email', 'avatar_url', 'password', 'last_app_active_at'];
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'last_app_active_at' => 'datetime',
'password' => 'hashed',
];
}
/** The owner reference used to scope every CRM record to this account. */
public function ownerRef(): string
{
return (string) $this->public_id;
}
public function avatarUrl(): ?string
{
$url = trim((string) $this->avatar_url);
return $url !== '' ? $url : null;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VoiceAnnouncement extends Model
{
use BelongsToOwner;
protected $table = 'queue_voice_announcements';
protected $fillable = [
'owner_ref', 'organization_id', 'branch_id', 'ticket_id',
'locale', 'message', 'voice', 'volume', 'repeat_count', 'status', 'played_at',
];
protected function casts(): array
{
return [
'played_at' => 'datetime',
];
}
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Models;
use App\Models\Concerns\BelongsToOwner;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Workflow extends Model
{
use BelongsToOwner, SoftDeletes;
protected $table = 'queue_workflows';
protected $fillable = [
'uuid', 'owner_ref', 'organization_id', 'branch_id',
'name', 'description', 'industry_template', 'is_active', 'settings',
];
protected function casts(): array
{
return [
'settings' => 'array',
'is_active' => 'boolean',
];
}
protected static function booted(): void
{
static::creating(function (self $model) {
$model->uuid ??= (string) Str::uuid();
});
}
public function organization(): BelongsTo
{
return $this->belongsTo(Organization::class, 'organization_id');
}
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class, 'branch_id');
}
public function steps(): HasMany
{
return $this->hasMany(WorkflowStep::class, 'workflow_id')->orderBy('sort_order');
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WorkflowStep extends Model
{
protected $table = 'queue_workflow_steps';
protected $fillable = [
'workflow_id', 'service_queue_id', 'name', 'sort_order', 'settings',
];
protected function casts(): array
{
return ['settings' => 'array'];
}
public function workflow(): BelongsTo
{
return $this->belongsTo(Workflow::class, 'workflow_id');
}
public function serviceQueue(): BelongsTo
{
return $this->belongsTo(ServiceQueue::class, 'service_queue_id');
}
}