new helpdesk module
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Modules\Helpdesk\Models\Category;
|
||||
|
||||
final class AgentAssignmentService
|
||||
{
|
||||
/**
|
||||
* Select the least-loaded available agent for a category.
|
||||
*
|
||||
* @param int|null $categoryId
|
||||
* @return int|null
|
||||
*/
|
||||
public function selectAgentId(?int $categoryId): ?int
|
||||
{
|
||||
$query = User::query()
|
||||
->where(static function (Builder $query): void {
|
||||
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
||||
})
|
||||
->withCount(['helpdeskAssignedTickets as open_helpdesk_tickets_count' => static function (Builder $query): void {
|
||||
$query->whereNull('completed_at');
|
||||
}]);
|
||||
|
||||
if ($categoryId !== null) {
|
||||
$category = Category::query()->find($categoryId);
|
||||
$agentIds = $category?->agents()->pluck('users.id')->all() ?? [];
|
||||
|
||||
if ($agentIds !== []) {
|
||||
$query->whereIn('id', $agentIds);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var User|null $agent */
|
||||
$agent = $query->orderBy('open_helpdesk_tickets_count')->orderBy('id')->first();
|
||||
|
||||
return $agent?->id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Helpdesk\Models\Attachment;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class AttachmentService
|
||||
{
|
||||
/**
|
||||
* Store uploaded files against a ticket and optional comment.
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @param array<int, UploadedFile> $files
|
||||
* @param int $userId
|
||||
* @param Comment|null $comment
|
||||
* @return void
|
||||
*/
|
||||
public function storeMany(Ticket $ticket, array $files, int $userId, ?Comment $comment = null): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
$disk = (string) config('helpdesk.disk');
|
||||
$path = $file->store('tickets/' . $ticket->id, $disk);
|
||||
|
||||
Attachment::query()->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'comment_id' => $comment?->id,
|
||||
'user_id' => $userId,
|
||||
'disk' => $disk,
|
||||
'path' => $path,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'mime_type' => $file->getClientMimeType(),
|
||||
'size' => $file->getSize() ?: 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a stored attachment and its backing file.
|
||||
*
|
||||
* @param Attachment $attachment
|
||||
* @return void
|
||||
*/
|
||||
public function delete(Attachment $attachment): void
|
||||
{
|
||||
Storage::disk($attachment->disk)->delete($attachment->path);
|
||||
$attachment->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Services;
|
||||
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class TicketReferenceService
|
||||
{
|
||||
/**
|
||||
* Build the next human-readable ticket reference.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function next(): string
|
||||
{
|
||||
$year = now()->format('Y');
|
||||
$prefix = 'HD-' . $year . '-';
|
||||
$last = Ticket::query()
|
||||
->where('reference', 'like', $prefix . '%')
|
||||
->latest('id')
|
||||
->value('reference');
|
||||
|
||||
$next = $last ? ((int) substr((string) $last, -5)) + 1 : 1;
|
||||
|
||||
return $prefix . str_pad((string) $next, 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
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,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ticket and store initial attachments.
|
||||
*
|
||||
* @param User $user
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<int, \Illuminate\Http\UploadedFile> $files
|
||||
* @return Ticket
|
||||
*/
|
||||
public function create(User $user, array $data, array $files = []): Ticket
|
||||
{
|
||||
return DB::transaction(function () use ($user, $data, $files): Ticket {
|
||||
$status = Status::query()->where('slug', config('helpdesk.default_status_slug'))->firstOrFail();
|
||||
|
||||
/** @var Ticket $ticket */
|
||||
$ticket = Ticket::query()->create([
|
||||
'reference' => $this->references->next(),
|
||||
'user_id' => $user->id,
|
||||
'agent_id' => $this->assignment->selectAgentId($data['category_id'] ?? null),
|
||||
'status_id' => $status->id,
|
||||
'priority_id' => $data['priority_id'],
|
||||
'category_id' => $data['category_id'] ?? null,
|
||||
'project_id' => $data['project_id'] ?? null,
|
||||
'subject' => $data['subject'],
|
||||
'body' => $data['body'],
|
||||
'due_at' => $data['due_at'] ?? null,
|
||||
'last_activity_at' => now(),
|
||||
]);
|
||||
|
||||
$this->attachments->storeMany($ticket, $files, (int) $user->id);
|
||||
|
||||
return $ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'attachments']) ?? $ticket;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a public or internal comment and optional attachments.
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @param User $user
|
||||
* @param string $body
|
||||
* @param bool $internal
|
||||
* @param array<int, \Illuminate\Http\UploadedFile> $files
|
||||
* @return Comment
|
||||
*/
|
||||
public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = []): Comment
|
||||
{
|
||||
return DB::transaction(function () use ($ticket, $user, $body, $internal, $files): Comment {
|
||||
/** @var Comment $comment */
|
||||
$comment = $ticket->comments()->create([
|
||||
'user_id' => $user->id,
|
||||
'body' => $body,
|
||||
'is_internal' => $internal,
|
||||
]);
|
||||
|
||||
$ticket->forceFill(['last_activity_at' => now()])->save();
|
||||
$this->attachments->storeMany($ticket, $files, (int) $user->id, $comment);
|
||||
|
||||
return $comment->fresh(['user', 'attachments']) ?? $comment;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a ticket.
|
||||
*
|
||||
* @param Ticket $ticket
|
||||
* @param User $user
|
||||
* @return 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();
|
||||
|
||||
return $ticket->fresh(['status']) ?? $ticket;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user