Files
WMS/Modules/Helpdesk/Livewire/TicketCreate.php
T
2026-09-21 10:14:57 +01:00

352 lines
12 KiB
PHP
Executable File

<?php
declare(strict_types=1);
namespace Modules\Helpdesk\Livewire;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
use Modules\Helpdesk\Models\Category;
use Modules\Helpdesk\Models\KanbanBoard;
use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\Project;
use Modules\Helpdesk\Models\Tag;
use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Services\TicketService;
final class TicketCreate extends Component
{
private const COMPANY_NULL_ORDER = 'company_id is null';
use WithFileUploads;
public ?int $company_id = null;
public string $subject = '';
public string $body = '';
public ?int $board_id = null;
public ?int $priority_id = null;
public ?int $category_id = null;
public ?int $project_id = null;
public ?int $product_id = null;
public ?int $team_id = null;
public ?string $due_at = null;
/** @var array<int, int> */
public array $tag_ids = [];
/** @var array<int, TemporaryUploadedFile> */
public array $attachments = [];
public bool $isSaving = false;
/**
* Validation rules are intentionally evaluated only when the user submits
* the form, not while rich-text/select widgets are synchronising.
*
* @return array<string, mixed>
*/
protected function rules(): array
{
return [
'company_id' => ['nullable', 'integer'],
'subject' => ['required', 'string', 'max:200'],
'body' => ['required', 'string', 'min:10'],
'board_id' => ['nullable', 'integer', 'exists:helpdesk_kanban_boards,id'],
'priority_id' => ['required', 'integer', 'exists:helpdesk_priorities,id'],
'category_id' => ['nullable', 'integer', 'exists:helpdesk_categories,id'],
'project_id' => ['nullable', 'integer', 'exists:helpdesk_projects,id'],
'product_id' => ['nullable', 'integer', 'exists:helpdesk_products,id'],
'team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
'due_at' => ['nullable', 'date'],
'tag_ids' => ['array'],
'tag_ids.*' => ['integer', 'exists:helpdesk_tags,id'],
'attachments' => ['array'],
'attachments.*' => $this->attachmentFileRules(),
];
}
/**
* Initialise configured Helpdesk defaults for a new ticket.
*/
public function mount(): void
{
$this->applyDefaults();
}
/**
* Store a new helpdesk ticket.
*/
public function save(TicketService $tickets)
{
if ($this->isSaving) {
return null;
}
$this->isSaving = true;
try {
$this->body = trim($this->body);
$this->validate();
$ticket = $tickets->create(Auth::user(), [
'company_id' => $this->company_id,
'subject' => $this->subject,
'body' => $this->body,
'board_id' => $this->board_id,
'priority_id' => $this->priority_id,
'category_id' => $this->category_id,
'project_id' => $this->project_id,
'product_id' => $this->product_id,
'team_id' => $this->team_id,
'due_at' => $this->due_at,
'tag_ids' => $this->tag_ids,
], $this->attachments);
session()->flash('success', 'Ticket ' . $ticket->reference . ' has been created.');
return redirect()->to(route('helpdesk.tickets.index'));
} finally {
$this->isSaving = false;
}
}
/**
* Clear the create form, including Select2 and Flatpickr browser widgets.
*/
public function clearForm(): void
{
$this->resetErrorBag();
$this->resetValidation();
$this->subject = '';
$this->body = '';
$this->tag_ids = [];
$this->attachments = [];
$this->dispatch('helpdesk-form-cleared');
$this->dispatch('helpdesk-editor-reset', model: 'body');
}
public function render(): View
{
return view('helpdesk::livewire.ticket-create', [
'boards' => $this->boardOptions(),
'priorities' => $this->priorityOptions(),
'categories' => $this->categoryOptions(),
'companies' => $this->companyOptions(),
'teams' => $this->teamOptions(),
'projects' => $this->projectOptions(),
'products' => Product::query()->where('is_active', true)->orderBy('name')->get(),
'tags' => Tag::query()->orderBy('name')->get(),
])->layout('layouts.app');
}
/**
* Apply default values from Helpdesk/company lookup configuration.
*/
private function applyDefaults(): void
{
if (blank($this->company_id) && Schema::hasTable('companies') && Schema::hasColumn('companies', 'is_default_helpdesk_company')) {
$this->company_id = DB::table('companies')
->where('is_default_helpdesk_company', true)
->orderBy('name')
->value('id');
}
if (blank($this->board_id) && Schema::hasTable('helpdesk_kanban_boards')) {
$this->board_id = KanbanBoard::query()
->where('is_default', true)
->orderBy('sort_order')
->value('id') ?: KanbanBoard::query()->orderBy('sort_order')->value('id');
}
if (blank($this->priority_id) && $this->hasDefaultColumn('helpdesk_priorities')) {
$this->priority_id = Priority::query()
->where('is_default', true)
->orderBy('sort_order')
->value('id');
}
if (blank($this->category_id) && $this->hasDefaultColumn('helpdesk_categories')) {
$this->category_id = Category::query()
->where('is_default', true)
->when($this->company_id, fn ($query) => $query->where(function ($query): void {
$query->where('company_id', $this->company_id)
->orWhereNull('company_id');
}))
->orderByRaw(self::COMPANY_NULL_ORDER)
->orderBy('name')
->value('id');
}
if (blank($this->team_id) && $this->hasDefaultColumn('helpdesk_teams')) {
$this->team_id = Team::query()
->where('is_default', true)
->whereNull('archived_at')
->when($this->company_id, fn ($query) => $query->where(function ($query): void {
$query->where('company_id', $this->company_id)
->orWhereNull('company_id');
}))
->orderByRaw(self::COMPANY_NULL_ORDER)
->orderBy('name')
->value('id');
}
if (blank($this->project_id) && $this->hasDefaultColumn('helpdesk_projects')) {
$this->project_id = Project::query()
->where('is_default', true)
->whereNull('archived_at')
->when($this->company_id, fn ($query) => $query->where(function ($query): void {
$query->where('company_id', $this->company_id)
->orWhereNull('company_id');
}))
->when($this->team_id, fn ($query) => $query->where(function ($query): void {
$query->where('team_id', $this->team_id)
->orWhereNull('team_id');
}))
->orderByRaw(self::COMPANY_NULL_ORDER)
->orderByRaw('team_id is null')
->orderBy('title')
->value('id');
}
}
/**
* Return Kanban board options with the default board listed first.
*/
private function boardOptions(): mixed
{
return KanbanBoard::query()
->with('owner:id,name,email')
->orderByDesc('is_default')
->orderBy('sort_order')
->orderBy('name')
->get();
}
/**
* Return priority options with default records listed first.
*/
private function priorityOptions(): mixed
{
return Priority::query()
->when($this->hasDefaultColumn('helpdesk_priorities'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('sort_order')
->get();
}
/**
* Return category options with default records listed first.
*/
private function categoryOptions(): mixed
{
return Category::query()
->with('agents:id')
->when($this->hasDefaultColumn('helpdesk_categories'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name')
->get();
}
/**
* Return team options with default records listed first.
*/
private function teamOptions(): mixed
{
return Team::query()
->whereNull('archived_at')
->when($this->hasDefaultColumn('helpdesk_teams'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('name')
->get();
}
/**
* Return project options with default records listed first.
*/
private function projectOptions(): mixed
{
return Project::query()
->whereNull('archived_at')
->when($this->hasDefaultColumn('helpdesk_projects'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('title')
->get();
}
/**
* Return company options with default flag support when available.
*/
private function companyOptions(): mixed
{
if (! Schema::hasTable('companies')) {
return collect();
}
$columns = ['id', 'name'];
if (Schema::hasColumn('companies', 'is_default_helpdesk_company')) {
$columns[] = 'is_default_helpdesk_company';
}
return DB::table('companies')
->select($columns)
->orderByDesc(Schema::hasColumn('companies', 'is_default_helpdesk_company') ? 'is_default_helpdesk_company' : 'id')
->orderBy('name')
->get();
}
/**
* Check whether a Helpdesk lookup table has the default flag column.
*/
private function hasDefaultColumn(string $table): bool
{
return Schema::hasTable($table) && Schema::hasColumn($table, 'is_default');
}
/**
* Return Helpdesk attachment validation rules without relying on a newly autoloaded support class.
*
* Laravel validates source-code attachments by extension because browsers often report
* PHP, JavaScript, RPG and other source files as text/plain or octet-stream.
*
* @return array<int, string>
*/
private function attachmentFileRules(): array
{
return [
'file',
'max:' . (int) config('helpdesk.max_upload_kb', 10240),
'extensions:' . implode(',', $this->allowedAttachmentExtensions()),
];
}
/**
* Return lower-case Helpdesk attachment extensions.
*
* @return array<int, string>
*/
private function allowedAttachmentExtensions(): array
{
return collect(config('helpdesk.allowed_extensions', config('helpdesk.allowed_mimes', [])))
->filter(static fn (mixed $extension): bool => is_string($extension) && trim($extension) !== '')
->map(static fn (string $extension): string => strtolower(ltrim(trim($extension), '.')))
->unique()
->values()
->all();
}
}