146 lines
5.7 KiB
PHP
146 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Services;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\Helpdesk\Models\Comment;
|
|
use Modules\Helpdesk\Models\KanbanBoard;
|
|
use Modules\Helpdesk\Models\Status;
|
|
use Modules\Helpdesk\Models\Ticket;
|
|
|
|
final class TicketService
|
|
{
|
|
public function __construct(
|
|
private readonly TicketReferenceService $references,
|
|
private readonly AgentAssignmentService $assignment,
|
|
private readonly AttachmentService $attachments,
|
|
private readonly WorkflowNotificationService $workflow,
|
|
private readonly AutomationRuleService $automation,
|
|
private readonly SlaService $sla,
|
|
private readonly ChecklistService $checklists,
|
|
) {}
|
|
|
|
/**
|
|
* Create a ticket and store initial attachments.
|
|
*
|
|
* @param array<string, mixed> $data
|
|
* @param array<int, UploadedFile> $files
|
|
*/
|
|
public function create(User $user, array $data, array $files = []): Ticket
|
|
{
|
|
return DB::transaction(function () use ($user, $data, $files): Ticket {
|
|
$defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new');
|
|
$status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')
|
|
? Status::query()->where('is_default', true)->orderBy('sort_order')->first()
|
|
: null;
|
|
|
|
if (! $status) {
|
|
$status = Status::query()->where('slug', $defaultStatusSlug)->first();
|
|
}
|
|
|
|
if (! $status) {
|
|
$status = Status::query()->create([
|
|
'name' => 'New',
|
|
'slug' => $defaultStatusSlug,
|
|
'color' => 'sky',
|
|
'sort_order' => 1,
|
|
'is_closed' => false,
|
|
]);
|
|
}
|
|
|
|
$boardId = $data['board_id'] ?? null;
|
|
if (blank($boardId) && Schema::hasTable('helpdesk_kanban_boards')) {
|
|
$boardId = KanbanBoard::query()
|
|
->where('is_default', true)
|
|
->orderBy('sort_order')
|
|
->value('id') ?: KanbanBoard::query()->orderBy('sort_order')->value('id');
|
|
}
|
|
|
|
/** @var Ticket $ticket */
|
|
$ticket = Ticket::query()->create([
|
|
'reference' => $this->references->next(),
|
|
'company_id' => $data['company_id'] ?? ($user->company_id ?? null),
|
|
'user_id' => $user->id,
|
|
'agent_id' => $this->assignment->selectAgentId($data['category_id'] ?? null),
|
|
'status_id' => $status->id,
|
|
'board_id' => $boardId,
|
|
'priority_id' => $data['priority_id'],
|
|
'category_id' => $data['category_id'] ?? null,
|
|
'project_id' => $data['project_id'] ?? null,
|
|
'product_id' => $data['product_id'] ?? null,
|
|
'team_id' => $data['team_id'] ?? null,
|
|
'subject' => $data['subject'],
|
|
'body' => $data['body'],
|
|
'due_at' => $data['due_at'] ?? null,
|
|
'last_activity_at' => now(),
|
|
]);
|
|
|
|
if (! empty($data['tag_ids']) && is_array($data['tag_ids'])) {
|
|
$ticket->tags()->sync(array_values(array_unique(array_map('intval', $data['tag_ids']))));
|
|
}
|
|
|
|
$ticket = $this->automation->apply($ticket, $data);
|
|
$ticket = $this->sla->applyPolicy($ticket);
|
|
$this->checklists->applyTemplates($ticket);
|
|
$this->attachments->storeMany($ticket, $files, (int) $user->id);
|
|
|
|
$ticket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'product', 'tags', 'attachments', 'checklistItems']) ?? $ticket;
|
|
$this->workflow->dispatch($ticket, 'ticket.created', $user);
|
|
|
|
return $ticket;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a public or internal comment and optional attachments.
|
|
*
|
|
* @param array<int, UploadedFile> $files
|
|
*/
|
|
public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null): Comment
|
|
{
|
|
return DB::transaction(function () use ($ticket, $user, $body, $internal, $files, $parentId): Comment {
|
|
/** @var Comment $comment */
|
|
$comment = $ticket->comments()->create([
|
|
'parent_id' => $parentId,
|
|
'user_id' => $user->id,
|
|
'body' => $body,
|
|
'is_internal' => $internal,
|
|
]);
|
|
|
|
$ticket->forceFill(['last_activity_at' => now()])->save();
|
|
if (! $internal) {
|
|
$this->sla->recordPublicResponse($ticket, $user);
|
|
}
|
|
$this->attachments->storeMany($ticket, $files, (int) $user->id, $comment);
|
|
$this->workflow->dispatch($ticket->fresh(['status', 'priority', 'category', 'project', 'team']) ?? $ticket, $parentId === null ? 'ticket.commented' : 'ticket.reply_created', $user);
|
|
|
|
return $comment->fresh(['user', 'attachments']) ?? $comment;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Close a ticket.
|
|
*/
|
|
public function close(Ticket $ticket, User $user): Ticket
|
|
{
|
|
$status = Status::query()->where('slug', config('helpdesk.closed_status_slug'))->firstOrFail();
|
|
|
|
$ticket->forceFill([
|
|
'status_id' => $status->id,
|
|
'completed_at' => now(),
|
|
'last_activity_at' => now(),
|
|
])->save();
|
|
|
|
$ticket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'product']) ?? $ticket;
|
|
$this->sla->recordClosure($ticket);
|
|
$this->workflow->dispatch($ticket, 'ticket.closed', $user);
|
|
|
|
return $ticket;
|
|
}
|
|
}
|