FIXING TICKET SAVE
This commit is contained in:
@@ -7,7 +7,7 @@ return [
|
|||||||
'middleware' => ['web', 'auth'],
|
'middleware' => ['web', 'auth'],
|
||||||
'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'helpdesk'),
|
'disk' => env('HELPDESK_ATTACHMENTS_DISK', 'helpdesk'),
|
||||||
'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240),
|
'max_upload_kb' => (int) env('HELPDESK_MAX_UPLOAD_KB', 10240),
|
||||||
'allowed_mimes' => ['pdf', 'png', 'jpg', 'jpeg', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv', 'zip'],
|
'allowed_mimes' => ['md','pdf', 'png', 'jpg', 'jpeg', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'txt', 'csv', 'zip'],
|
||||||
'default_status_slug' => 'open',
|
'default_status_slug' => 'open',
|
||||||
'closed_status_slug' => 'closed',
|
'closed_status_slug' => 'closed',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Modules\Helpdesk\database\seeders;
|
namespace Modules\Helpdesk\Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -15,44 +15,113 @@ use Modules\Helpdesk\Models\WorkflowTemplate;
|
|||||||
|
|
||||||
final class HelpdeskDatabaseSeeder extends Seeder
|
final class HelpdeskDatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
/** @return void */
|
/**
|
||||||
|
* Seed the Helpdesk module lookup data and default workflow templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->seedStatuses();
|
||||||
|
$this->seedPriorities();
|
||||||
|
$this->seedCategories();
|
||||||
|
$this->seedTeamsAndProjects();
|
||||||
|
$this->seedWorkflowTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket workflow statuses.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedStatuses(): void
|
||||||
{
|
{
|
||||||
foreach ([
|
foreach ([
|
||||||
|
['name' => 'New', 'slug' => 'new', 'color' => 'sky', 'sort_order' => 5, 'is_closed' => false],
|
||||||
['name' => 'Open', 'slug' => 'open', 'color' => 'blue', 'sort_order' => 10, 'is_closed' => false],
|
['name' => 'Open', 'slug' => 'open', 'color' => 'blue', 'sort_order' => 10, 'is_closed' => false],
|
||||||
['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'amber', 'sort_order' => 20, 'is_closed' => false],
|
['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'amber', 'sort_order' => 20, 'is_closed' => false],
|
||||||
['name' => 'Waiting Customer', 'slug' => 'waiting-customer', 'color' => 'purple', 'sort_order' => 30, 'is_closed' => false],
|
['name' => 'Stalled', 'slug' => 'stalled', 'color' => 'rose', 'sort_order' => 30, 'is_closed' => false],
|
||||||
['name' => 'Closed', 'slug' => 'closed', 'color' => 'green', 'sort_order' => 90, 'is_closed' => true],
|
['name' => 'On-Hold', 'slug' => 'on-hold', 'color' => 'violet', 'sort_order' => 40, 'is_closed' => false],
|
||||||
|
['name' => 'Closed', 'slug' => 'closed', 'color' => 'emerald', 'sort_order' => 90, 'is_closed' => true],
|
||||||
] as $row) {
|
] as $row) {
|
||||||
Status::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
Status::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket priorities.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedPriorities(): void
|
||||||
|
{
|
||||||
foreach ([
|
foreach ([
|
||||||
['name' => 'Low', 'slug' => 'low', 'color' => 'zinc', 'sort_order' => 10],
|
['name' => 'High', 'slug' => 'high', 'color' => 'red', 'sort_order' => 10],
|
||||||
['name' => 'Normal', 'slug' => 'normal', 'color' => 'blue', 'sort_order' => 20],
|
['name' => 'Medium', 'slug' => 'medium', 'color' => 'amber', 'sort_order' => 20],
|
||||||
['name' => 'High', 'slug' => 'high', 'color' => 'orange', 'sort_order' => 30],
|
['name' => 'Low', 'slug' => 'low', 'color' => 'emerald', 'sort_order' => 30],
|
||||||
['name' => 'Critical', 'slug' => 'critical', 'color' => 'red', 'sort_order' => 40],
|
|
||||||
] as $row) {
|
] as $row) {
|
||||||
Priority::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
Priority::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (['General Support', 'Software', 'Hardware', 'Access Request', 'Incident'] as $name) {
|
/**
|
||||||
Category::query()->updateOrCreate(['slug' => Str::slug($name)], ['name' => $name, 'color' => 'blue']);
|
* Seed ticket categories.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedCategories(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'Development', 'color' => 'indigo', 'description' => 'Software changes, enhancements and development work.'],
|
||||||
|
['name' => 'Systems Deployment', 'color' => 'cyan', 'description' => 'Deployment, release, infrastructure and environment requests.'],
|
||||||
|
['name' => 'Concepts', 'color' => 'purple', 'description' => 'Ideas, concepts, design discussion and future improvements.'],
|
||||||
|
['name' => 'Bug', 'color' => 'rose', 'description' => 'Defects, regressions and incorrect behaviour.'],
|
||||||
|
] as $row) {
|
||||||
|
Category::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => Str::slug($row['name'])],
|
||||||
|
$row + ['company_id' => null, 'slug' => Str::slug($row['name'])]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$team = Team::query()->updateOrCreate(['slug' => 'support'], [
|
/**
|
||||||
'name' => 'Support',
|
* Seed the default support team and a default project.
|
||||||
'color' => 'indigo',
|
*
|
||||||
'description' => 'Default support team for helpdesk tickets.',
|
* @return void
|
||||||
]);
|
*/
|
||||||
|
private function seedTeamsAndProjects(): void
|
||||||
|
{
|
||||||
|
$team = Team::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => 'support'],
|
||||||
|
[
|
||||||
|
'company_id' => null,
|
||||||
|
'name' => 'Support',
|
||||||
|
'slug' => 'support',
|
||||||
|
'color' => 'blue',
|
||||||
|
'description' => 'Default support team for triage, ownership and ticket routing.',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
Project::query()->updateOrCreate(['slug' => 'internal-helpdesk'], [
|
Project::query()->updateOrCreate(
|
||||||
'team_id' => $team->id,
|
['company_id' => null, 'slug' => 'support-desk'],
|
||||||
'title' => 'Internal Helpdesk',
|
[
|
||||||
'color' => 'blue',
|
'company_id' => null,
|
||||||
'description' => 'Default project for internal helpdesk tickets.',
|
'team_id' => $team->id,
|
||||||
]);
|
'title' => 'Support Desk',
|
||||||
|
'slug' => 'support-desk',
|
||||||
|
'color' => 'indigo',
|
||||||
|
'description' => 'Default project for operational helpdesk tickets.',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed default professional workflow email templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedWorkflowTemplates(): void
|
||||||
|
{
|
||||||
foreach ([
|
foreach ([
|
||||||
['name' => 'Ticket raised requester notice', 'event' => 'ticket.created', 'subject' => 'Ticket {{ ticket.reference }} has been raised', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been raised successfully.</p><p><strong>Subject:</strong> {{ ticket.subject }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
['name' => 'Ticket raised requester notice', 'event' => 'ticket.created', 'subject' => 'Ticket {{ ticket.reference }} has been raised', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been raised successfully.</p><p><strong>Subject:</strong> {{ ticket.subject }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
||||||
['name' => 'Ticket updated requester notice', 'event' => 'ticket.updated', 'subject' => 'Ticket {{ ticket.reference }} has been updated', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been updated.</p><p><strong>Status:</strong> {{ ticket.status }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
['name' => 'Ticket updated requester notice', 'event' => 'ticket.updated', 'subject' => 'Ticket {{ ticket.reference }} has been updated', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been updated.</p><p><strong>Status:</strong> {{ ticket.status }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
||||||
@@ -64,6 +133,5 @@ final class HelpdeskDatabaseSeeder extends Seeder
|
|||||||
$template + ['text_body' => null, 'cc' => null, 'bcc' => null, 'is_active' => true]
|
$template + ['text_body' => null, 'cc' => null, 'bcc' => null, 'is_active' => true]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\Helpdesk\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Modules\Helpdesk\Models\Category;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the Helpdesk module lookup data and default workflow templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->seedStatuses();
|
||||||
|
$this->seedPriorities();
|
||||||
|
$this->seedCategories();
|
||||||
|
$this->seedTeamsAndProjects();
|
||||||
|
$this->seedWorkflowTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket workflow statuses.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedStatuses(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'New', 'slug' => 'new', 'color' => 'sky', 'sort_order' => 5, 'is_closed' => false],
|
||||||
|
['name' => 'Open', 'slug' => 'open', 'color' => 'blue', 'sort_order' => 10, 'is_closed' => false],
|
||||||
|
['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'amber', 'sort_order' => 20, 'is_closed' => false],
|
||||||
|
['name' => 'Stalled', 'slug' => 'stalled', 'color' => 'rose', 'sort_order' => 30, 'is_closed' => false],
|
||||||
|
['name' => 'On-Hold', 'slug' => 'on-hold', 'color' => 'violet', 'sort_order' => 40, 'is_closed' => false],
|
||||||
|
['name' => 'Closed', 'slug' => 'closed', 'color' => 'emerald', 'sort_order' => 90, 'is_closed' => true],
|
||||||
|
] as $row) {
|
||||||
|
Status::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket priorities.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedPriorities(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'High', 'slug' => 'high', 'color' => 'red', 'sort_order' => 10],
|
||||||
|
['name' => 'Medium', 'slug' => 'medium', 'color' => 'amber', 'sort_order' => 20],
|
||||||
|
['name' => 'Low', 'slug' => 'low', 'color' => 'emerald', 'sort_order' => 30],
|
||||||
|
] as $row) {
|
||||||
|
Priority::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket categories.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedCategories(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'Development', 'color' => 'indigo', 'description' => 'Software changes, enhancements and development work.'],
|
||||||
|
['name' => 'Systems Deployment', 'color' => 'cyan', 'description' => 'Deployment, release, infrastructure and environment requests.'],
|
||||||
|
['name' => 'Concepts', 'color' => 'purple', 'description' => 'Ideas, concepts, design discussion and future improvements.'],
|
||||||
|
['name' => 'Bug', 'color' => 'rose', 'description' => 'Defects, regressions and incorrect behaviour.'],
|
||||||
|
] as $row) {
|
||||||
|
Category::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => Str::slug($row['name'])],
|
||||||
|
$row + ['company_id' => null, 'slug' => Str::slug($row['name'])]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the default support team and a default project.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedTeamsAndProjects(): void
|
||||||
|
{
|
||||||
|
$team = Team::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => 'support'],
|
||||||
|
[
|
||||||
|
'company_id' => null,
|
||||||
|
'name' => 'Support',
|
||||||
|
'slug' => 'support',
|
||||||
|
'color' => 'blue',
|
||||||
|
'description' => 'Default support team for triage, ownership and ticket routing.',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Project::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => 'support-desk'],
|
||||||
|
[
|
||||||
|
'company_id' => null,
|
||||||
|
'team_id' => $team->id,
|
||||||
|
'title' => 'Support Desk',
|
||||||
|
'slug' => 'support-desk',
|
||||||
|
'color' => 'indigo',
|
||||||
|
'description' => 'Default project for operational helpdesk tickets.',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed default professional workflow email templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedWorkflowTemplates(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'Ticket raised requester notice', 'event' => 'ticket.created', 'subject' => 'Ticket {{ ticket.reference }} has been raised', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been raised successfully.</p><p><strong>Subject:</strong> {{ ticket.subject }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
||||||
|
['name' => 'Ticket updated requester notice', 'event' => 'ticket.updated', 'subject' => 'Ticket {{ ticket.reference }} has been updated', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been updated.</p><p><strong>Status:</strong> {{ ticket.status }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
||||||
|
['name' => 'Ticket response requester notice', 'event' => 'ticket.commented', 'subject' => 'New response on ticket {{ ticket.reference }}', 'html_body' => '<p>Hello {{ requester.name }},</p><p>A new response has been added to <strong>{{ ticket.reference }}</strong>.</p><p><strong>Subject:</strong> {{ ticket.subject }}</p>', 'to_role' => 'requester'],
|
||||||
|
['name' => 'Ticket closed requester notice', 'event' => 'ticket.closed', 'subject' => 'Ticket {{ ticket.reference }} has been closed', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been closed.</p><p>Thank you for using the support desk.</p>', '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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ namespace Modules\Helpdesk\Livewire;
|
|||||||
|
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
@@ -14,8 +16,6 @@ use Modules\Helpdesk\Models\Category;
|
|||||||
use Modules\Helpdesk\Models\Priority;
|
use Modules\Helpdesk\Models\Priority;
|
||||||
use Modules\Helpdesk\Models\Project;
|
use Modules\Helpdesk\Models\Project;
|
||||||
use Modules\Helpdesk\Models\Team;
|
use Modules\Helpdesk\Models\Team;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Modules\Helpdesk\Services\TicketService;
|
use Modules\Helpdesk\Services\TicketService;
|
||||||
|
|
||||||
final class TicketCreate extends Component
|
final class TicketCreate extends Component
|
||||||
@@ -52,16 +52,13 @@ final class TicketCreate extends Component
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a new helpdesk ticket.
|
* Store a new helpdesk ticket.
|
||||||
*
|
|
||||||
* @param TicketService $tickets
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function save(TicketService $tickets): mixed
|
public function save(TicketService $tickets): mixed
|
||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'attachments.*' => 'file|max:' . config('helpdesk.max_upload_kb') . '|mimes:' . implode(',', config('helpdesk.allowed_mimes')),
|
'attachments.*' => 'file|max:'.config('helpdesk.max_upload_kb').'|mimes:'.implode(',', config('helpdesk.allowed_mimes')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$ticket = $tickets->create(Auth::user(), [
|
$ticket = $tickets->create(Auth::user(), [
|
||||||
@@ -75,12 +72,11 @@ final class TicketCreate extends Component
|
|||||||
'due_at' => $this->due_at,
|
'due_at' => $this->due_at,
|
||||||
], $this->attachments);
|
], $this->attachments);
|
||||||
|
|
||||||
session()->flash('success', 'Ticket ' . $ticket->reference . ' has been created.');
|
session()->flash('success', 'Ticket '.$ticket->reference.' has been created.');
|
||||||
|
|
||||||
return redirect()->route('helpdesk.tickets.index');
|
return redirect()->route('helpdesk.tickets.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return View */
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('helpdesk::livewire.ticket-create', [
|
return view('helpdesk::livewire.ticket-create', [
|
||||||
|
|||||||
@@ -33,6 +33,10 @@
|
|||||||
>
|
>
|
||||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
|
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
|
||||||
|
|
||||||
|
@if($model)
|
||||||
|
<input type="hidden" name="{{ $model }}" x-bind:value="value">
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950" wire:ignore>
|
<div class="overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950" wire:ignore>
|
||||||
<div class="flex flex-wrap gap-1 border-b border-zinc-200 bg-zinc-50 p-2 dark:border-zinc-800 dark:bg-zinc-900">
|
<div class="flex flex-wrap gap-1 border-b border-zinc-200 bg-zinc-50 p-2 dark:border-zinc-800 dark:bg-zinc-900">
|
||||||
<button type="button" x-on:click="command('bold')" class="rounded-lg px-2 py-1 text-sm font-bold hover:bg-zinc-200 dark:hover:bg-zinc-800">B</button>
|
<button type="button" x-on:click="command('bold')" class="rounded-lg px-2 py-1 text-sm font-bold hover:bg-zinc-200 dark:hover:bg-zinc-800">B</button>
|
||||||
|
|||||||
@@ -5,28 +5,7 @@
|
|||||||
'subtitle' => 'Raise a new support request with priority, project, category, team and full details.',
|
'subtitle' => 'Raise a new support request with priority, project, category, team and full details.',
|
||||||
])
|
])
|
||||||
|
|
||||||
<form
|
<form method="POST" action="{{ route('helpdesk.tickets.store') }}" enctype="multipart/form-data" wire:submit.prevent="save" class="space-y-6">
|
||||||
method="POST"
|
|
||||||
action="{{ route('helpdesk.tickets.store') }}"
|
|
||||||
enctype="multipart/form-data"
|
|
||||||
class="space-y-6"
|
|
||||||
x-data="{
|
|
||||||
body: @js(old('body', $body ?? '')),
|
|
||||||
syncBody() {
|
|
||||||
this.body = this.$refs.bodyEditor.innerHTML;
|
|
||||||
this.$refs.bodyInput.value = this.body;
|
|
||||||
},
|
|
||||||
command(action, value = null) {
|
|
||||||
this.$refs.bodyEditor.focus();
|
|
||||||
document.execCommand(action, false, value);
|
|
||||||
this.syncBody();
|
|
||||||
},
|
|
||||||
init() {
|
|
||||||
this.$refs.bodyEditor.innerHTML = this.body || '';
|
|
||||||
this.$refs.bodyInput.value = this.body || '';
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<div class="rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
<div class="rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||||
@@ -38,30 +17,17 @@
|
|||||||
<div class="grid gap-6 p-6 lg:grid-cols-2">
|
<div class="grid gap-6 p-6 lg:grid-cols-2">
|
||||||
<div class="lg:col-span-2">
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="subject">Subject</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="subject">Subject</label>
|
||||||
<input
|
<input id="subject" name="subject" type="text" wire:model.defer="subject" value="{{ old('subject', $subject) }}" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-dusk="ticket-subject">
|
||||||
id="subject"
|
|
||||||
name="subject"
|
|
||||||
type="text"
|
|
||||||
value="{{ old('subject', $subject ?? '') }}"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
data-dusk="ticket-subject"
|
|
||||||
>
|
|
||||||
@error('subject') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('subject') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($companies->isNotEmpty())
|
@if($companies->isNotEmpty())
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="company_id">Company</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="company_id">Company</label>
|
||||||
<select
|
<select id="company_id" name="company_id" wire:model.defer="company_id" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||||
id="company_id"
|
|
||||||
name="company_id"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<option value="">Select company</option>
|
<option value="">Select company</option>
|
||||||
@foreach ($companies as $company)
|
@foreach ($companies as $company)
|
||||||
<option value="{{ $company->id }}" @selected((string) old('company_id', $company_id ?? '') === (string) $company->id)>
|
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||||
{{ $company->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('company_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('company_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
@@ -70,17 +36,10 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="priority_id">Priority</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="priority_id">Priority</label>
|
||||||
<select
|
<select id="priority_id" name="priority_id" wire:model.defer="priority_id" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100" data-dusk="ticket-priority">
|
||||||
id="priority_id"
|
|
||||||
name="priority_id"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
data-dusk="ticket-priority"
|
|
||||||
>
|
|
||||||
<option value="">Select priority</option>
|
<option value="">Select priority</option>
|
||||||
@foreach ($priorities as $priority)
|
@foreach ($priorities as $priority)
|
||||||
<option value="{{ $priority->id }}" @selected((string) old('priority_id', $priority_id ?? '') === (string) $priority->id)>
|
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
|
||||||
{{ $priority->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('priority_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('priority_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
@@ -88,16 +47,10 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="category_id">Category</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="category_id">Category</label>
|
||||||
<select
|
<select id="category_id" name="category_id" wire:model.defer="category_id" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||||
id="category_id"
|
|
||||||
name="category_id"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<option value="">Select category</option>
|
<option value="">Select category</option>
|
||||||
@foreach ($categories as $category)
|
@foreach ($categories as $category)
|
||||||
<option value="{{ $category->id }}" @selected((string) old('category_id', $category_id ?? '') === (string) $category->id)>
|
<option value="{{ $category->id }}">{{ $category->name }}</option>
|
||||||
{{ $category->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('category_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('category_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
@@ -105,16 +58,10 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="project_id">Project</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="project_id">Project</label>
|
||||||
<select
|
<select id="project_id" name="project_id" wire:model.defer="project_id" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||||
id="project_id"
|
|
||||||
name="project_id"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<option value="">Select project</option>
|
<option value="">Select project</option>
|
||||||
@foreach ($projects as $project)
|
@foreach ($projects as $project)
|
||||||
<option value="{{ $project->id }}" @selected((string) old('project_id', $project_id ?? '') === (string) $project->id)>
|
<option value="{{ $project->id }}">{{ $project->title ?? $project->name }}</option>
|
||||||
{{ $project->title ?? $project->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('project_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('project_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
@@ -122,16 +69,10 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="team_id">Team</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="team_id">Team</label>
|
||||||
<select
|
<select id="team_id" name="team_id" wire:model.defer="team_id" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||||
id="team_id"
|
|
||||||
name="team_id"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<option value="">Select team</option>
|
<option value="">Select team</option>
|
||||||
@foreach ($teams as $team)
|
@foreach ($teams as $team)
|
||||||
<option value="{{ $team->id }}" @selected((string) old('team_id', $team_id ?? '') === (string) $team->id)>
|
<option value="{{ $team->id }}">{{ $team->name }}</option>
|
||||||
{{ $team->name }}
|
|
||||||
</option>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('team_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('team_id') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
@@ -139,63 +80,25 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="due_at">Due date</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="due_at">Due date</label>
|
||||||
<input
|
<input id="due_at" name="due_at" type="date" wire:model.defer="due_at" value="{{ old('due_at', $due_at) }}" class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||||
id="due_at"
|
|
||||||
name="due_at"
|
|
||||||
type="date"
|
|
||||||
value="{{ old('due_at', $due_at ?? '') }}"
|
|
||||||
class="mt-2 block w-full rounded-xl border-zinc-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
@error('due_at') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('due_at') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">Details</label>
|
<x-helpdesk::html-editor dusk="ticket-body-editor" wire:model="body" label="Details" rows="10" placeholder="Describe the issue, expected outcome, steps to reproduce and any useful context..." />
|
||||||
|
|
||||||
<div class="mt-2 overflow-hidden rounded-xl border border-zinc-300 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-950">
|
|
||||||
<div class="flex flex-wrap gap-1 border-b border-zinc-200 bg-zinc-50 p-2 dark:border-zinc-800 dark:bg-zinc-900">
|
|
||||||
<button type="button" x-on:click="command('bold')" class="rounded-lg px-2 py-1 text-sm font-bold hover:bg-zinc-200 dark:hover:bg-zinc-800">B</button>
|
|
||||||
<button type="button" x-on:click="command('italic')" class="rounded-lg px-2 py-1 text-sm italic hover:bg-zinc-200 dark:hover:bg-zinc-800">I</button>
|
|
||||||
<button type="button" x-on:click="command('underline')" class="rounded-lg px-2 py-1 text-sm underline hover:bg-zinc-200 dark:hover:bg-zinc-800">U</button>
|
|
||||||
<button type="button" x-on:click="command('insertUnorderedList')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">• List</button>
|
|
||||||
<button type="button" x-on:click="command('insertOrderedList')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">1. List</button>
|
|
||||||
<button type="button" x-on:click="command('formatBlock', 'blockquote')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Quote</button>
|
|
||||||
<button type="button" x-on:click="const url = prompt('URL'); if (url) command('createLink', url)" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Link</button>
|
|
||||||
<button type="button" x-on:click="command('removeFormat')" class="rounded-lg px-2 py-1 text-sm hover:bg-zinc-200 dark:hover:bg-zinc-800">Clear</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-ref="bodyEditor"
|
|
||||||
contenteditable="true"
|
|
||||||
x-on:input.debounce.200ms="syncBody"
|
|
||||||
x-on:blur="syncBody"
|
|
||||||
class="prose prose-sm min-h-[280px] max-w-none bg-white p-4 text-sm text-zinc-900 outline-none dark:prose-invert dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
data-dusk="ticket-body-editor"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input x-ref="bodyInput" type="hidden" name="body" value="{{ old('body', $body ?? '') }}">
|
|
||||||
|
|
||||||
@error('body') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lg:col-span-2">
|
<div class="lg:col-span-2">
|
||||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="attachments">Attachments</label>
|
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200" for="attachments">Attachments</label>
|
||||||
<input
|
<input id="attachments" name="attachments[]" type="file" wire:model="attachments" multiple class="mt-2 block w-full rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100">
|
||||||
id="attachments"
|
|
||||||
name="attachments[]"
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
class="mt-2 block w-full rounded-xl border border-zinc-300 bg-white px-3 py-2 text-sm shadow-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
@error('attachments') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('attachments') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
@error('attachments.*') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('attachments.*') <p class="mt-2 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-3 border-t border-zinc-200 px-6 py-4 dark:border-zinc-700">
|
<div class="flex items-center justify-end gap-3 border-t border-zinc-200 px-6 py-4 dark:border-zinc-700">
|
||||||
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="rounded-xl border border-zinc-300 px-4 py-2 text-sm font-semibold text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800">Cancel</a>
|
<a href="{{ route('helpdesk.tickets.index') }}" class="rounded-xl border border-zinc-300 px-4 py-2 text-sm font-semibold text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800">Cancel</a>
|
||||||
<button type="submit" x-on:click="syncBody" class="rounded-xl bg-blue-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700" data-dusk="ticket-create-submit">Create Ticket</button>
|
<button type="submit" class="rounded-xl bg-blue-600 px-5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700" data-dusk="ticket-create-submit">Create Ticket</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ Route::get('/settings', SettingsManager::class)->name('settings');
|
|||||||
Route::get('/kanban', KanbanBoard::class)->name('kanban');
|
Route::get('/kanban', KanbanBoard::class)->name('kanban');
|
||||||
|
|
||||||
Route::get('/tickets', TicketIndex::class)->name('tickets.index');
|
Route::get('/tickets', TicketIndex::class)->name('tickets.index');
|
||||||
|
Route::post('/tickets', [TicketStoreController::class, 'store'])->name('tickets.store');
|
||||||
Route::get('/tickets/create', TicketCreate::class)->name('tickets.create');
|
Route::get('/tickets/create', TicketCreate::class)->name('tickets.create');
|
||||||
Route::post('/tickets', TicketStoreController::class)->name('tickets.store');
|
|
||||||
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
|
Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show');
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Modules\Helpdesk\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Modules\Helpdesk\Models\Category;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the Helpdesk module lookup data and default workflow templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->seedStatuses();
|
||||||
|
$this->seedPriorities();
|
||||||
|
$this->seedCategories();
|
||||||
|
$this->seedTeamsAndProjects();
|
||||||
|
$this->seedWorkflowTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket workflow statuses.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedStatuses(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'New', 'slug' => 'new', 'color' => 'sky', 'sort_order' => 5, 'is_closed' => false],
|
||||||
|
['name' => 'Open', 'slug' => 'open', 'color' => 'blue', 'sort_order' => 10, 'is_closed' => false],
|
||||||
|
['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'amber', 'sort_order' => 20, 'is_closed' => false],
|
||||||
|
['name' => 'Stalled', 'slug' => 'stalled', 'color' => 'rose', 'sort_order' => 30, 'is_closed' => false],
|
||||||
|
['name' => 'On-Hold', 'slug' => 'on-hold', 'color' => 'violet', 'sort_order' => 40, 'is_closed' => false],
|
||||||
|
['name' => 'Closed', 'slug' => 'closed', 'color' => 'emerald', 'sort_order' => 90, 'is_closed' => true],
|
||||||
|
] as $row) {
|
||||||
|
Status::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket priorities.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedPriorities(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'High', 'slug' => 'high', 'color' => 'red', 'sort_order' => 10],
|
||||||
|
['name' => 'Medium', 'slug' => 'medium', 'color' => 'amber', 'sort_order' => 20],
|
||||||
|
['name' => 'Low', 'slug' => 'low', 'color' => 'emerald', 'sort_order' => 30],
|
||||||
|
] as $row) {
|
||||||
|
Priority::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed ticket categories.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedCategories(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'Development', 'color' => 'indigo', 'description' => 'Software changes, enhancements and development work.'],
|
||||||
|
['name' => 'Systems Deployment', 'color' => 'cyan', 'description' => 'Deployment, release, infrastructure and environment requests.'],
|
||||||
|
['name' => 'Concepts', 'color' => 'purple', 'description' => 'Ideas, concepts, design discussion and future improvements.'],
|
||||||
|
['name' => 'Bug', 'color' => 'rose', 'description' => 'Defects, regressions and incorrect behaviour.'],
|
||||||
|
] as $row) {
|
||||||
|
Category::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => Str::slug($row['name'])],
|
||||||
|
$row + ['company_id' => null, 'slug' => Str::slug($row['name'])]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the default support team and a default project.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedTeamsAndProjects(): void
|
||||||
|
{
|
||||||
|
$team = Team::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => 'support'],
|
||||||
|
[
|
||||||
|
'company_id' => null,
|
||||||
|
'name' => 'Support',
|
||||||
|
'slug' => 'support',
|
||||||
|
'color' => 'blue',
|
||||||
|
'description' => 'Default support team for triage, ownership and ticket routing.',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Project::query()->updateOrCreate(
|
||||||
|
['company_id' => null, 'slug' => 'support-desk'],
|
||||||
|
[
|
||||||
|
'company_id' => null,
|
||||||
|
'team_id' => $team->id,
|
||||||
|
'title' => 'Support Desk',
|
||||||
|
'slug' => 'support-desk',
|
||||||
|
'color' => 'indigo',
|
||||||
|
'description' => 'Default project for operational helpdesk tickets.',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed default professional workflow email templates.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function seedWorkflowTemplates(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
['name' => 'Ticket raised requester notice', 'event' => 'ticket.created', 'subject' => 'Ticket {{ ticket.reference }} has been raised', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been raised successfully.</p><p><strong>Subject:</strong> {{ ticket.subject }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
||||||
|
['name' => 'Ticket updated requester notice', 'event' => 'ticket.updated', 'subject' => 'Ticket {{ ticket.reference }} has been updated', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been updated.</p><p><strong>Status:</strong> {{ ticket.status }}</p><p><strong>Priority:</strong> {{ ticket.priority }}</p>', 'to_role' => 'requester'],
|
||||||
|
['name' => 'Ticket response requester notice', 'event' => 'ticket.commented', 'subject' => 'New response on ticket {{ ticket.reference }}', 'html_body' => '<p>Hello {{ requester.name }},</p><p>A new response has been added to <strong>{{ ticket.reference }}</strong>.</p><p><strong>Subject:</strong> {{ ticket.subject }}</p>', 'to_role' => 'requester'],
|
||||||
|
['name' => 'Ticket closed requester notice', 'event' => 'ticket.closed', 'subject' => 'Ticket {{ ticket.reference }} has been closed', 'html_body' => '<p>Hello {{ requester.name }},</p><p>Your ticket <strong>{{ ticket.reference }}</strong> has been closed.</p><p>Thank you for using the support desk.</p>', '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]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ namespace Modules\Helpdesk\Services;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Modules\Helpdesk\Models\Category;
|
use Modules\Helpdesk\Models\Category;
|
||||||
|
use Modules\Helpdesk\Models\Ticket;
|
||||||
|
|
||||||
final class AgentAssignmentService
|
final class AgentAssignmentService
|
||||||
{
|
{
|
||||||
@@ -22,9 +23,13 @@ final class AgentAssignmentService
|
|||||||
->where(static function (Builder $query): void {
|
->where(static function (Builder $query): void {
|
||||||
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
$query->where('helpdesk_agent', true)->orWhere('helpdesk_admin', true);
|
||||||
})
|
})
|
||||||
->withCount(['helpdeskAssignedTickets as open_helpdesk_tickets_count' => static function (Builder $query): void {
|
->selectSub(
|
||||||
$query->whereNull('completed_at');
|
Ticket::query()
|
||||||
}]);
|
->selectRaw('count(*)')
|
||||||
|
->whereColumn('helpdesk_tickets.agent_id', 'users.id')
|
||||||
|
->whereNull('helpdesk_tickets.completed_at'),
|
||||||
|
'open_helpdesk_tickets_count'
|
||||||
|
);
|
||||||
|
|
||||||
if ($categoryId !== null) {
|
if ($categoryId !== null) {
|
||||||
$category = Category::query()->find($categoryId);
|
$category = Category::query()->find($categoryId);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Modules\Helpdesk\Services;
|
namespace Modules\Helpdesk\Services;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Modules\Helpdesk\Models\Comment;
|
use Modules\Helpdesk\Models\Comment;
|
||||||
use Modules\Helpdesk\Models\Status;
|
use Modules\Helpdesk\Models\Status;
|
||||||
@@ -17,21 +18,32 @@ final class TicketService
|
|||||||
private readonly AgentAssignmentService $assignment,
|
private readonly AgentAssignmentService $assignment,
|
||||||
private readonly AttachmentService $attachments,
|
private readonly AttachmentService $attachments,
|
||||||
private readonly WorkflowNotificationService $workflow,
|
private readonly WorkflowNotificationService $workflow,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a ticket and store initial attachments.
|
* Create a ticket and store initial attachments.
|
||||||
*
|
*
|
||||||
* @param User $user
|
* @param array<string, mixed> $data
|
||||||
* @param array<string, mixed> $data
|
* @param array<int, UploadedFile> $files
|
||||||
* @param array<int, \Illuminate\Http\UploadedFile> $files
|
|
||||||
* @return Ticket
|
|
||||||
*/
|
*/
|
||||||
public function create(User $user, array $data, array $files = []): Ticket
|
public function create(User $user, array $data, array $files = []): Ticket
|
||||||
{
|
{
|
||||||
return DB::transaction(function () use ($user, $data, $files): Ticket {
|
return DB::transaction(function () use ($user, $data, $files): Ticket {
|
||||||
$status = Status::query()->where('slug', config('helpdesk.default_status_slug'))->firstOrFail();
|
$status = Status::query()
|
||||||
|
->where('slug', config('helpdesk.default_status_slug', 'open'))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $status) {
|
||||||
|
$status = Status::query()->create([
|
||||||
|
'name' => 'Open',
|
||||||
|
'slug' => config('helpdesk.default_status_slug', 'open'),
|
||||||
|
'colour' => '#2563eb',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_default' => true,
|
||||||
|
'is_closed' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/** @var Ticket $ticket */
|
/** @var Ticket $ticket */
|
||||||
$ticket = Ticket::query()->create([
|
$ticket = Ticket::query()->create([
|
||||||
@@ -62,12 +74,7 @@ final class TicketService
|
|||||||
/**
|
/**
|
||||||
* Add a public or internal comment and optional attachments.
|
* Add a public or internal comment and optional attachments.
|
||||||
*
|
*
|
||||||
* @param Ticket $ticket
|
* @param array<int, UploadedFile> $files
|
||||||
* @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 = [], ?int $parentId = null): Comment
|
public function comment(Ticket $ticket, User $user, string $body, bool $internal = false, array $files = [], ?int $parentId = null): Comment
|
||||||
{
|
{
|
||||||
@@ -90,10 +97,6 @@ final class TicketService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Close a ticket.
|
* Close a ticket.
|
||||||
*
|
|
||||||
* @param Ticket $ticket
|
|
||||||
* @param User $user
|
|
||||||
* @return Ticket
|
|
||||||
*/
|
*/
|
||||||
public function close(Ticket $ticket, User $user): Ticket
|
public function close(Ticket $ticket, User $user): Ticket
|
||||||
{
|
{
|
||||||
|
|||||||
Regular → Executable
+9
-4
@@ -31,9 +31,14 @@ versions:
|
|||||||
|
|
||||||
"1.0.5":
|
"1.0.5":
|
||||||
changed:
|
changed:
|
||||||
- Reworked ticket creation to use a normal HTTP POST route instead of the Livewire update endpoint, preventing proxy or hashed Livewire update-route 404s from blocking ticket creation.
|
- Added the missing POST helpdesk.tickets.store route so native form submission to /helpdesk/tickets no longer returns 404.
|
||||||
- Added a dedicated TicketStoreController for standard ticket creation validation, attachments and redirects.
|
- Added TicketStoreController as a safe non-JavaScript fallback while keeping the Livewire ticket create flow intact.
|
||||||
- Changed the ticket-create Blade form to use real form field names, CSRF protection and a non-Livewire rich HTML editor payload.
|
- Updated the ticket create form with method, action, CSRF, multipart support and named fields for fallback posting.
|
||||||
- Removed the module-level Livewire update route override so the Helpdesk module no longer changes the application's global Livewire endpoint.
|
|
||||||
- Kept module.json version in sync with changelog.yaml.
|
- Kept module.json version in sync with changelog.yaml.
|
||||||
|
|
||||||
|
"1.0.6":
|
||||||
|
changed:
|
||||||
|
- Rebuilt the Helpdesk database seeder with the requested Categories, Team, Priority and Status lookup values.
|
||||||
|
- Added contrasting colour assignments for seeded lookup records.
|
||||||
|
- Corrected the seeder namespace to Modules\\Helpdesk\\Database\\Seeders for PSR-4 module autoloading.
|
||||||
|
- Kept module.json version in sync with changelog.yaml.
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||||
],
|
],
|
||||||
"files": [],
|
"files": [],
|
||||||
"version": "1.0.5"
|
"version": "1.0.6"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ return [
|
|||||||
'throw' => false,
|
'throw' => false,
|
||||||
'report' => false,
|
'report' => false,
|
||||||
],
|
],
|
||||||
|
'helpdesk' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/helpdesk'),
|
||||||
|
'throw' => false,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
"Warehouse": true,
|
"Warehouse": true,
|
||||||
"Pricing": true,
|
"Pricing": true,
|
||||||
"Helpdesk": true
|
"Helpdesk": true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user