diff --git a/Modules/Helpdesk/Database/Migrations/2026_04_24_000002_create_helpdesk_lookup_tables.php b/Modules/Helpdesk/Database/Migrations/2026_04_24_000002_create_helpdesk_lookup_tables.php index e0e2fd37..982e6b28 100644 --- a/Modules/Helpdesk/Database/Migrations/2026_04_24_000002_create_helpdesk_lookup_tables.php +++ b/Modules/Helpdesk/Database/Migrations/2026_04_24_000002_create_helpdesk_lookup_tables.php @@ -31,22 +31,43 @@ return new class extends Migration { Schema::create('helpdesk_categories', static function (Blueprint $table): void { $table->id(); + $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); $table->string('name', 120); - $table->string('slug', 140)->unique(); + $table->string('slug', 140); $table->string('color', 30)->default('blue'); $table->text('description')->nullable(); $table->timestamps(); + + $table->unique(['company_id', 'slug']); + }); + + Schema::create('helpdesk_teams', static function (Blueprint $table): void { + $table->id(); + $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); + $table->string('name', 120); + $table->string('slug', 140); + $table->string('color', 30)->default('indigo'); + $table->text('description')->nullable(); + $table->timestamp('archived_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + + $table->unique(['company_id', 'slug']); }); Schema::create('helpdesk_projects', static function (Blueprint $table): void { $table->id(); + $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); + $table->foreignId('team_id')->nullable()->constrained('helpdesk_teams')->nullOnDelete(); $table->string('title', 160); - $table->string('slug', 180)->unique(); + $table->string('slug', 180); $table->string('color', 30)->default('blue'); $table->text('description')->nullable(); $table->timestamp('archived_at')->nullable(); $table->softDeletes(); $table->timestamps(); + + $table->unique(['company_id', 'slug']); }); } @@ -54,6 +75,7 @@ return new class extends Migration { public function down(): void { Schema::dropIfExists('helpdesk_projects'); + Schema::dropIfExists('helpdesk_teams'); Schema::dropIfExists('helpdesk_categories'); Schema::dropIfExists('helpdesk_priorities'); Schema::dropIfExists('helpdesk_statuses'); diff --git a/Modules/Helpdesk/Database/Migrations/2026_04_24_000003_create_helpdesk_tickets_table.php b/Modules/Helpdesk/Database/Migrations/2026_04_24_000003_create_helpdesk_tickets_table.php index 2b5b9698..3594aa29 100644 --- a/Modules/Helpdesk/Database/Migrations/2026_04_24_000003_create_helpdesk_tickets_table.php +++ b/Modules/Helpdesk/Database/Migrations/2026_04_24_000003_create_helpdesk_tickets_table.php @@ -13,12 +13,14 @@ return new class extends Migration { Schema::create('helpdesk_tickets', static function (Blueprint $table): void { $table->id(); $table->string('reference', 32)->unique(); + $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete(); $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); $table->foreignId('agent_id')->nullable()->constrained('users')->nullOnDelete(); $table->foreignId('status_id')->constrained('helpdesk_statuses')->restrictOnDelete(); $table->foreignId('priority_id')->constrained('helpdesk_priorities')->restrictOnDelete(); $table->foreignId('category_id')->nullable()->constrained('helpdesk_categories')->nullOnDelete(); $table->foreignId('project_id')->nullable()->constrained('helpdesk_projects')->nullOnDelete(); + $table->foreignId('team_id')->nullable()->constrained('helpdesk_teams')->nullOnDelete(); $table->string('subject', 200); $table->longText('body'); $table->timestamp('due_at')->nullable(); @@ -28,6 +30,8 @@ return new class extends Migration { $table->softDeletes(); $table->timestamps(); + $table->index(['company_id', 'project_id']); + $table->index(['team_id', 'status_id']); $table->index(['status_id', 'priority_id']); $table->index(['agent_id', 'completed_at']); $table->index(['user_id', 'created_at']); diff --git a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php index 36ced7b7..932d74c3 100644 --- a/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php +++ b/Modules/Helpdesk/Database/Seeders/HelpdeskDatabaseSeeder.php @@ -10,6 +10,7 @@ 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; final class HelpdeskDatabaseSeeder extends Seeder { @@ -38,7 +39,14 @@ final class HelpdeskDatabaseSeeder extends Seeder Category::query()->updateOrCreate(['slug' => Str::slug($name)], ['name' => $name, 'color' => 'blue']); } + $team = Team::query()->updateOrCreate(['slug' => 'support'], [ + 'name' => 'Support', + 'color' => 'indigo', + 'description' => 'Default support team for helpdesk tickets.', + ]); + Project::query()->updateOrCreate(['slug' => 'internal-helpdesk'], [ + 'team_id' => $team->id, 'title' => 'Internal Helpdesk', 'color' => 'blue', 'description' => 'Default project for internal helpdesk tickets.', diff --git a/Modules/Helpdesk/Livewire/SettingsManager.php b/Modules/Helpdesk/Livewire/SettingsManager.php new file mode 100644 index 00000000..5956f78a --- /dev/null +++ b/Modules/Helpdesk/Livewire/SettingsManager.php @@ -0,0 +1,249 @@ + */ + public array $statusForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0, 'is_closed' => false]; + + /** @var array */ + public array $priorityForm = ['id' => null, 'name' => '', 'slug' => '', 'color' => 'zinc', 'sort_order' => 0]; + + /** @var array */ + public array $categoryForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'blue', 'description' => '']; + + /** @var array */ + public array $teamForm = ['id' => null, 'company_id' => null, 'name' => '', 'slug' => '', 'color' => 'indigo', 'description' => '', 'archived_at' => null]; + + /** @var array */ + public array $projectForm = ['id' => null, 'company_id' => null, 'team_id' => null, 'title' => '', 'slug' => '', 'color' => 'blue', 'description' => '', 'archived_at' => null]; + + /** @return void */ + public function saveStatus(): void + { + $data = $this->validate([ + 'statusForm.name' => ['required', 'string', 'max:80'], + 'statusForm.slug' => ['nullable', 'string', 'max:100'], + 'statusForm.color' => ['required', 'string', 'max:30'], + 'statusForm.sort_order' => ['required', 'integer', 'min:0'], + 'statusForm.is_closed' => ['boolean'], + ])['statusForm']; + + $data['slug'] = $this->slug($data['slug'] ?: $data['name']); + Status::query()->updateOrCreate(['id' => $this->statusForm['id']], $data); + $this->resetStatusForm(); + 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([ + 'priorityForm.name' => ['required', 'string', 'max:80'], + 'priorityForm.slug' => ['nullable', 'string', 'max:100'], + 'priorityForm.color' => ['required', 'string', 'max:30'], + 'priorityForm.sort_order' => ['required', 'integer', 'min:0'], + ])['priorityForm']; + + $data['slug'] = $this->slug($data['slug'] ?: $data['name']); + Priority::query()->updateOrCreate(['id' => $this->priorityForm['id']], $data); + $this->resetPriorityForm(); + 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([ + 'categoryForm.company_id' => ['nullable', 'integer'], + 'categoryForm.name' => ['required', 'string', 'max:120'], + 'categoryForm.slug' => ['nullable', 'string', 'max:140'], + 'categoryForm.color' => ['required', 'string', 'max:30'], + 'categoryForm.description' => ['nullable', 'string'], + ])['categoryForm']; + + $data['slug'] = $this->slug($data['slug'] ?: $data['name']); + Category::query()->updateOrCreate(['id' => $this->categoryForm['id']], $data); + $this->resetCategoryForm(); + 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([ + 'teamForm.company_id' => ['nullable', 'integer'], + 'teamForm.name' => ['required', 'string', 'max:120'], + 'teamForm.slug' => ['nullable', 'string', 'max:140'], + 'teamForm.color' => ['required', 'string', 'max:30'], + 'teamForm.description' => ['nullable', 'string'], + ])['teamForm']; + + $data['slug'] = $this->slug($data['slug'] ?: $data['name']); + Team::query()->updateOrCreate(['id' => $this->teamForm['id']], $data); + $this->resetTeamForm(); + 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([ + 'projectForm.company_id' => ['nullable', 'integer'], + 'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'], + 'projectForm.title' => ['required', 'string', 'max:160'], + 'projectForm.slug' => ['nullable', 'string', 'max:180'], + 'projectForm.color' => ['required', 'string', 'max:30'], + 'projectForm.description' => ['nullable', 'string'], + ])['projectForm']; + + $data['slug'] = $this->slug($data['slug'] ?: $data['title']); + Project::query()->updateOrCreate(['id' => $this->projectForm['id']], $data); + $this->resetProjectForm(); + 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 View */ + public function render(): View + { + return view('helpdesk::livewire.settings-manager', [ + 'companies' => $this->companies(), + 'statuses' => Status::query()->withCount('tickets')->orderBy('sort_order')->orderBy('name')->get(), + 'priorities' => Priority::query()->withCount('tickets')->orderBy('sort_order')->orderBy('name')->get(), + 'categories' => Category::query()->withCount('tickets')->orderBy('name')->get(), + 'teams' => Team::query()->withCount(['projects', 'tickets'])->orderBy('name')->get(), + 'projects' => Project::query()->with(['team'])->withCount('tickets')->orderBy('title')->get(), + ])->layout('layouts.app'); + } + + /** @return array */ + private function companies(): array + { + if (! Schema::hasTable('companies')) { + return []; + } + + return DB::table('companies')->select(['id', 'name'])->orderBy('name')->get()->all(); + } + + private function slug(string $value): string + { + return Str::slug($value); + } +} diff --git a/Modules/Helpdesk/Livewire/TicketCreate.php b/Modules/Helpdesk/Livewire/TicketCreate.php index 50c684ef..e0dc3d5f 100644 --- a/Modules/Helpdesk/Livewire/TicketCreate.php +++ b/Modules/Helpdesk/Livewire/TicketCreate.php @@ -13,12 +13,18 @@ use Livewire\WithFileUploads; use Modules\Helpdesk\Models\Category; use Modules\Helpdesk\Models\Priority; use Modules\Helpdesk\Models\Project; +use Modules\Helpdesk\Models\Team; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use Modules\Helpdesk\Services\TicketService; final class TicketCreate extends Component { use WithFileUploads; + #[Validate('nullable|integer')] + public ?int $company_id = null; + #[Validate('required|string|max:200')] public string $subject = ''; @@ -34,6 +40,9 @@ final class TicketCreate extends Component #[Validate('nullable|integer|exists:helpdesk_projects,id')] public ?int $project_id = null; + #[Validate('nullable|integer|exists:helpdesk_teams,id')] + public ?int $team_id = null; + #[Validate('nullable|date')] public ?string $due_at = null; @@ -54,11 +63,13 @@ final class TicketCreate extends Component ]); $ticket = $tickets->create(Auth::user(), [ + 'company_id' => $this->company_id, 'subject' => $this->subject, 'body' => $this->body, 'priority_id' => $this->priority_id, 'category_id' => $this->category_id, 'project_id' => $this->project_id, + 'team_id' => $this->team_id, 'due_at' => $this->due_at, ], $this->attachments); @@ -73,6 +84,8 @@ final class TicketCreate extends Component return view('helpdesk::livewire.ticket-create', [ 'priorities' => Priority::query()->orderBy('sort_order')->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(), 'projects' => Project::query()->whereNull('archived_at')->orderBy('title')->get(), ])->layout('layouts.app'); } diff --git a/Modules/Helpdesk/Livewire/TicketIndex.php b/Modules/Helpdesk/Livewire/TicketIndex.php index 64b1b8ca..8f40f87e 100644 --- a/Modules/Helpdesk/Livewire/TicketIndex.php +++ b/Modules/Helpdesk/Livewire/TicketIndex.php @@ -65,7 +65,7 @@ final class TicketIndex extends Component $user = Auth::user(); return Ticket::query() - ->with(['owner', 'agent', 'status', 'priority', 'category', 'project']) + ->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team']) ->visibleTo($user) ->when($this->scope === 'open', static fn (Builder $query): Builder => $query->whereNull('completed_at')) ->when($this->scope === 'closed', static fn (Builder $query): Builder => $query->whereNotNull('completed_at')) diff --git a/Modules/Helpdesk/Models/Category.php b/Modules/Helpdesk/Models/Category.php index 468536f0..53e0c774 100644 --- a/Modules/Helpdesk/Models/Category.php +++ b/Modules/Helpdesk/Models/Category.php @@ -17,7 +17,10 @@ final class Category extends Model protected $table = 'helpdesk_categories'; /** @var list */ - protected $fillable = ['name', 'slug', 'color', 'description']; + protected $fillable = ['company_id', 'name', 'slug', 'color', 'description']; + + /** @var array */ + protected $casts = ['company_id' => 'integer']; /** @return HasMany */ public function tickets(): HasMany diff --git a/Modules/Helpdesk/Models/Project.php b/Modules/Helpdesk/Models/Project.php index 7473b41c..eea3f35a 100644 --- a/Modules/Helpdesk/Models/Project.php +++ b/Modules/Helpdesk/Models/Project.php @@ -6,6 +6,7 @@ namespace Modules\Helpdesk\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -17,10 +18,16 @@ final class Project extends Model protected $table = 'helpdesk_projects'; /** @var list */ - protected $fillable = ['title', 'slug', 'color', 'description', 'archived_at']; + protected $fillable = ['company_id', 'team_id', 'title', 'slug', 'color', 'description', 'archived_at']; /** @var array */ - protected $casts = ['archived_at' => 'datetime']; + protected $casts = ['company_id' => 'integer', 'team_id' => 'integer', 'archived_at' => 'datetime']; + + /** @return BelongsTo */ + public function team(): BelongsTo + { + return $this->belongsTo(Team::class, 'team_id'); + } /** @return HasMany */ public function tickets(): HasMany diff --git a/Modules/Helpdesk/Models/Team.php b/Modules/Helpdesk/Models/Team.php new file mode 100644 index 00000000..c85c3bc3 --- /dev/null +++ b/Modules/Helpdesk/Models/Team.php @@ -0,0 +1,39 @@ + */ + protected $fillable = ['company_id', 'name', 'slug', 'color', 'description', 'archived_at']; + + /** @var array */ + protected $casts = [ + 'company_id' => 'integer', + 'archived_at' => 'datetime', + ]; + + /** @return HasMany */ + public function projects(): HasMany + { + return $this->hasMany(Project::class, 'team_id'); + } + + /** @return HasMany */ + public function tickets(): HasMany + { + return $this->hasMany(Ticket::class, 'team_id'); + } +} diff --git a/Modules/Helpdesk/Models/Ticket.php b/Modules/Helpdesk/Models/Ticket.php index 78e93b37..e213fcf6 100644 --- a/Modules/Helpdesk/Models/Ticket.php +++ b/Modules/Helpdesk/Models/Ticket.php @@ -27,12 +27,14 @@ final class Ticket extends Model /** @var list */ protected $fillable = [ - 'reference', 'user_id', 'agent_id', 'status_id', 'priority_id', 'category_id', 'project_id', + 'reference', 'company_id', 'user_id', 'agent_id', 'status_id', 'priority_id', 'category_id', 'project_id', 'team_id', 'subject', 'body', 'due_at', 'completed_at', 'last_activity_at', 'archived_at', ]; /** @var array */ protected $casts = [ + 'company_id' => 'integer', + 'team_id' => 'integer', 'due_at' => 'datetime', 'completed_at' => 'datetime', 'last_activity_at' => 'datetime', @@ -75,6 +77,12 @@ final class Ticket extends Model return $this->belongsTo(Project::class, 'project_id'); } + /** @return BelongsTo */ + public function team(): BelongsTo + { + return $this->belongsTo(Team::class, 'team_id'); + } + /** @return HasMany */ public function comments(): HasMany { diff --git a/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php b/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php index 5ba825f8..739f4db3 100644 --- a/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php +++ b/Modules/Helpdesk/Providers/HelpdeskServiceProvider.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\ServiceProvider; use Livewire\Livewire; use Modules\Helpdesk\Livewire\HelpdeskDashboardWidget; +use Modules\Helpdesk\Livewire\SettingsManager; use Modules\Helpdesk\Livewire\TicketCreate; use Modules\Helpdesk\Livewire\TicketIndex; use Modules\Helpdesk\Livewire\TicketShow; @@ -78,6 +79,7 @@ final class HelpdeskServiceProvider extends ServiceProvider Livewire::component('helpdesk.ticket-create', TicketCreate::class); Livewire::component('helpdesk.ticket-show', TicketShow::class); Livewire::component('helpdesk.dashboard-widget', HelpdeskDashboardWidget::class); + Livewire::component('helpdesk.settings-manager', SettingsManager::class); } /** diff --git a/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php b/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php new file mode 100644 index 00000000..a5f153bc --- /dev/null +++ b/Modules/Helpdesk/Resources/views/components/lookup-table.blade.php @@ -0,0 +1,20 @@ +
+ + + + + + @forelse ($items as $item) + + + + + + + + @empty + + @endforelse + +
NameSlugColourTicketsActions
{{ $item->name }}{{ $item->slug }}{{ $item->color }}{{ $item->tickets_count ?? 0 }}EditDelete
No records yet.
+
diff --git a/Modules/Helpdesk/Resources/views/components/project-table.blade.php b/Modules/Helpdesk/Resources/views/components/project-table.blade.php new file mode 100644 index 00000000..21c8485d --- /dev/null +++ b/Modules/Helpdesk/Resources/views/components/project-table.blade.php @@ -0,0 +1 @@ +
@forelse ($items as $item)@empty@endforelse
ProjectTeamSlugColourTicketsActions
{{ $item->title }}{{ $item->team?->name ?? 'No team' }}{{ $item->slug }}{{ $item->color }}{{ $item->tickets_count }}EditDelete
No projects yet.
diff --git a/Modules/Helpdesk/Resources/views/components/settings-section.blade.php b/Modules/Helpdesk/Resources/views/components/settings-section.blade.php new file mode 100644 index 00000000..05892b6e --- /dev/null +++ b/Modules/Helpdesk/Resources/views/components/settings-section.blade.php @@ -0,0 +1,8 @@ +
+
+

{{ $title }}

+

{{ $description }}

+
+
{{ $form }}
+
{{ $slot }}
+
diff --git a/Modules/Helpdesk/Resources/views/components/team-table.blade.php b/Modules/Helpdesk/Resources/views/components/team-table.blade.php new file mode 100644 index 00000000..a923de3a --- /dev/null +++ b/Modules/Helpdesk/Resources/views/components/team-table.blade.php @@ -0,0 +1 @@ +
@forelse ($items as $item)@empty@endforelse
TeamSlugColourProjectsTicketsActions
{{ $item->name }}{{ $item->slug }}{{ $item->color }}{{ $item->projects_count }}{{ $item->tickets_count }}EditDelete
No teams yet.
diff --git a/Modules/Helpdesk/Resources/views/index.blade.php b/Modules/Helpdesk/Resources/views/index.blade.php index cc9adc5a..05498771 100644 --- a/Modules/Helpdesk/Resources/views/index.blade.php +++ b/Modules/Helpdesk/Resources/views/index.blade.php @@ -1,3 +1,22 @@ - +
+
+
+
+
+

Support operations

+

Helpdesk

+

Manage tickets, priorities, statuses, categories, company teams and company projects from one professional workspace.

+
+ +
+ New ticket + Settings +
+
+
+
+ + +
diff --git a/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php new file mode 100644 index 00000000..2f891446 --- /dev/null +++ b/Modules/Helpdesk/Resources/views/livewire/settings-manager.blade.php @@ -0,0 +1,117 @@ +
+
+
+
+
+

Helpdesk administration

+

Support configuration

+

Manage the lookup data used by tickets, including statuses, priorities, categories, company teams and company projects.

+
+ +
+ Helpdesk + Tickets +
+
+
+
+ + @if (session('success')) + {{ session('success') }} + @endif + +
+ @foreach ([ + 'statuses' => ['label' => 'Statuses', 'count' => $statuses->count()], + 'priorities' => ['label' => 'Priorities', 'count' => $priorities->count()], + 'categories' => ['label' => 'Categories', 'count' => $categories->count()], + 'teams' => ['label' => 'Teams', 'count' => $teams->count()], + 'projects' => ['label' => 'Projects', 'count' => $projects->count()], + ] as $key => $item) + + @endforeach +
+ + @if ($section === 'statuses') + + +
+ + + + + +
Save statusReset
+ +
+ +
+ @endif + + @if ($section === 'priorities') + + +
+ + + + +
Save priorityReset
+ +
+ +
+ @endif + + @if ($section === 'categories') + + +
+ @foreach ($companies as $company)@endforeach + + + +
+
Save categoryReset
+ +
+ +
+ @endif + + @if ($section === 'teams') + + +
+ @foreach ($companies as $company)@endforeach + + + +
+
Save teamReset
+ +
+ +
+ @endif + + @if ($section === 'projects') + + +
+ @foreach ($companies as $company)@endforeach + @foreach ($teams as $team)@endforeach + + + +
+
Save projectReset
+ +
+ +
+ @endif +
diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php index a976b028..94c19bac 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-create.blade.php @@ -12,6 +12,21 @@
+ + + + @foreach ($companies as $company) + + @endforeach + + + + + @foreach ($teams as $team) + + @endforeach + + @foreach ($priorities as $priority) diff --git a/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php b/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php index 9a63a86f..56ff8f5c 100644 --- a/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/ticket-index.blade.php @@ -67,7 +67,7 @@
{{ $ticket->subject }}
-
{{ $ticket->category?->name ?? 'No category' }}
+
{{ $ticket->category?->name ?? 'No category' }} @if($ticket->project) · {{ $ticket->project->title }} @endif @if($ticket->team) · {{ $ticket->team->name }} @endif
{{ $ticket->status->name }} {{ $ticket->priority->name }} diff --git a/Modules/Helpdesk/Routes/web.php b/Modules/Helpdesk/Routes/web.php index 04ba61b5..00d4fac0 100644 --- a/Modules/Helpdesk/Routes/web.php +++ b/Modules/Helpdesk/Routes/web.php @@ -4,12 +4,15 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; use Modules\Helpdesk\Http\Controllers\HelpdeskController; +use Modules\Helpdesk\Livewire\SettingsManager; use Modules\Helpdesk\Livewire\TicketCreate; use Modules\Helpdesk\Livewire\TicketIndex; use Modules\Helpdesk\Livewire\TicketShow; Route::get('/', HelpdeskController::class)->name('index'); +Route::get('/settings', SettingsManager::class)->name('settings'); + Route::get('/tickets', TicketIndex::class)->name('tickets.index'); Route::get('/tickets/create', TicketCreate::class)->name('tickets.create'); Route::get('/tickets/{ticket}', TicketShow::class)->name('tickets.show'); diff --git a/Modules/Helpdesk/Services/TicketService.php b/Modules/Helpdesk/Services/TicketService.php index 6ac6413a..1ed5e0c4 100644 --- a/Modules/Helpdesk/Services/TicketService.php +++ b/Modules/Helpdesk/Services/TicketService.php @@ -35,12 +35,14 @@ final class TicketService /** @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, 'priority_id' => $data['priority_id'], 'category_id' => $data['category_id'] ?? null, 'project_id' => $data['project_id'] ?? null, + 'team_id' => $data['team_id'] ?? null, 'subject' => $data['subject'], 'body' => $data['body'], 'due_at' => $data['due_at'] ?? null, @@ -49,7 +51,7 @@ final class TicketService $this->attachments->storeMany($ticket, $files, (int) $user->id); - return $ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'attachments']) ?? $ticket; + return $ticket->fresh(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team', 'attachments']) ?? $ticket; }); } @@ -68,6 +70,7 @@ final class TicketService return DB::transaction(function () use ($ticket, $user, $body, $internal, $files): Comment { /** @var Comment $comment */ $comment = $ticket->comments()->create([ + 'company_id' => $data['company_id'] ?? ($user->company_id ?? null), 'user_id' => $user->id, 'body' => $body, 'is_internal' => $internal, diff --git a/Modules/Helpdesk/View/Components/LookupTable.php b/Modules/Helpdesk/View/Components/LookupTable.php new file mode 100644 index 00000000..21702303 --- /dev/null +++ b/Modules/Helpdesk/View/Components/LookupTable.php @@ -0,0 +1,24 @@ +