phase 6 added helpdesk
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
<?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'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user