Initial Commit

This commit is contained in:
Mark Gregory
2026-08-25 12:36:04 +01:00
commit 903423cd6e
330 changed files with 39763 additions and 0 deletions
View File
+255
View File
@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
final class Attachment extends Model
{
use HasFactory;
protected $table = 'helpdesk_attachments';
/** @var list<string> */
protected $fillable = [
'ticket_id',
'comment_id',
'user_id',
'disk',
'path',
'original_name',
'mime_type',
'size',
'extension',
'category',
'checksum_sha256',
'download_count',
'last_downloaded_at',
'deleted_at',
'deleted_by',
];
/** @var array<string, string> */
protected $casts = [
'size' => 'integer',
'download_count' => 'integer',
'last_downloaded_at' => 'datetime',
'deleted_at' => 'datetime',
];
/** @return BelongsTo<Ticket, Attachment> */
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
/** @return BelongsTo<Comment, Attachment> */
public function comment(): BelongsTo
{
return $this->belongsTo(Comment::class, 'comment_id');
}
/** @return BelongsTo<User, Attachment> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/**
* Return an attribute only when it was actually selected from the database.
*
* Laravel 13 can throw when accessing missing attributes. The Helpdesk
* attachment metadata columns are introduced by an optional forward
* migration, so model helpers must remain safe before that migration has
* been applied.
*/
private function loadedAttribute(string $key): mixed
{
$attributes = $this->getAttributes();
return array_key_exists($key, $attributes) ? $this->getAttributeValue($key) : null;
}
/**
* Return the original client filename with compatibility fallbacks.
*/
public function displayName(): string
{
$name = $this->loadedAttribute('original_name')
?? $this->loadedAttribute('original_filename')
?? $this->loadedAttribute('filename')
?? $this->loadedAttribute('file_name');
if (is_string($name) && $name !== '') {
return $name;
}
return basename((string) ($this->loadedAttribute('path') ?? $this->loadedAttribute('file_path') ?? 'attachment'));
}
/**
* Return the stored byte size, falling back to legacy names.
*/
public function byteSize(): int
{
return (int) ($this->loadedAttribute('size') ?? $this->loadedAttribute('size_bytes') ?? 0);
}
/**
* Return the safe download count when the column exists.
*/
public function downloadCount(): int
{
return (int) ($this->loadedAttribute('download_count') ?? 0);
}
/**
* Determine whether the attachment has been soft deleted.
*/
public function isDeletedAttachment(): bool
{
return filled($this->loadedAttribute('deleted_at'));
}
/**
* Return the lower-case file extension from stored metadata or filename.
*/
public function extension(): string
{
$extension = $this->loadedAttribute('extension');
if (! is_string($extension) || $extension === '') {
$extension = pathinfo($this->displayName(), PATHINFO_EXTENSION);
}
return strtolower(ltrim((string) $extension, '.'));
}
/**
* Return the display category used by Helpdesk attachment cards.
*/
public function category(): string
{
$category = $this->loadedAttribute('category');
if (is_string($category) && $category !== '') {
return $category;
}
return match (true) {
$this->isImage() => 'image',
$this->isPdf() => 'pdf',
$this->isArchive() => 'archive',
$this->isSourceCode() => 'source',
default => 'file',
};
}
/**
* Determine whether the attachment is a browser-previewable image.
*/
public function isImage(): bool
{
return in_array($this->extension(), ['png', 'jpg', 'jpeg', 'webp', 'gif'], true)
|| Str::startsWith((string) $this->loadedAttribute('mime_type'), 'image/');
}
/**
* Determine whether the attachment is a PDF document.
*/
public function isPdf(): bool
{
return $this->extension() === 'pdf' || $this->loadedAttribute('mime_type') === 'application/pdf';
}
/**
* Determine whether the attachment is an archive.
*/
public function isArchive(): bool
{
return in_array($this->extension(), ['zip', 'tar', 'gz', '7z', 'rar'], true);
}
/**
* Determine whether the attachment is source/configuration text safe for the read-only viewer.
*/
public function isSourceCode(): bool
{
return in_array($this->extension(), [
'php', 'phtml', 'phps', 'blade.php', 'js', 'jsx', 'ts', 'tsx', 'vue', 'css', 'scss', 'sass', 'less',
'html', 'htm', 'xml', 'json', 'yaml', 'yml', 'sql', 'sh', 'bash', 'zsh', 'ps1', 'bat', 'cmd',
'py', 'rb', 'go', 'rs', 'java', 'kt', 'kts', 'cs', 'cpp', 'c', 'h', 'hpp', 'swift', 'dart',
'rpg', 'rpgle', 'cl', 'clle', 'cbl', 'cob', 'copybook', 'inc', 'env', 'ini', 'conf', 'config', 'txt', 'log', 'md',
], true);
}
/**
* Determine whether the attachment can be streamed inline by the preview route.
*/
public function isPreviewableBinary(): bool
{
return $this->isImage() || $this->isPdf();
}
/**
* Determine whether the Helpdesk UI can show an inline preview or viewer link.
*/
public function canPreview(): bool
{
return $this->isSourceCode() || $this->isPreviewableBinary();
}
/**
* Return a Font Awesome icon for the attachment type.
*/
public function icon(): string
{
return match ($this->category()) {
'image' => 'fa-regular fa-file-image',
'pdf' => 'fa-regular fa-file-pdf',
'source' => 'fa-solid fa-code',
'archive' => 'fa-regular fa-file-zipper',
default => 'fa-regular fa-file-lines',
};
}
/**
* Return the syntax language key used by the source viewer.
*/
public function language(): string
{
return match ($this->extension()) {
'php', 'phtml', 'phps', 'blade.php' => 'php',
'js', 'jsx' => 'javascript',
'ts', 'tsx' => 'typescript',
'vue' => 'vue',
'css', 'scss', 'sass', 'less' => 'css',
'html', 'htm' => 'html',
'xml' => 'xml',
'json' => 'json',
'yaml', 'yml' => 'yaml',
'sql' => 'sql',
'sh', 'bash', 'zsh' => 'shell',
'ps1' => 'powershell',
'py' => 'python',
'java', 'kt', 'kts' => 'java',
'c', 'h', 'cpp', 'hpp', 'cs' => 'c-like',
'rpg', 'rpgle' => 'rpgle',
'cl', 'clle' => 'clle',
'cbl', 'cob', 'copybook' => 'cobol',
default => 'text',
};
}
public function url(): string
{
return Storage::disk((string) $this->loadedAttribute('disk'))->url((string) $this->loadedAttribute('path'));
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
final class AutomationRule extends Model
{
protected $table = 'helpdesk_automation_rules';
protected $fillable = ['name', 'slug', 'match_sender', 'match_subject', 'match_mailbox', 'match_category_id', 'set_priority_id', 'set_category_id', 'set_team_id', 'set_project_id', 'set_status_id', 'set_agent_id', 'is_active', 'sort_order'];
protected $casts = ['match_category_id' => 'integer', 'set_priority_id' => 'integer', 'set_category_id' => 'integer', 'set_team_id' => 'integer', 'set_project_id' => 'integer', 'set_status_id' => 'integer', 'set_agent_id' => 'integer', 'is_active' => 'boolean', 'sort_order' => 'integer'];
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class CannedResponse extends Model
{
protected $table = 'helpdesk_canned_responses';
protected $fillable = ['category_id', 'team_id', 'name', 'slug', 'visibility', 'subject', 'body', 'is_active', 'sort_order'];
protected $casts = ['category_id' => 'integer', 'team_id' => 'integer', 'is_active' => 'boolean', 'sort_order' => 'integer'];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
public function team(): BelongsTo
{
return $this->belongsTo(Team::class, 'team_id');
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Category extends Model
{
protected $table = 'helpdesk_categories';
/** @var list<string> */
protected $fillable = ['name', 'slug', 'color', 'description', 'is_default'];
/** @var array<string, string> */
protected $casts = ['is_default' => 'boolean'];
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'category_id');
}
/** @return BelongsToMany<User> */
public function agents(): BelongsToMany
{
return $this->belongsToMany(User::class, 'helpdesk_category_user', 'category_id', 'user_id')->withTimestamps();
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class ChecklistItem extends Model
{
protected $table = 'helpdesk_checklist_items';
protected $fillable = ['ticket_id', 'template_id', 'title', 'description', 'is_completed', 'completed_by', 'completed_at', 'sort_order'];
protected $casts = ['ticket_id' => 'integer', 'template_id' => 'integer', 'is_completed' => 'boolean', 'completed_by' => 'integer', 'completed_at' => 'datetime', 'sort_order' => 'integer'];
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
final class ChecklistTemplate extends Model
{
protected $table = 'helpdesk_checklist_templates';
protected $fillable = ['category_id', 'project_id', 'name', 'slug', 'items', 'is_active'];
protected $casts = ['category_id' => 'integer', 'project_id' => 'integer', 'items' => 'array', 'is_active' => 'boolean'];
}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Schema;
final class Comment extends Model
{
protected $table = 'helpdesk_comments';
/** @var list<string> */
protected $fillable = [
'ticket_id',
'parent_id',
'user_id',
'subject',
'body',
'body_text',
'is_internal',
'guid',
'unid',
'replica_id',
'parent_unid',
'root_unid',
'originator_id',
'sync_sequence',
'sync_hash',
'source_system',
'source_record_id',
'replicated_at',
'synced_at',
'tombstoned_at',
'deleted_by',
'delete_reason',
];
/** @var array<string, string> */
protected $casts = [
'is_internal' => 'boolean',
'sync_sequence' => 'integer',
'replicated_at' => 'datetime',
'synced_at' => 'datetime',
'tombstoned_at' => 'datetime',
'deleted_by' => 'integer',
];
/** @return BelongsTo<Ticket, Comment> */
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
/** @return BelongsTo<Comment, Comment> */
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id');
}
/** @return HasMany<Comment> */
public function replies(): HasMany
{
return $this->hasMany(self::class, 'parent_id')
->active()
->oldest();
}
/** @return BelongsTo<User, Comment> */
public function deleter(): BelongsTo
{
return $this->belongsTo(User::class, 'deleted_by');
}
/** @return BelongsTo<User, Comment> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/** @return HasMany<Attachment> */
public function attachments(): HasMany
{
return $this->hasMany(Attachment::class, 'comment_id');
}
/**
* Scope the query to non-tombstoned comments when the legacy column exists.
*
* Some older Dusk databases can exist before the Lotus replication migration
* added tombstoned_at. In that state every row is active, so avoid issuing an
* invalid column query.
*
* @param Builder<Comment> $query
* @return Builder<Comment>
*/
public function scopeActive(Builder $query): Builder
{
if (! Schema::hasColumn($this->getTable(), 'tombstoned_at')) {
return $query;
}
return $query->whereNull('tombstoned_at');
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class GitCommit extends Model
{
use HasFactory;
protected $table = 'helpdesk_git_commits';
/** @var list<string> */
protected $fillable = [
'repository_id', 'ticket_id', 'hash', 'short_hash', 'branch', 'author_name', 'author_email', 'message', 'url', 'committed_at',
];
/** @var array<string, string> */
protected $casts = ['repository_id' => 'integer', 'ticket_id' => 'integer', 'committed_at' => 'datetime'];
/** @return BelongsTo<GitRepository, GitCommit> */
public function repository(): BelongsTo
{
return $this->belongsTo(GitRepository::class, 'repository_id');
}
/** @return BelongsTo<Ticket, GitCommit> */
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class GitRepository extends Model
{
use HasFactory;
protected $table = 'helpdesk_git_repositories';
/** @var list<string> */
protected $fillable = ['project_id', 'name', 'provider', 'remote_url', 'default_branch', 'webhook_secret', 'is_active'];
/** @var array<string, string> */
protected $casts = ['project_id' => 'integer', 'is_active' => 'boolean'];
/** @return BelongsTo<Project, GitRepository> */
public function project(): BelongsTo
{
return $this->belongsTo(Project::class, 'project_id');
}
/** @return HasMany<GitCommit> */
public function commits(): HasMany
{
return $this->hasMany(GitCommit::class, 'repository_id')->latest('committed_at');
}
/** @return HasMany<GitWebhookLog> */
public function webhookLogs(): HasMany
{
return $this->hasMany(GitWebhookLog::class, 'repository_id')->latest('received_at');
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class GitWebhookLog extends Model
{
use HasFactory;
protected $table = 'helpdesk_git_webhook_logs';
/** @var list<string> */
protected $fillable = [
'repository_id',
'provider',
'delivery_id',
'event',
'status',
'message',
'commit_count',
'linked_count',
'ignored_count',
'payload',
'received_at',
];
/** @var array<string, string> */
protected $casts = [
'repository_id' => 'integer',
'commit_count' => 'integer',
'linked_count' => 'integer',
'ignored_count' => 'integer',
'payload' => 'array',
'received_at' => 'datetime',
];
/**
* Repository that received the provider webhook.
*
* @return BelongsTo<GitRepository, GitWebhookLog>
*/
public function repository(): BelongsTo
{
return $this->belongsTo(GitRepository::class, 'repository_id');
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Configurable Helpdesk Kanban board.
*/
final class KanbanBoard extends Model
{
use HasFactory;
protected $table = 'helpdesk_kanban_boards';
/** @var list<string> */
protected $fillable = [
'name',
'slug',
'description',
'owner_id',
'is_default',
'sort_order',
];
/** @var array<string, string> */
protected $casts = [
'owner_id' => 'integer',
'is_default' => 'boolean',
'sort_order' => 'integer',
];
/** @return BelongsTo<User, KanbanBoard> */
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'board_id');
}
/** @return BelongsToMany<Status> */
public function statuses(): BelongsToMany
{
return $this->belongsToMany(Status::class, 'helpdesk_kanban_board_statuses', 'board_id', 'status_id')
->withPivot(['sort_order'])
->orderBy('helpdesk_kanban_board_statuses.sort_order')
->orderBy('helpdesk_statuses.sort_order')
->withTimestamps();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Priority extends Model
{
use HasFactory;
protected $table = 'helpdesk_priorities';
/** @var list<string> */
protected $fillable = ['name', 'slug', 'color', 'sort_order', 'is_default'];
/** @var array<string, string> */
protected $casts = ['sort_order' => 'integer', 'is_default' => 'boolean'];
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'priority_id');
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class Product extends Model
{
protected $table = 'helpdesk_products';
protected $fillable = ['name', 'slug', 'type', 'version', 'release_channel', 'description', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'product_id');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
final class Project extends Model
{
use SoftDeletes;
protected $table = 'helpdesk_projects';
/** @var list<string> */
protected $fillable = ['team_id', 'title', 'slug', 'color', 'description', 'archived_at', 'is_default'];
/** @var array<string, string> */
protected $casts = ['team_id' => 'integer', 'archived_at' => 'datetime', 'is_default' => 'boolean'];
/** @return BelongsTo<Team, Project> */
public function team(): BelongsTo
{
return $this->belongsTo(Team::class, 'team_id');
}
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'project_id');
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class SlaPolicy extends Model
{
protected $table = 'helpdesk_sla_policies';
protected $fillable = ['priority_id', 'category_id', 'name', 'slug', 'first_response_minutes', 'resolution_minutes', 'business_hours_only', 'is_default', 'is_active'];
protected $casts = ['priority_id' => 'integer', 'category_id' => 'integer', 'first_response_minutes' => 'integer', 'resolution_minutes' => 'integer', 'business_hours_only' => 'boolean', 'is_default' => 'boolean', 'is_active' => 'boolean'];
public function priority(): BelongsTo
{
return $this->belongsTo(Priority::class, 'priority_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $name
* @property string $slug
* @property string $color
* @property bool $is_closed
*/
final class Status extends Model
{
use HasFactory;
protected $table = 'helpdesk_statuses';
/** @var list<string> */
protected $fillable = ['name', 'slug', 'color', 'sort_order', 'is_closed', 'is_default'];
/** @var array<string, string> */
protected $casts = ['is_closed' => 'boolean', 'is_default' => 'boolean', 'sort_order' => 'integer'];
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'status_id');
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
final class Tag extends Model
{
protected $table = 'helpdesk_tags';
protected $fillable = ['name', 'slug', 'color'];
public function tickets(): BelongsToMany
{
return $this->belongsToMany(Ticket::class, 'helpdesk_ticket_tag', 'tag_id', 'ticket_id');
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
final class Team extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'helpdesk_teams';
/** @var list<string> */
protected $fillable = ['name', 'slug', 'color', 'description', 'archived_at', 'is_default'];
/** @var array<string, string> */
protected $casts = [
'archived_at' => 'datetime',
'is_default' => 'boolean',
];
/** @return HasMany<Project> */
public function projects(): HasMany
{
return $this->hasMany(Project::class, 'team_id');
}
/** @return HasMany<Ticket> */
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class, 'team_id');
}
}
+217
View File
@@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
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 Modules\Helpdesk\Database\Factories\TicketFactory;
/**
* @property int $id
* @property string $reference
* @property string $subject
* @property string $body
*/
final class Ticket extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'helpdesk_tickets';
protected $with = ['user'];
/** @var list<string> */
protected $fillable = [
'reference', 'user_id', 'agent_id', 'status_id', 'board_id', 'priority_id', 'category_id', 'project_id', 'team_id',
'subject', 'body', 'due_at', 'completed_at', 'last_activity_at', 'archived_at',
'sla_policy_id', 'product_id', 'first_response_at', 'sla_first_response_due_at', 'sla_resolution_due_at',
'sla_first_response_breached_at', 'sla_resolution_breached_at', 'public_replies_count', 'time_spent_minutes',
];
/** @var array<string, string> */
protected $casts = [
'team_id' => 'integer',
'board_id' => 'integer',
'due_at' => 'datetime',
'completed_at' => 'datetime',
'last_activity_at' => 'datetime',
'archived_at' => 'datetime',
'sla_policy_id' => 'integer',
'product_id' => 'integer',
'first_response_at' => 'datetime',
'sla_first_response_due_at' => 'datetime',
'sla_resolution_due_at' => 'datetime',
'sla_first_response_breached_at' => 'datetime',
'sla_resolution_breached_at' => 'datetime',
'public_replies_count' => 'integer',
'time_spent_minutes' => 'integer',
];
/** @return BelongsTo<User, Ticket> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function tickets(): Ticket|HasMany
{
return $this->hasMany(Ticket::class, 'ticket_id');
}
/** @return BelongsTo<User, Ticket> */
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
/** @return BelongsTo<User, Ticket> */
public function agent(): BelongsTo
{
return $this->belongsTo(User::class, 'agent_id');
}
/** @return BelongsTo<Status, Ticket> */
public function status(): BelongsTo
{
return $this->belongsTo(Status::class, 'status_id');
}
/** @return BelongsTo<KanbanBoard, Ticket> */
public function board(): BelongsTo
{
return $this->belongsTo(KanbanBoard::class, 'board_id');
}
/** @return BelongsTo<Priority, Ticket> */
public function priority(): BelongsTo
{
return $this->belongsTo(Priority::class, 'priority_id');
}
/** @return BelongsTo<Category, Ticket> */
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}
/** @return BelongsTo<Project, Ticket> */
public function project(): BelongsTo
{
return $this->belongsTo(Project::class, 'project_id');
}
/** @return BelongsTo<Team, Ticket> */
public function team(): BelongsTo
{
return $this->belongsTo(Team::class, 'team_id');
}
/** @return HasMany<Comment> */
public function comments(): HasMany
{
return $this->hasMany(Comment::class, 'ticket_id')
->whereNull('parent_id')
->oldest();
}
/** @return BelongsTo<SlaPolicy, Ticket> */
public function slaPolicy(): BelongsTo
{
return $this->belongsTo(SlaPolicy::class, 'sla_policy_id');
}
/** @return BelongsTo<Product, Ticket> */
public function product(): BelongsTo
{
return $this->belongsTo(Product::class, 'product_id');
}
/** @return HasMany<TicketSlaEvent> */
public function slaEvents(): HasMany
{
return $this->hasMany(TicketSlaEvent::class, 'ticket_id')->latest();
}
/** @return HasMany<TicketRating> */
public function ratings(): HasMany
{
return $this->hasMany(TicketRating::class, 'ticket_id')->latest();
}
/** @return HasMany<ChecklistItem> */
public function checklistItems(): HasMany
{
return $this->hasMany(ChecklistItem::class, 'ticket_id')->orderBy('sort_order')->orderBy('id');
}
/** @return BelongsToMany<Tag> */
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class, 'helpdesk_ticket_tag', 'ticket_id', 'tag_id');
}
public function getSlaFirstResponseBreachedAttribute(): bool
{
return $this->first_response_at === null
&& $this->sla_first_response_due_at !== null
&& now()->greaterThan($this->sla_first_response_due_at);
}
public function getSlaResolutionBreachedAttribute(): bool
{
return $this->completed_at === null
&& $this->sla_resolution_due_at !== null
&& now()->greaterThan($this->sla_resolution_due_at);
}
/** @return HasMany<GitCommit> */
public function gitCommits(): HasMany
{
return $this->hasMany(GitCommit::class, 'ticket_id')->latest('committed_at');
}
/** @return HasMany<Attachment> */
public function attachments(): HasMany
{
return $this->hasMany(Attachment::class, 'ticket_id')->latest();
}
/** @param Builder<Ticket> $query @return Builder<Ticket> */
public function scopeOpen(Builder $query): Builder
{
return $query->whereNull('completed_at');
}
/** @param Builder<Ticket> $query @return Builder<Ticket> */
public function scopeVisibleTo(Builder $query, User $user): Builder
{
if ($user->isAdmin() || (bool) ($user->helpdesk_admin ?? false)) {
return $query;
}
if ((bool) ($user->helpdesk_agent ?? false)) {
return $query->where(static function (Builder $subQuery) use ($user): void {
$subQuery->where('agent_id', $user->id)->orWhere('user_id', $user->id);
});
}
return $query->where('user_id', $user->id);
}
/**
* Create a new factory instance for the model.
*/
protected static function newFactory(): TicketFactory
{
return TicketFactory::new();
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
final class TicketRating extends Model
{
protected $table = 'helpdesk_ticket_ratings';
protected $fillable = ['ticket_id', 'agent_id', 'team_id', 'category_id', 'rating', 'token', 'comment', 'rated_at'];
protected $casts = ['ticket_id' => 'integer', 'agent_id' => 'integer', 'team_id' => 'integer', 'category_id' => 'integer', 'rating' => 'integer', 'rated_at' => 'datetime'];
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Model;
final class TicketSlaEvent extends Model
{
protected $table = 'helpdesk_ticket_sla_events';
protected $fillable = ['ticket_id', 'sla_policy_id', 'event', 'due_at', 'occurred_at', 'breached', 'meta'];
protected $casts = ['ticket_id' => 'integer', 'sla_policy_id' => 'integer', 'due_at' => 'datetime', 'occurred_at' => 'datetime', 'breached' => 'boolean', 'meta' => 'array'];
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketTimeLog extends Model
{
use HasFactory;
protected $table = 'helpdesk_ticket_time_logs';
protected $fillable = [
'ticket_id',
'user_id',
'minutes',
'seconds',
'description',
'started_at',
'ended_at',
'logged_at',
];
protected function casts(): array
{
return [
'ticket_id' => 'integer',
'user_id' => 'integer',
'minutes' => 'integer',
'seconds' => 'integer',
'started_at' => 'datetime',
'ended_at' => 'datetime',
'logged_at' => 'datetime',
];
}
/**
* Get the ticket this time log belongs to.
*/
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
/**
* Get the user who logged the time.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class TimeLog extends Model
{
protected $table = 'helpdesk_time_logs';
/** @var list<string> */
protected $fillable = [
'ticket_id',
'user_id',
'minutes',
'description',
'logged_at',
];
/** @var array<string, string> */
protected $casts = [
'minutes' => 'integer',
'logged_at' => 'datetime',
];
/** @return BelongsTo<Ticket, TimeLog> */
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
/** @return BelongsTo<User, TimeLog> */
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class WorkflowLog extends Model
{
use HasFactory;
protected $table = 'helpdesk_workflow_logs';
/** @var list<string> */
protected $fillable = ['template_id', 'ticket_id', 'event', 'recipient', 'subject', 'status', 'tracking_token', 'first_opened_at', 'last_opened_at', 'opened_count', 'error'];
/** @var array<string, string> */
protected $casts = [
'first_opened_at' => 'datetime',
'last_opened_at' => 'datetime',
'opened_count' => 'integer',
];
/** @return BelongsTo<WorkflowTemplate, WorkflowLog> */
public function template(): BelongsTo
{
return $this->belongsTo(WorkflowTemplate::class, 'template_id');
}
/** @return BelongsTo<Ticket, WorkflowLog> */
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class, 'ticket_id');
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
final class WorkflowTemplate extends Model
{
use HasFactory;
protected $table = 'helpdesk_workflow_templates';
/** @var list<string> */
protected $fillable = ['name', 'event', 'subject', 'html_body', 'text_body', 'to_role', 'cc', 'bcc', 'is_active'];
/** @var array<string, string> */
protected $casts = ['is_active' => 'boolean'];
/** @return HasMany<WorkflowLog> */
public function logs(): HasMany
{
return $this->hasMany(WorkflowLog::class, 'template_id');
}
}