major ui updates
This commit is contained in:
@@ -12,7 +12,6 @@ use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
final class HelpdeskDashboardWidget extends Component
|
||||
{
|
||||
/** @return View */
|
||||
public function render(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
@@ -8,26 +8,32 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
use Modules\Helpdesk\Models\KanbanBoard as KanbanBoardModel;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Modules\Helpdesk\Services\WorkflowNotificationService;
|
||||
|
||||
final class KanbanBoard extends Component
|
||||
{
|
||||
public ?int $board_id = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->board_id = KanbanBoardModel::query()
|
||||
->where('is_default', true)
|
||||
->orderBy('sort_order')
|
||||
->value('id') ?: KanbanBoardModel::query()->orderBy('sort_order')->value('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a ticket to another workflow status.
|
||||
*
|
||||
* @param int $ticketId
|
||||
* @param int $statusId
|
||||
* @param WorkflowNotificationService $workflow
|
||||
* @return void
|
||||
*/
|
||||
public function moveTicket(int $ticketId, int $statusId, WorkflowNotificationService $workflow): void
|
||||
public function moveTicket(int|string $ticketId, int|string $statusId, WorkflowNotificationService $workflow): void
|
||||
{
|
||||
$ticket = Ticket::query()->visibleTo(Auth::user())->findOrFail($ticketId);
|
||||
$status = Status::query()->findOrFail($statusId);
|
||||
$ticket = Ticket::query()->visibleTo(Auth::user())->findOrFail((int) $ticketId);
|
||||
$status = Status::query()->findOrFail((int) $statusId);
|
||||
|
||||
$oldStatus = $ticket->status?->name;
|
||||
$this->authorize('update', $ticket);
|
||||
|
||||
$ticket->forceFill([
|
||||
'status_id' => $status->id,
|
||||
@@ -36,8 +42,8 @@ final class KanbanBoard extends Component
|
||||
])->save();
|
||||
|
||||
$workflow->dispatch(
|
||||
$ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']),
|
||||
$status->is_closed ? 'ticket_closed' : 'ticket_updated',
|
||||
$ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']) ?? $ticket,
|
||||
$status->is_closed ? 'ticket.closed' : 'ticket.updated',
|
||||
Auth::user()
|
||||
);
|
||||
|
||||
@@ -46,13 +52,22 @@ final class KanbanBoard extends Component
|
||||
|
||||
/**
|
||||
* Render the Kanban board.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
$boards = KanbanBoardModel::query()
|
||||
->with('statuses')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$board = $boards->firstWhere('id', $this->board_id) ?? $boards->first();
|
||||
$statuses = $board?->statuses ?? Status::query()->orderBy('sort_order')->orderBy('name')->get();
|
||||
|
||||
return view('helpdesk::livewire.kanban-board', [
|
||||
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
||||
'boards' => $boards,
|
||||
'board' => $board,
|
||||
'statuses' => $statuses,
|
||||
'ticketsByStatus' => $this->ticketsByStatus(),
|
||||
])->layout('layouts.app');
|
||||
}
|
||||
@@ -68,7 +83,7 @@ final class KanbanBoard extends Component
|
||||
->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])
|
||||
->visibleTo(Auth::user())
|
||||
->latest('last_activity_at')
|
||||
->limit(200)
|
||||
->limit(300)
|
||||
->get()
|
||||
->groupBy('status_id')
|
||||
->all();
|
||||
|
||||
@@ -42,7 +42,6 @@ final class SettingsManager extends Component
|
||||
/** @var array<string, mixed> */
|
||||
public array $workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
|
||||
|
||||
/** @return void */
|
||||
public function saveStatus(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -59,27 +58,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Status saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editStatus(int $id): void
|
||||
{
|
||||
$this->statusForm = Status::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order', 'is_closed']);
|
||||
$this->section = 'statuses';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deleteStatus(int $id): void
|
||||
{
|
||||
Status::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Status deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetStatusForm(): void
|
||||
{
|
||||
$this->statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_closed' => false];
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function savePriority(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -95,27 +90,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Priority saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editPriority(int $id): void
|
||||
{
|
||||
$this->priorityForm = Priority::query()->findOrFail($id)->only(['id', 'name', 'slug', 'color', 'sort_order']);
|
||||
$this->section = 'priorities';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deletePriority(int $id): void
|
||||
{
|
||||
Priority::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Priority deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetPriorityForm(): void
|
||||
{
|
||||
$this->priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0];
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function saveCategory(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -132,27 +123,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Category saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editCategory(int $id): void
|
||||
{
|
||||
$this->categoryForm = Category::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'color', 'description']);
|
||||
$this->section = 'categories';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deleteCategory(int $id): void
|
||||
{
|
||||
Category::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Category deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetCategoryForm(): void
|
||||
{
|
||||
$this->categoryForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => ''];
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function saveTeam(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -169,27 +156,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Team saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editTeam(int $id): void
|
||||
{
|
||||
$this->teamForm = Team::query()->findOrFail($id)->only(['id', 'company_id', 'name', 'slug', 'color', 'description', 'archived_at']);
|
||||
$this->section = 'teams';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deleteTeam(int $id): void
|
||||
{
|
||||
Team::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Team deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetTeamForm(): void
|
||||
{
|
||||
$this->teamForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'archived_at' => null];
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function saveProject(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -207,28 +190,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Project saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editProject(int $id): void
|
||||
{
|
||||
$this->projectForm = Project::query()->findOrFail($id)->only(['id', 'company_id', 'team_id', 'title', 'slug', 'color', 'description', 'archived_at']);
|
||||
$this->section = 'projects';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deleteProject(int $id): void
|
||||
{
|
||||
Project::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Project deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetProjectForm(): void
|
||||
{
|
||||
$this->projectForm = ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'archived_at' => null];
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function saveRepository(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -246,28 +224,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Git repository saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editRepository(int $id): void
|
||||
{
|
||||
$this->repositoryForm = GitRepository::query()->findOrFail($id)->only(['id', 'company_id', 'project_id', 'name', 'provider', 'remote_url', 'default_branch', 'webhook_secret']);
|
||||
$this->section = 'git';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deleteRepository(int $id): void
|
||||
{
|
||||
GitRepository::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Git repository deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetRepositoryForm(): void
|
||||
{
|
||||
$this->repositoryForm = ['id' => null, 'company_id' => null, 'project_id' => null, 'name' => '', 'provider' => 'git', 'remote_url' => '', 'default_branch' => 'main', 'webhook_secret' => ''];
|
||||
}
|
||||
|
||||
|
||||
/** @return void */
|
||||
public function saveWorkflowTemplate(): void
|
||||
{
|
||||
$data = $this->validate([
|
||||
@@ -287,27 +260,23 @@ final class SettingsManager extends Component
|
||||
session()->flash('success', 'Workflow message saved.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function editWorkflowTemplate(int $id): void
|
||||
{
|
||||
$this->workflowForm = WorkflowTemplate::query()->findOrFail($id)->only(['id', 'name', 'event', 'subject', 'html_body', 'text_body', 'to_role', 'cc', 'bcc', 'is_active']);
|
||||
$this->section = 'workflow';
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function deleteWorkflowTemplate(int $id): void
|
||||
{
|
||||
WorkflowTemplate::query()->findOrFail($id)->delete();
|
||||
session()->flash('success', 'Workflow message deleted.');
|
||||
}
|
||||
|
||||
/** @return void */
|
||||
public function resetWorkflowTemplateForm(): void
|
||||
{
|
||||
$this->workflowForm = ['id' => null, 'name' => '', 'event' => 'ticket.created', 'subject' => '', 'html_body' => '', 'text_body' => '', 'to_role' => 'requester', 'cc' => '', 'bcc' => '', 'is_active' => true];
|
||||
}
|
||||
|
||||
/** @return View */
|
||||
public function render(): View
|
||||
{
|
||||
return view('helpdesk::livewire.settings-manager', [
|
||||
|
||||
@@ -26,9 +26,6 @@ final class TicketEdit extends Component
|
||||
#[Validate('nullable|integer')]
|
||||
public ?int $company_id = null;
|
||||
|
||||
#[Validate('nullable|integer|exists:users,id')]
|
||||
public ?int $agent_id = null;
|
||||
|
||||
#[Validate('required|string|max:200')]
|
||||
public string $subject = '';
|
||||
|
||||
@@ -50,19 +47,18 @@ final class TicketEdit extends Component
|
||||
#[Validate('nullable|integer|exists:helpdesk_teams,id')]
|
||||
public ?int $team_id = null;
|
||||
|
||||
#[Validate('nullable|integer|exists:users,id')]
|
||||
public ?int $agent_id = null;
|
||||
|
||||
#[Validate('nullable|date')]
|
||||
public ?string $due_at = null;
|
||||
|
||||
/**
|
||||
* Populate editable ticket state.
|
||||
*/
|
||||
public function mount(Ticket $ticket): void
|
||||
{
|
||||
$this->authorize('update', $ticket);
|
||||
$this->ticket = $ticket->fresh(['status', 'priority', 'category', 'project', 'team', 'agent']) ?? $ticket;
|
||||
|
||||
$this->company_id = $this->ticket->company_id;
|
||||
$this->agent_id = $this->ticket->agent_id;
|
||||
$this->subject = $this->ticket->subject;
|
||||
$this->body = $this->ticket->body;
|
||||
$this->status_id = $this->ticket->status_id;
|
||||
@@ -70,12 +66,10 @@ final class TicketEdit extends Component
|
||||
$this->category_id = $this->ticket->category_id;
|
||||
$this->project_id = $this->ticket->project_id;
|
||||
$this->team_id = $this->ticket->team_id;
|
||||
$this->agent_id = $this->ticket->agent_id;
|
||||
$this->due_at = $this->ticket->due_at?->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save ticket changes.
|
||||
*/
|
||||
public function save(WorkflowNotificationService $workflow): mixed
|
||||
{
|
||||
$this->authorize('update', $this->ticket);
|
||||
@@ -85,7 +79,6 @@ final class TicketEdit extends Component
|
||||
|
||||
$this->ticket->forceFill([
|
||||
'company_id' => $this->company_id,
|
||||
'agent_id' => $this->agent_id,
|
||||
'subject' => $this->subject,
|
||||
'body' => $this->body,
|
||||
'status_id' => $status->id,
|
||||
@@ -93,27 +86,25 @@ final class TicketEdit extends Component
|
||||
'category_id' => $this->category_id,
|
||||
'project_id' => $this->project_id,
|
||||
'team_id' => $this->team_id,
|
||||
'due_at' => $this->due_at ?: null,
|
||||
'agent_id' => $this->agent_id,
|
||||
'due_at' => $this->due_at,
|
||||
'completed_at' => $status->is_closed ? now() : null,
|
||||
'last_activity_at' => now(),
|
||||
])->save();
|
||||
|
||||
$this->ticket = $this->ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']) ?? $this->ticket;
|
||||
$workflow->dispatch($this->ticket, $status->is_closed ? 'ticket.closed' : 'ticket.updated', Auth::user());
|
||||
$fresh = $this->ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']) ?? $this->ticket;
|
||||
$workflow->dispatch($fresh, $status->is_closed ? 'ticket.closed' : 'ticket.updated', Auth::user());
|
||||
|
||||
session()->flash('success', 'Ticket ' . $this->ticket->reference . ' has been updated.');
|
||||
session()->flash('success', 'Ticket ' . $fresh->reference . ' has been updated.');
|
||||
|
||||
return redirect()->route('helpdesk.tickets.show', $this->ticket);
|
||||
return redirect()->route('helpdesk.tickets.show', $fresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ticket edit screen.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('helpdesk::livewire.ticket-edit', [
|
||||
'statuses' => Status::query()->orderBy('sort_order')->get(),
|
||||
'priorities' => Priority::query()->orderBy('sort_order')->get(),
|
||||
'statuses' => Status::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'priorities' => Priority::query()->orderBy('sort_order')->orderBy('name')->get(),
|
||||
'categories' => Category::query()->orderBy('name')->get(),
|
||||
'companies' => Schema::hasTable('companies') ? DB::table('companies')->select(['id', 'name'])->orderBy('name')->get() : collect(),
|
||||
'teams' => Team::query()->whereNull('archived_at')->orderBy('name')->get(),
|
||||
|
||||
@@ -33,8 +33,6 @@ final class TicketIndex extends Component
|
||||
|
||||
/**
|
||||
* Reset pagination when filters change.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updated(): void
|
||||
{
|
||||
@@ -43,8 +41,6 @@ final class TicketIndex extends Component
|
||||
|
||||
/**
|
||||
* Render the ticket list.
|
||||
*
|
||||
* @return View
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
@@ -73,9 +69,9 @@ final class TicketIndex extends Component
|
||||
->when($this->priority !== '', fn (Builder $query): Builder => $query->where('priority_id', (int) $this->priority))
|
||||
->when($this->search !== '', function (Builder $query): Builder {
|
||||
return $query->where(static function (Builder $subQuery): void {
|
||||
$subQuery->where('reference', 'like', '%' . $this->search . '%')
|
||||
->orWhere('subject', 'like', '%' . $this->search . '%')
|
||||
->orWhere('body', 'like', '%' . $this->search . '%');
|
||||
$subQuery->where('reference', 'like', '%'.$this->search.'%')
|
||||
->orWhere('subject', 'like', '%'.$this->search.'%')
|
||||
->orWhere('body', 'like', '%'.$this->search.'%');
|
||||
});
|
||||
})
|
||||
->latest('last_activity_at')
|
||||
|
||||
@@ -12,7 +12,6 @@ use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
@@ -145,36 +144,39 @@ final class TicketShow extends Component
|
||||
'team',
|
||||
'attachments.user',
|
||||
'gitCommits.repository',
|
||||
'comments.attachments',
|
||||
'comments.replies.attachments',
|
||||
'comments.replies.replies.attachments',
|
||||
]) ?? $ticket;
|
||||
|
||||
$comments = Comment::query()
|
||||
->where('ticket_id', $ticket->id)
|
||||
->with(['user', 'attachments'])
|
||||
->oldest()
|
||||
->get();
|
||||
$comments = $ticket->comments
|
||||
->merge($ticket->comments->flatMap->replies)
|
||||
->merge($ticket->comments->flatMap->replies->flatMap->replies);
|
||||
|
||||
$grouped = $comments->groupBy('parent_id');
|
||||
$users = User::query()
|
||||
->whereIn('id', $comments->pluck('user_id')->filter()->unique())
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$comments->each(function ($comment) use ($grouped): void {
|
||||
$comment->setRelation('replies', $grouped->get($comment->id, collect()));
|
||||
});
|
||||
|
||||
$ticket->setRelation('comments', $this->buildCommentTree($grouped));
|
||||
$this->attachCommentUsers($ticket->comments, $users);
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
private function buildCommentTree($grouped, ?int $parentId = null)
|
||||
|
||||
/**
|
||||
* Attach loaded users to comment hierarchy without triggering lazy loading.
|
||||
*
|
||||
* @param Collection<int, mixed> $comments
|
||||
* @param Collection<int, User> $users
|
||||
*/
|
||||
private function attachCommentUsers(Collection $comments, Collection $users): void
|
||||
{
|
||||
foreach ($comments as $comment) {
|
||||
$comment->setRelation('user', $users->get($comment->user_id));
|
||||
|
||||
return $grouped->get($parentId, collect())->map(function ($comment) use ($grouped) {
|
||||
|
||||
$comment->setRelation('replies', $this->buildCommentTree($grouped, $comment->id));
|
||||
|
||||
return $comment;
|
||||
|
||||
});
|
||||
|
||||
if ($comment->relationLoaded('replies')) {
|
||||
$this->attachCommentUsers($comment->replies, $users);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user