diff --git a/Modules/Helpdesk/Database/Migrations/2026_04_24_000004_create_helpdesk_comments_and_attachments_tables.php b/Modules/Helpdesk/Database/Migrations/2026_04_24_000004_create_helpdesk_comments_and_attachments_tables.php index 83e7011a..3b059424 100644 --- a/Modules/Helpdesk/Database/Migrations/2026_04_24_000004_create_helpdesk_comments_and_attachments_tables.php +++ b/Modules/Helpdesk/Database/Migrations/2026_04_24_000004_create_helpdesk_comments_and_attachments_tables.php @@ -13,12 +13,13 @@ return new class extends Migration { Schema::create('helpdesk_comments', static function (Blueprint $table): void { $table->id(); $table->foreignId('ticket_id')->constrained('helpdesk_tickets')->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('helpdesk_comments')->cascadeOnDelete(); $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); $table->longText('body'); $table->boolean('is_internal')->default(false); $table->timestamps(); - $table->index(['ticket_id', 'created_at']); + $table->index(['ticket_id', 'parent_id', 'created_at']); }); Schema::create('helpdesk_attachments', static function (Blueprint $table): void { @@ -33,7 +34,7 @@ return new class extends Migration { $table->unsignedBigInteger('size')->default(0); $table->timestamps(); - $table->index(['ticket_id', 'created_at']); + $table->index(['ticket_id', 'comment_id', 'created_at']); }); } diff --git a/Modules/Helpdesk/Database/Migrations/2026_04_24_000006_create_helpdesk_git_and_workflow_tables.php b/Modules/Helpdesk/Database/Migrations/2026_04_24_000006_create_helpdesk_git_and_workflow_tables.php new file mode 100644 index 00000000..cf0db684 --- /dev/null +++ b/Modules/Helpdesk/Database/Migrations/2026_04_24_000006_create_helpdesk_git_and_workflow_tables.php @@ -0,0 +1,87 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); + $table->foreignId('project_id')->nullable()->constrained('helpdesk_projects')->nullOnDelete(); + $table->string('name', 160); + $table->string('provider', 40)->default('git'); + $table->string('remote_url', 500); + $table->string('default_branch', 120)->default('main'); + $table->string('webhook_secret', 120)->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index(['company_id', 'project_id']); + $table->index(['provider', 'is_active']); + }); + + Schema::create('helpdesk_git_commits', static function (Blueprint $table): void { + $table->id(); + $table->foreignId('repository_id')->constrained('helpdesk_git_repositories')->cascadeOnDelete(); + $table->foreignId('ticket_id')->nullable()->constrained('helpdesk_tickets')->nullOnDelete(); + $table->string('hash', 80); + $table->string('short_hash', 16); + $table->string('branch', 120)->nullable(); + $table->string('author_name', 160)->nullable(); + $table->string('author_email', 190)->nullable(); + $table->text('message'); + $table->string('url', 500)->nullable(); + $table->timestamp('committed_at')->nullable(); + $table->timestamps(); + + $table->unique(['repository_id', 'hash']); + $table->index(['ticket_id', 'committed_at']); + }); + + Schema::create('helpdesk_workflow_templates', static function (Blueprint $table): void { + $table->id(); + $table->string('name', 160); + $table->string('event', 80); + $table->string('subject', 220); + $table->longText('html_body'); + $table->longText('text_body')->nullable(); + $table->string('to_role', 40)->default('requester'); + $table->string('cc', 500)->nullable(); + $table->string('bcc', 500)->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index(['event', 'is_active']); + }); + + Schema::create('helpdesk_workflow_logs', static function (Blueprint $table): void { + $table->id(); + $table->foreignId('template_id')->nullable()->constrained('helpdesk_workflow_templates')->nullOnDelete(); + $table->foreignId('ticket_id')->nullable()->constrained('helpdesk_tickets')->nullOnDelete(); + $table->string('event', 80); + $table->string('recipient', 190)->nullable(); + $table->string('subject', 220)->nullable(); + $table->string('status', 40)->default('pending'); + $table->text('error')->nullable(); + $table->timestamps(); + + $table->index(['ticket_id', 'event']); + $table->index(['status', 'created_at']); + }); + } + + /** @return void */ + public function down(): void + { + Schema::dropIfExists('helpdesk_workflow_logs'); + Schema::dropIfExists('helpdesk_workflow_templates'); + Schema::dropIfExists('helpdesk_git_commits'); + Schema::dropIfExists('helpdesk_git_repositories'); + } +}; diff --git a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php index 932d74c3..ab50d524 100644 --- a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php +++ b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php @@ -11,6 +11,7 @@ use Modules\Helpdesk\Models\Priority; use Modules\Helpdesk\Models\Project; use Modules\Helpdesk\Models\Status; use Modules\Helpdesk\Models\Team; +use Modules\Helpdesk\Models\WorkflowTemplate; final class HelpdeskDatabaseSeeder extends Seeder { @@ -51,5 +52,18 @@ final class HelpdeskDatabaseSeeder extends Seeder 'color' => 'blue', 'description' => 'Default project for internal helpdesk tickets.', ]); + + foreach ([ + ['name' => 'Ticket raised requester notice', 'event' => 'ticket.created', 'subject' => 'Ticket {{ ticket.reference }} has been raised', 'html_body' => '
Hello {{ requester.name }},
Your ticket {{ ticket.reference }} has been raised successfully.
Subject: {{ ticket.subject }}
Priority: {{ ticket.priority }}
', 'to_role' => 'requester'], + ['name' => 'Ticket updated requester notice', 'event' => 'ticket.updated', 'subject' => 'Ticket {{ ticket.reference }} has been updated', 'html_body' => 'Hello {{ requester.name }},
Your ticket {{ ticket.reference }} has been updated.
Status: {{ ticket.status }}
Priority: {{ ticket.priority }}
', 'to_role' => 'requester'], + ['name' => 'Ticket response requester notice', 'event' => 'ticket.commented', 'subject' => 'New response on ticket {{ ticket.reference }}', 'html_body' => 'Hello {{ requester.name }},
A new response has been added to {{ ticket.reference }}.
Subject: {{ ticket.subject }}
', 'to_role' => 'requester'], + ['name' => 'Ticket closed requester notice', 'event' => 'ticket.closed', 'subject' => 'Ticket {{ ticket.reference }} has been closed', 'html_body' => 'Hello {{ requester.name }},
Your ticket {{ ticket.reference }} has been closed.
Thank you for using the support desk.
', 'to_role' => 'requester'], + ] as $template) { + WorkflowTemplate::query()->updateOrCreate( + ['name' => $template['name'], 'event' => $template['event']], + $template + ['text_body' => null, 'cc' => null, 'bcc' => null, 'is_active' => true] + ); + } + } } diff --git a/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php b/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php index 2595ae84..64563a6b 100644 --- a/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php +++ b/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php @@ -7,6 +7,7 @@ namespace Modules\Helpdesk\Livewire; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Auth; use Livewire\Component; +use Modules\Helpdesk\Models\GitCommit; use Modules\Helpdesk\Models\Ticket; final class HelpdeskDashboardWidget extends Component @@ -15,13 +16,22 @@ final class HelpdeskDashboardWidget extends Component public function render(): View { $user = Auth::user(); - $base = Ticket::query()->visibleTo($user); + $totalCount = (clone $base)->count(); + $openCount = (clone $base)->whereNull('completed_at')->count(); + $closedCount = (clone $base)->whereNotNull('completed_at')->count(); + $criticalCount = (clone $base)->whereNull('completed_at')->whereHas('priority', static fn ($q) => $q->whereIn('slug', ['critical', 'urgent', 'high']))->count(); + $closedPercent = $totalCount > 0 ? (int) round(($closedCount / $totalCount) * 100) : 0; + $openPercent = $totalCount > 0 ? (int) round(($openCount / $totalCount) * 100) : 0; return view('helpdesk::livewire.dashboard-widget', [ - 'openCount' => (clone $base)->whereNull('completed_at')->count(), - 'closedCount' => (clone $base)->whereNotNull('completed_at')->count(), - 'criticalCount' => (clone $base)->whereNull('completed_at')->whereHas('priority', static fn ($q) => $q->where('slug', 'critical'))->count(), + 'totalCount' => $totalCount, + 'openCount' => $openCount, + 'closedCount' => $closedCount, + 'criticalCount' => $criticalCount, + 'closedPercent' => $closedPercent, + 'openPercent' => $openPercent, + 'recentCommits' => GitCommit::query()->with(['repository', 'ticket'])->latest('committed_at')->limit(6)->get(), 'recentTickets' => (clone $base)->with(['status', 'priority'])->latest('last_activity_at')->limit(5)->get(), ]); } diff --git a/Modules/Helpdesk/Livewire/KanbanBoard.php b/Modules/Helpdesk/Livewire/KanbanBoard.php new file mode 100644 index 00000000..56e02b17 --- /dev/null +++ b/Modules/Helpdesk/Livewire/KanbanBoard.php @@ -0,0 +1,76 @@ +visibleTo(Auth::user())->findOrFail($ticketId); + $status = Status::query()->findOrFail($statusId); + + $oldStatus = $ticket->status?->name; + + $ticket->forceFill([ + 'status_id' => $status->id, + 'completed_at' => $status->is_closed ? now() : null, + 'last_activity_at' => now(), + ])->save(); + + $workflow->dispatch( + $ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']), + $status->is_closed ? 'ticket_closed' : 'ticket_updated', + Auth::user() + ); + + session()->flash('success', 'Ticket moved to ' . $status->name . '.'); + } + + /** + * Render the Kanban board. + * + * @return View + */ + public function render(): View + { + return view('helpdesk::livewire.kanban-board', [ + 'statuses' => Status::query()->orderBy('sort_order')->get(), + 'ticketsByStatus' => $this->ticketsByStatus(), + ])->layout('layouts.app'); + } + + /** + * Get visible tickets grouped by status. + * + * @return array| Repository | +Project | +Provider | +Branch | +Commits | +Actions | +
|---|---|---|---|---|---|
| {{ $item->name }} | +{{ $item->project?->title ?? 'Global' }} | +{{ $item->provider }} | +{{ $item->default_branch }} | +
+ |
+ |
| No repositories yet. | |||||
Support operations
-Manage tickets, priorities, statuses, categories, company teams and company projects from one professional workspace.
+Track open workload, closed tickets, response performance, project activity, priorities, Git commits and workflow communication from one professional support workspace.
+Your current support workload.
+Closure rate and remaining open workload.
Latest support activity.
+Helpdesk workflow
+Move tickets through your configured statuses, keep priorities visible and trigger workflow notifications when work changes state.
+{{ $tickets->count() }} tickets
+Helpdesk administration
Manage the lookup data used by tickets, including statuses, priorities, categories, company teams and company projects.
+Manage the lookup data used by tickets, including statuses, priorities, categories, company teams, company projects, Git repositories and workflow messages.
Raise a support request with optional attachments.
-