This commit is contained in:
Mark Gregory
2026-06-15 18:05:23 +01:00
parent 0a9746fa77
commit a96369c90a
16 changed files with 164 additions and 143 deletions
+10 -63
View File
@@ -16,7 +16,6 @@ use Modules\Helpdesk\Models\Category;
use Modules\Helpdesk\Models\Priority;
use Modules\Helpdesk\Models\Product;
use Modules\Helpdesk\Models\Project;
use Modules\Helpdesk\Models\Status;
use Modules\Helpdesk\Models\Team;
use Modules\Helpdesk\Models\Tag;
use Modules\Helpdesk\Services\TicketService;
@@ -34,9 +33,6 @@ final class TicketCreate extends Component
#[Validate('required|string|min:10')]
public string $body = '';
#[Validate('nullable|integer|exists:helpdesk_statuses,id')]
public ?int $status_id = null;
#[Validate('required|integer|exists:helpdesk_priorities,id')]
public ?int $priority_id = null;
@@ -88,7 +84,6 @@ final class TicketCreate extends Component
'company_id' => $this->company_id,
'subject' => $this->subject,
'body' => $this->body,
'status_id' => $this->status_id,
'priority_id' => $this->priority_id,
'category_id' => $this->category_id,
'project_id' => $this->project_id,
@@ -105,10 +100,7 @@ final class TicketCreate extends Component
public function render(): View
{
$this->applyDefaults();
return view('helpdesk::livewire.ticket-create', [
'statuses' => $this->statusOptions(),
'priorities' => $this->priorityOptions(),
'categories' => $this->categoryOptions(),
'companies' => $this->companyOptions(),
@@ -125,28 +117,21 @@ final class TicketCreate extends Component
private function applyDefaults(): void
{
if (blank($this->company_id) && Schema::hasTable('companies') && Schema::hasColumn('companies', 'is_default_helpdesk_company')) {
$this->company_id = $this->nullableInt(DB::table('companies')
$this->company_id = DB::table('companies')
->where('is_default_helpdesk_company', true)
->orderBy('name')
->value('id'));
}
if (blank($this->status_id) && $this->hasDefaultColumn('helpdesk_statuses')) {
$this->status_id = $this->nullableInt(Status::query()
->where('is_default', true)
->orderBy('sort_order')
->value('id'));
->value('id');
}
if (blank($this->priority_id) && $this->hasDefaultColumn('helpdesk_priorities')) {
$this->priority_id = $this->nullableInt(Priority::query()
$this->priority_id = Priority::query()
->where('is_default', true)
->orderBy('sort_order')
->value('id'));
->value('id');
}
if (blank($this->category_id) && $this->hasDefaultColumn('helpdesk_categories')) {
$this->category_id = $this->nullableInt(Category::query()
$this->category_id = Category::query()
->where('is_default', true)
->when($this->company_id, fn ($query) => $query->where(function ($query): void {
$query->where('company_id', $this->company_id)
@@ -154,11 +139,11 @@ final class TicketCreate extends Component
}))
->orderByRaw('company_id is null')
->orderBy('name')
->value('id'));
->value('id');
}
if (blank($this->team_id) && $this->hasDefaultColumn('helpdesk_teams')) {
$this->team_id = $this->nullableInt(Team::query()
$this->team_id = Team::query()
->where('is_default', true)
->whereNull('archived_at')
->when($this->company_id, fn ($query) => $query->where(function ($query): void {
@@ -167,11 +152,11 @@ final class TicketCreate extends Component
}))
->orderByRaw('company_id is null')
->orderBy('name')
->value('id'));
->value('id');
}
if (blank($this->project_id) && $this->hasDefaultColumn('helpdesk_projects')) {
$this->project_id = $this->nullableInt(Project::query()
$this->project_id = Project::query()
->where('is_default', true)
->whereNull('archived_at')
->when($this->company_id, fn ($query) => $query->where(function ($query): void {
@@ -185,23 +170,11 @@ final class TicketCreate extends Component
->orderByRaw('company_id is null')
->orderByRaw('team_id is null')
->orderBy('title')
->value('id'));
->value('id');
}
}
/**
* Return status options with default records listed first.
*/
private function statusOptions(): mixed
{
return Status::query()
->when($this->hasDefaultColumn('helpdesk_statuses'), fn ($query) => $query->orderByDesc('is_default'))
->orderBy('sort_order')
->get();
}
/**
* Return priority options with default records listed first.
*/
@@ -270,32 +243,6 @@ final class TicketCreate extends Component
->get();
}
/**
* Reload dependent defaults after the company changes.
*/
public function updatedCompanyId(mixed $value): void
{
$this->company_id = $this->nullableInt($value);
$this->category_id = null;
$this->team_id = null;
$this->project_id = null;
$this->applyDefaults();
}
/**
* Convert nullable database / Livewire values to nullable integers.
*/
private function nullableInt(mixed $value): ?int
{
if ($value === null || $value === '') {
return null;
}
return (int) $value;
}
/**
* Check whether a Helpdesk lookup table has the default flag column.
*/
@@ -23,8 +23,8 @@
</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->tickets_count ?? 0 }}</td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" wire:click="{{ $edit }}({{ $item->id }})" variant="primary">Edit</flux:button>
<flux:button size="sm" wire:click="{{ $delete }}({{ $item->id }})" variant="danger">Delete</flux:button>
<flux:button size="sm" wire:click="{{ $edit }}({{ $item- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})" variant="primary">Edit</flux:button>
<flux:button size="sm" wire:click="{{ $delete }}({{ $item- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" variant="danger">Delete</flux:button>
</td>
</tr>
@empty
@@ -25,8 +25,8 @@
</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->tickets_count }}</td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" wire:click="editProject({{ $item->id }})" variant="primary">Edit</flux:button>
<flux:button size="sm" wire:click="deleteProject({{ $item->id }})" variant="danger">Delete</flux:button>
<flux:button size="sm" wire:click="editProject({{ $item- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})" variant="primary">Edit</flux:button>
<flux:button size="sm" wire:click="deleteProject({{ $item- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" variant="danger">Delete</flux:button>
</td>
</tr>
@empty
@@ -26,8 +26,8 @@
<code class="mt-1 block max-w-xs truncate rounded bg-zinc-100 px-2 py-1 text-[11px] text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">{{ route('api.helpdesk.git.webhook', ['repository' => $item->id]) }}</code>
</td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="primary" wire:click="editRepository({{ $item->id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteRepository({{ $item->id }})" wire:confirm="Delete this repository?">Delete</flux:button>
<flux:button size="sm" variant="primary" wire:click="editRepository({{ $item- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteRepository({{ $item- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this repository?">Delete</flux:button>
</td>
</tr>
@empty
@@ -25,8 +25,8 @@
<td class="px-4 py-3 text-zinc-500">{{ $item->projects_count }}</td>
<td class="px-4 py-3 text-zinc-500">{{ $item->tickets_count }}</td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" wire:click="editTeam({{ $item->id }})" variant="primary">Edit</flux:button>
<flux:button size="sm" wire:click="deleteTeam({{ $item->id }})" variant="danger">Delete</flux:button>
<flux:button size="sm" wire:click="editTeam({{ $item- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})" variant="primary">Edit</flux:button>
<flux:button size="sm" wire:click="deleteTeam({{ $item- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" variant="danger">Delete</flux:button>
</td>
</tr>
@empty
@@ -7,10 +7,22 @@
<h1 class="mt-2 text-3xl font-semibold tracking-tight md:text-4xl">Helpdesk dashboard</h1>
<p class="mt-3 max-w-3xl text-sm leading-6 text-blue-100/90">Track open workload, closed tickets, response performance, project activity, priorities, Git commits and workflow communication from one professional support workspace.</p>
<div class="mt-5 flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-500">New ticket</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost" icon="ticket">Active tickets</flux:button>
<flux:button :href="route('helpdesk.kanban')" wire:navigate variant="ghost" icon="squares-2x2">Kanban</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost" icon="cog-6-tooth">Settings</flux:button>
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400/60">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
<span>+ New Ticket</span>
</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/60">
<i class="fa-solid fa-ticket" aria-hidden="true"></i>
<span>Active tickets</span>
</a>
<a href="{{ route('helpdesk.kanban') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-amber-500 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-600 focus:outline-none focus:ring-2 focus:ring-amber-300/60">
<i class="fa-solid fa-table-columns" aria-hidden="true"></i>
<span>Kanban</span>
</a>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-violet-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-violet-400/60">
<i class="fa-solid fa-gear" aria-hidden="true"></i>
<span>Settings</span>
</a>
</div>
</div>
@@ -32,11 +44,11 @@
<div class="rounded-2xl border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.index')" wire:navigate variant="primary">Dashboard</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost">Tickets</flux:button>
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="ghost">Create</flux:button>
<flux:button :href="route('helpdesk.kanban')" wire:navigate variant="ghost">Kanban</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost">Settings</flux:button>
<a href="{{ route('helpdesk.index') }}" wire:navigate class="inline-flex items-center rounded-xl bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-800">Dashboard</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="inline-flex items-center rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700">Tickets</a>
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="inline-flex items-center rounded-xl bg-green-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700">+ New Ticket</a>
<a href="{{ route('helpdesk.kanban') }}" wire:navigate class="inline-flex items-center rounded-xl bg-amber-500 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-600">Kanban</a>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="inline-flex items-center rounded-xl bg-violet-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-violet-700">Settings</a>
</div>
</div>
@@ -46,7 +46,7 @@
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="mb-5 flex items-center justify-between">
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Recent commits</h2>
<flux:button :href="route('helpdesk.settings')" wire:navigate size="sm" variant="ghost">Git setup</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate size="sm" variant="ghost" class="!bg-violet-600 !text-white hover:!bg-violet-700 focus:!ring-violet-400/60">Git setup</flux:button>
</div>
<div class="space-y-2">
@forelse ($recentCommits as $commit)
@@ -67,7 +67,7 @@
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Recent tickets</h2>
<p class="text-sm text-zinc-500 dark:text-zinc-400">Latest support activity.</p>
</div>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate size="sm" variant="ghost">View all</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate size="sm" variant="ghost" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">View all</flux:button>
</div>
<div class="space-y-2">
@forelse ($recentTickets as $ticket)
@@ -9,9 +9,18 @@
</div>
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-500">New ticket</flux:button>
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost" icon="cog-6-tooth">Configure boards</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost" icon="list-bullet">List view</flux:button>
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400/60">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
<span>+ New Ticket</span>
</a>
<a href="{{ route('helpdesk.settings') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-violet-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-violet-400/60">
<i class="fa-solid fa-gear" aria-hidden="true"></i>
<span>Configure boards</span>
</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/60">
<i class="fa-solid fa-list" aria-hidden="true"></i>
<span>List view</span>
</a>
</div>
</div>
</div>
@@ -31,8 +31,14 @@
</div>
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.index')" wire:navigate variant="ghost" icon="arrow-left">Helpdesk</flux:button>
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="primary" icon="ticket">Tickets</flux:button>
<a href="{{ route('helpdesk.index') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-slate-400/60">
<i class="fa-solid fa-arrow-left" aria-hidden="true"></i>
<span>Helpdesk</span>
</a>
<a href="{{ route('helpdesk.tickets.index') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/60">
<i class="fa-solid fa-ticket" aria-hidden="true"></i>
<span>Tickets</span>
</a>
</div>
</div>
</div>
@@ -55,7 +61,46 @@
'products' => ['label' => 'Products', 'count' => $products->count()],
'defaults' => ['label' => 'Defaults', 'count' => 1],
] as $key => $item)
<button type="button" wire:click="$set('section', '{{ $key }}')" class="rounded-2xl border px-4 py-3 text-left shadow-sm transition {{ $section === $key ? 'border-blue-500 bg-blue-50 text-blue-900 dark:border-blue-400 dark:bg-blue-950 dark:text-blue-100' : 'border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800' }}">
@php
$sectionTone = match ($key) {
'statuses' => 'border-sky-400/70 bg-sky-600/15 text-sky-900 hover:bg-sky-600/25 dark:border-sky-500/50 dark:bg-sky-950/50 dark:text-sky-100 dark:hover:bg-sky-900/70',
'priorities' => 'border-red-400/70 bg-red-600/15 text-red-900 hover:bg-red-600/25 dark:border-red-500/50 dark:bg-red-950/50 dark:text-red-100 dark:hover:bg-red-900/70',
'categories' => 'border-indigo-400/70 bg-indigo-600/15 text-indigo-900 hover:bg-indigo-600/25 dark:border-indigo-500/50 dark:bg-indigo-950/50 dark:text-indigo-100 dark:hover:bg-indigo-900/70',
'teams' => 'border-blue-400/70 bg-blue-600/15 text-blue-900 hover:bg-blue-600/25 dark:border-blue-500/50 dark:bg-blue-950/50 dark:text-blue-100 dark:hover:bg-blue-900/70',
'projects' => 'border-cyan-400/70 bg-cyan-600/15 text-cyan-900 hover:bg-cyan-600/25 dark:border-cyan-500/50 dark:bg-cyan-950/50 dark:text-cyan-100 dark:hover:bg-cyan-900/70',
'boards' => 'border-amber-400/70 bg-amber-600/15 text-amber-900 hover:bg-amber-600/25 dark:border-amber-500/50 dark:bg-amber-950/50 dark:text-amber-100 dark:hover:bg-amber-900/70',
'git' => 'border-slate-400/70 bg-slate-600/15 text-slate-900 hover:bg-slate-600/25 dark:border-slate-500/50 dark:bg-slate-950/50 dark:text-slate-100 dark:hover:bg-slate-900/70',
'workflow' => 'border-emerald-400/70 bg-emerald-600/15 text-emerald-900 hover:bg-emerald-600/25 dark:border-emerald-500/50 dark:bg-emerald-950/50 dark:text-emerald-100 dark:hover:bg-emerald-900/70',
'canned' => 'border-teal-400/70 bg-teal-600/15 text-teal-900 hover:bg-teal-600/25 dark:border-teal-500/50 dark:bg-teal-950/50 dark:text-teal-100 dark:hover:bg-teal-900/70',
'sla' => 'border-orange-400/70 bg-orange-600/15 text-orange-900 hover:bg-orange-600/25 dark:border-orange-500/50 dark:bg-orange-950/50 dark:text-orange-100 dark:hover:bg-orange-900/70',
'automation' => 'border-purple-400/70 bg-purple-600/15 text-purple-900 hover:bg-purple-600/25 dark:border-purple-500/50 dark:bg-purple-950/50 dark:text-purple-100 dark:hover:bg-purple-900/70',
'checklists' => 'border-lime-400/70 bg-lime-600/15 text-lime-900 hover:bg-lime-600/25 dark:border-lime-500/50 dark:bg-lime-950/50 dark:text-lime-100 dark:hover:bg-lime-900/70',
'tags' => 'border-pink-400/70 bg-pink-600/15 text-pink-900 hover:bg-pink-600/25 dark:border-pink-500/50 dark:bg-pink-950/50 dark:text-pink-100 dark:hover:bg-pink-900/70',
'products' => 'border-violet-400/70 bg-violet-600/15 text-violet-900 hover:bg-violet-600/25 dark:border-violet-500/50 dark:bg-violet-950/50 dark:text-violet-100 dark:hover:bg-violet-900/70',
'defaults' => 'border-green-400/70 bg-green-600/15 text-green-900 hover:bg-green-600/25 dark:border-green-500/50 dark:bg-green-950/50 dark:text-green-100 dark:hover:bg-green-900/70',
default => 'border-zinc-400/70 bg-zinc-600/15 text-zinc-900 hover:bg-zinc-600/25 dark:border-zinc-500/50 dark:bg-zinc-950/50 dark:text-zinc-100 dark:hover:bg-zinc-900/70',
};
$sectionActiveTone = match ($key) {
'statuses' => 'border-sky-300 bg-sky-600 text-white shadow-sky-950/20',
'priorities' => 'border-red-300 bg-red-600 text-white shadow-red-950/20',
'categories' => 'border-indigo-300 bg-indigo-600 text-white shadow-indigo-950/20',
'teams' => 'border-blue-300 bg-blue-600 text-white shadow-blue-950/20',
'projects' => 'border-cyan-300 bg-cyan-600 text-white shadow-cyan-950/20',
'boards' => 'border-amber-300 bg-amber-500 text-white shadow-amber-950/20',
'git' => 'border-slate-300 bg-slate-700 text-white shadow-slate-950/20',
'workflow' => 'border-emerald-300 bg-emerald-600 text-white shadow-emerald-950/20',
'canned' => 'border-teal-300 bg-teal-600 text-white shadow-teal-950/20',
'sla' => 'border-orange-300 bg-orange-600 text-white shadow-orange-950/20',
'automation' => 'border-purple-300 bg-purple-600 text-white shadow-purple-950/20',
'checklists' => 'border-lime-300 bg-lime-600 text-white shadow-lime-950/20',
'tags' => 'border-pink-300 bg-pink-600 text-white shadow-pink-950/20',
'products' => 'border-violet-300 bg-violet-600 text-white shadow-violet-950/20',
'defaults' => 'border-green-300 bg-green-600 text-white shadow-green-950/20',
default => 'border-blue-300 bg-blue-600 text-white shadow-blue-950/20',
};
@endphp
<button type="button" wire:click="$set('section', '{{ $key }}')" class="rounded-2xl border px-4 py-3 text-left shadow-sm transition {{ $section === $key ? $sectionActiveTone : $sectionTone }}">
<div class="text-xs font-semibold uppercase tracking-wide opacity-70">{{ $item['label'] }}</div>
<div class="mt-1 text-2xl font-semibold">{{ $item['count'] }}</div>
</button>
@@ -77,7 +122,7 @@
</flux:select>
<div class="md:col-span-3 flex gap-2">
<flux:button type="submit" variant="primary">Save default company</flux:button>
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save default company</flux:button>
</div>
</form>
</x-slot:form>
@@ -102,7 +147,7 @@
<input type="checkbox" wire:model="statusForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default status for new tickets
</label>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary">Save status</flux:button><flux:button type="button" wire:click="resetStatusForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save status</flux:button><flux:button type="button" wire:click="resetStatusForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$statuses" edit="editStatus" delete="deleteStatus" />
@@ -121,7 +166,7 @@
<input type="checkbox" wire:model="priorityForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default for new tickets
</label>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary">Save priority</flux:button><flux:button type="button" wire:click="resetPriorityForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save priority</flux:button><flux:button type="button" wire:click="resetPriorityForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$priorities" edit="editPriority" delete="deletePriority" />
@@ -141,7 +186,7 @@
<input type="checkbox" wire:model="categoryForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default category for new tickets
</label>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary">Save category</flux:button><flux:button type="button" wire:click="resetCategoryForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save category</flux:button><flux:button type="button" wire:click="resetCategoryForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<x-helpdesk::lookup-table :items="$categories" edit="editCategory" delete="deleteCategory" />
@@ -161,7 +206,7 @@
<input type="checkbox" wire:model="teamForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default team for new tickets
</label>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary">Save team</flux:button><flux:button type="button" wire:click="resetTeamForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-4 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save team</flux:button><flux:button type="button" wire:click="resetTeamForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<x-helpdesk::team-table :items="$teams" />
@@ -182,7 +227,7 @@
<input type="checkbox" wire:model="projectForm.is_default" class="rounded border-zinc-300 bg-zinc-950 text-blue-600 focus:ring-blue-500">
Default project for new tickets
</label>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary">Save project</flux:button><flux:button type="button" wire:click="resetProjectForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save project</flux:button><flux:button type="button" wire:click="resetProjectForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<x-helpdesk::project-table :items="$projects" />
@@ -216,8 +261,8 @@
</div>
</div>
<div class="md:col-span-6 flex gap-2">
<flux:button type="submit" variant="primary">Save board</flux:button>
<flux:button type="button" wire:click="resetBoardForm" variant="ghost">Reset</flux:button>
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save board</flux:button>
<flux:button type="button" wire:click="resetBoardForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button>
</div>
</form>
</x-slot:form>
@@ -248,8 +293,8 @@
</td>
<td class="px-4 py-3"><flux:badge color="{{ $boardItem->is_default ? 'emerald' : 'zinc' }}">{{ $boardItem->is_default ? 'Default' : 'No' }}</flux:badge></td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="primary" wire:click="editBoard({{ $boardItem->id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteBoard({{ $boardItem->id }})" wire:confirm="Delete this board?">Delete</flux:button>
<flux:button size="sm" variant="primary" wire:click="editBoard({{ $boardItem- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteBoard({{ $boardItem- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this board?">Delete</flux:button>
</td>
</tr>
@empty
@@ -285,8 +330,8 @@
<flux:input wire:model="repositoryForm.remote_url" label="Remote URL" placeholder="git@github.com:company/repo.git" />
</div>
<div class="md:col-span-6 flex gap-2">
<flux:button type="submit" variant="primary">Save repository</flux:button>
<flux:button type="button" wire:click="resetRepositoryForm" variant="ghost">Reset</flux:button>
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save repository</flux:button>
<flux:button type="button" wire:click="resetRepositoryForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button>
</div>
</form>
</x-slot:form>
@@ -325,8 +370,8 @@
<flux:textarea wire:model="workflowForm.text_body" label="Plain text body" rows="4" placeholder="Optional fallback text" />
</div>
<div class="md:col-span-6 flex gap-2">
<flux:button type="submit" variant="primary">Save workflow message</flux:button>
<flux:button type="button" wire:click="resetWorkflowTemplateForm" variant="ghost">Reset</flux:button>
<flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save workflow message</flux:button>
<flux:button type="button" wire:click="resetWorkflowTemplateForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button>
</div>
</form>
</x-slot:form>
@@ -350,8 +395,8 @@
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $template->to_role }}</td>
<td class="px-4 py-3"><flux:badge color="{{ $template->is_active ? 'emerald' : 'zinc' }}">{{ $template->is_active ? 'Active' : 'Inactive' }}</flux:badge></td>
<td class="px-4 py-3 text-right">
<flux:button size="sm" variant="ghost" wire:click="editWorkflowTemplate({{ $template->id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteWorkflowTemplate({{ $template->id }})" wire:confirm="Delete this workflow message?">Delete</flux:button>
<flux:button size="sm" variant="ghost" wire:click="editWorkflowTemplate({{ $template- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button>
<flux:button size="sm" variant="danger" wire:click="deleteWorkflowTemplate({{ $template- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this workflow message?">Delete</flux:button>
</td>
</tr>
@empty
@@ -376,7 +421,7 @@
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="cannedForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-6"><flux:input wire:model="cannedForm.subject" label="Optional subject" /></div>
<div class="md:col-span-6"><flux:textarea wire:model="cannedForm.body" label="Reply body" rows="6" placeholder="Hello @{{ requester.name }},&#10;&#10;Your ticket @{{ ticket.reference }} has been reviewed." /></div>
<div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary">Save canned response</flux:button><flux:button type="button" wire:click="resetCannedResponseForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save canned response</flux:button><flux:button type="button" wire:click="resetCannedResponseForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@@ -384,7 +429,7 @@
<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-3"><div><h3 class="font-semibold text-zinc-900 dark:text-zinc-100">{{ $response->name }}</h3><p class="text-xs text-zinc-500">{{ $response->category?->name ?? 'Any category' }} · {{ $response->team?->name ?? 'Any team' }}</p></div><flux:badge color="{{ $response->is_active ? 'emerald' : 'zinc' }}">{{ $response->is_active ? 'Active' : 'Inactive' }}</flux:badge></div>
<p class="mt-3 line-clamp-3 text-sm text-zinc-600 dark:text-zinc-300">{{ strip_tags($response->body) }}</p>
<div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editCannedResponse({{ $response->id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteCannedResponse({{ $response->id }})" wire:confirm="Delete this canned response?">Delete</flux:button></div>
<div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editCannedResponse({{ $response- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteCannedResponse({{ $response- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this canned response?">Delete</flux:button></div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No canned responses yet.</div>
@@ -406,10 +451,10 @@
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="slaForm.is_default" class="rounded border-zinc-300"> Default</label>
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="slaForm.business_hours_only" class="rounded border-zinc-300"> Business hours</label>
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="slaForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary">Save SLA policy</flux:button><flux:button type="button" wire:click="resetSlaPolicyForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save SLA policy</flux:button><flux:button type="button" wire:click="resetSlaPolicyForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<div class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"><table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700"><thead class="bg-zinc-50 text-left text-xs font-semibold uppercase text-zinc-500 dark:bg-zinc-800"><tr><th class="px-4 py-3">Policy</th><th class="px-4 py-3">Targets</th><th class="px-4 py-3">Applies to</th><th class="px-4 py-3 text-right">Actions</th></tr></thead><tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">@forelse ($slaPolicies as $policy)<tr><td class="px-4 py-3 font-medium">{{ $policy->name }} @if($policy->is_default)<flux:badge color="blue">Default</flux:badge>@endif</td><td class="px-4 py-3">{{ $policy->first_response_minutes }}m response · {{ $policy->resolution_minutes }}m close</td><td class="px-4 py-3">{{ $policy->priority?->name ?? 'Any priority' }} · {{ $policy->category?->name ?? 'Any category' }}</td><td class="px-4 py-3 text-right"><flux:button size="sm" variant="primary" wire:click="editSlaPolicy({{ $policy->id }})">Edit</flux:button> <flux:button size="sm" variant="danger" wire:click="deleteSlaPolicy({{ $policy->id }})" wire:confirm="Delete this SLA policy?">Delete</flux:button></td></tr>@empty<tr><td colspan="4" class="px-4 py-8 text-center text-zinc-500">No SLA policies yet.</td></tr>@endforelse</tbody></table></div>
<div class="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900"><table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-700"><thead class="bg-zinc-50 text-left text-xs font-semibold uppercase text-zinc-500 dark:bg-zinc-800"><tr><th class="px-4 py-3">Policy</th><th class="px-4 py-3">Targets</th><th class="px-4 py-3">Applies to</th><th class="px-4 py-3 text-right">Actions</th></tr></thead><tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">@forelse ($slaPolicies as $policy)<tr><td class="px-4 py-3 font-medium">{{ $policy->name }} @if($policy->is_default)<flux:badge color="blue">Default</flux:badge>@endif</td><td class="px-4 py-3">{{ $policy->first_response_minutes }}m response · {{ $policy->resolution_minutes }}m close</td><td class="px-4 py-3">{{ $policy->priority?->name ?? 'Any priority' }} · {{ $policy->category?->name ?? 'Any category' }}</td><td class="px-4 py-3 text-right"><flux:button size="sm" variant="primary" wire:click="editSlaPolicy({{ $policy- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button> <flux:button size="sm" variant="danger" wire:click="deleteSlaPolicy({{ $policy- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this SLA policy?">Delete</flux:button></td></tr>@empty<tr><td colspan="4" class="px-4 py-8 text-center text-zinc-500">No SLA policies yet.</td></tr>@endforelse</tbody></table></div>
</x-helpdesk::settings-section>
@endif
@@ -429,31 +474,31 @@
<flux:select wire:model="automationForm.set_project_id" label="Set project"><option value="">No change</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select>
<flux:select wire:model="automationForm.set_status_id" label="Set status"><option value="">No change</option>@foreach ($statuses as $status)<option value="{{ $status->id }}">{{ $status->name }}</option>@endforeach</flux:select>
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="automationForm.is_active" class="rounded border-zinc-300"> Active</label>
<div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary">Save automation rule</flux:button><flux:button type="button" wire:click="resetAutomationRuleForm" variant="ghost">Reset</flux:button></div>
<div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save automation rule</flux:button><flux:button type="button" wire:click="resetAutomationRuleForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div>
</form>
</x-slot:form>
<div class="grid gap-3 md:grid-cols-2">@forelse ($automationRules as $rule)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $rule->name }}</h3><p class="mt-1 text-sm text-zinc-500">Sender: {{ $rule->match_sender ?: 'Any' }} · Subject: {{ $rule->match_subject ?: 'Any' }}</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editAutomationRule({{ $rule->id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteAutomationRule({{ $rule->id }})" wire:confirm="Delete this rule?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2">No automation rules yet.</div>@endforelse</div>
<div class="grid gap-3 md:grid-cols-2">@forelse ($automationRules as $rule)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $rule->name }}</h3><p class="mt-1 text-sm text-zinc-500">Sender: {{ $rule->match_sender ?: 'Any' }} · Subject: {{ $rule->match_subject ?: 'Any' }}</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editAutomationRule({{ $rule- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteAutomationRule({{ $rule- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this rule?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2">No automation rules yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'checklists')
<x-helpdesk::settings-section title="Checklist templates" description="Apply repeatable checklist items by category or project and show completion progress on tickets and Kanban cards.">
<x-slot:form><form wire:submit="saveChecklistTemplate" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="checklistTemplateForm.name" label="Name" /><flux:input wire:model="checklistTemplateForm.slug" label="Slug" /><flux:select wire:model="checklistTemplateForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select><flux:select wire:model="checklistTemplateForm.project_id" label="Project"><option value="">Any project</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="checklistTemplateForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-6"><flux:textarea wire:model="checklistTemplateForm.items_text" label="Checklist items, one per line" rows="6" /></div><div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary">Save checklist</flux:button><flux:button type="button" wire:click="resetChecklistTemplateForm" variant="ghost">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($checklistTemplates as $template)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $template->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ count($template->items ?? []) }} items</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editChecklistTemplate({{ $template->id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteChecklistTemplate({{ $template->id }})" wire:confirm="Delete this checklist?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No checklist templates yet.</div>@endforelse</div>
<x-slot:form><form wire:submit="saveChecklistTemplate" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="checklistTemplateForm.name" label="Name" /><flux:input wire:model="checklistTemplateForm.slug" label="Slug" /><flux:select wire:model="checklistTemplateForm.category_id" label="Category"><option value="">Any category</option>@foreach ($categories as $category)<option value="{{ $category->id }}">{{ $category->name }}</option>@endforeach</flux:select><flux:select wire:model="checklistTemplateForm.project_id" label="Project"><option value="">Any project</option>@foreach ($projects as $project)<option value="{{ $project->id }}">{{ $project->title }}</option>@endforeach</flux:select><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="checklistTemplateForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-6"><flux:textarea wire:model="checklistTemplateForm.items_text" label="Checklist items, one per line" rows="6" /></div><div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save checklist</flux:button><flux:button type="button" wire:click="resetChecklistTemplateForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($checklistTemplates as $template)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $template->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ count($template->items ?? []) }} items</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editChecklistTemplate({{ $template- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteChecklistTemplate({{ $template- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this checklist?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No checklist templates yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'tags')
<x-helpdesk::settings-section title="Ticket tags" description="Maintain visible ticket labels for search, triage and Kanban card context.">
<x-slot:form><form wire:submit="saveTag" class="grid gap-4 md:grid-cols-5"><flux:input wire:model="tagForm.name" label="Name" /><flux:input wire:model="tagForm.slug" label="Slug" /><flux:input wire:model="tagForm.color" label="Colour" /><div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary">Save tag</flux:button><flux:button type="button" wire:click="resetTagForm" variant="ghost">Reset</flux:button></div></form></x-slot:form>
<div class="flex flex-wrap gap-3">@forelse ($tags as $tag)<div class="rounded-2xl border border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900"><span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span><div class="mt-3 flex gap-2"><flux:button size="sm" variant="primary" wire:click="editTag({{ $tag->id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteTag({{ $tag->id }})" wire:confirm="Delete this tag?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700">No tags yet.</div>@endforelse</div>
<x-slot:form><form wire:submit="saveTag" class="grid gap-4 md:grid-cols-5"><flux:input wire:model="tagForm.name" label="Name" /><flux:input wire:model="tagForm.slug" label="Slug" /><flux:input wire:model="tagForm.color" label="Colour" /><div class="md:col-span-5 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save tag</flux:button><flux:button type="button" wire:click="resetTagForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="flex flex-wrap gap-3">@forelse ($tags as $tag)<div class="rounded-2xl border border-zinc-200 bg-white px-4 py-3 dark:border-zinc-700 dark:bg-zinc-900"><span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ \Modules\Helpdesk\Support\Color::css($tag->color) }}">{{ $tag->name }}</span><div class="mt-3 flex gap-2"><flux:button size="sm" variant="primary" wire:click="editTag({{ $tag- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteTag({{ $tag- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this tag?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700">No tags yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@if ($section === 'products')
<x-helpdesk::settings-section title="Products and services" description="Link tickets to products, services, releases or builds and report support cost/time by product.">
<x-slot:form><form wire:submit="saveProduct" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="productForm.name" label="Name" /><flux:input wire:model="productForm.slug" label="Slug" /><flux:input wire:model="productForm.type" label="Type" /><flux:input wire:model="productForm.version" label="Version" /><flux:input wire:model="productForm.release_channel" label="Release/build" /><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="productForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-6"><flux:textarea wire:model="productForm.description" label="Description" rows="3" /></div><div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary">Save product/service</flux:button><flux:button type="button" wire:click="resetProductForm" variant="ghost">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($products as $product)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $product->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ ucfirst($product->type) }} · {{ $product->version ?: 'No version' }} · {{ $product->tickets_count }} tickets</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editProduct({{ $product->id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteProduct({{ $product->id }})" wire:confirm="Delete this product/service?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No products or services yet.</div>@endforelse</div>
<x-slot:form><form wire:submit="saveProduct" class="grid gap-4 md:grid-cols-6"><flux:input wire:model="productForm.name" label="Name" /><flux:input wire:model="productForm.slug" label="Slug" /><flux:input wire:model="productForm.type" label="Type" /><flux:input wire:model="productForm.version" label="Version" /><flux:input wire:model="productForm.release_channel" label="Release/build" /><label class="flex items-end gap-2 pb-2 text-sm"><input type="checkbox" wire:model="productForm.is_active" class="rounded border-zinc-300"> Active</label><div class="md:col-span-6"><flux:textarea wire:model="productForm.description" label="Description" rows="3" /></div><div class="md:col-span-6 flex gap-2"><flux:button type="submit" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-400/60">Save product/service</flux:button><flux:button type="button" wire:click="resetProductForm" variant="ghost" class="!bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60">Reset</flux:button></div></form></x-slot:form>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">@forelse ($products as $product)<div class="rounded-2xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900"><h3 class="font-semibold">{{ $product->name }}</h3><p class="mt-1 text-sm text-zinc-500">{{ ucfirst($product->type) }} · {{ $product->version ?: 'No version' }} · {{ $product->tickets_count }} tickets</p><div class="mt-4 flex justify-end gap-2"><flux:button size="sm" variant="primary" wire:click="editProduct({{ $product- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Edit</flux:button><flux:button size="sm" variant="danger" wire:click="deleteProduct({{ $product- class="!bg-red-600 !text-white hover:!bg-red-700 focus:!ring-red-400/60">id }})" wire:confirm="Delete this product/service?">Delete</flux:button></div></div>@empty<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500 dark:border-zinc-700 md:col-span-2 xl:col-span-3">No products or services yet.</div>@endforelse</div>
</x-helpdesk::settings-section>
@endif
@@ -104,7 +104,7 @@
<select id="company_id" wire:model.defer="company_id" data-helpdesk-select2 class="mt-2 block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select company</option>
@foreach ($companies as $company)
<option value="{{ $company->id }}" @selected((int) $company_id === (int) $company->id)>{{ $company->name }}</option>
<option value="{{ $company->id }}">{{ $company->name }}</option>
@endforeach
</select>
@@ -122,7 +122,7 @@
<select id="priority_id" wire:model.defer="priority_id" data-helpdesk-select2 class="mt-2 block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500" data-dusk="ticket-priority">
<option value="">Select priority</option>
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}" @selected((int) $priority_id === (int) $priority->id)>{{ $priority->name }}</option>
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</select>
@@ -139,7 +139,7 @@
<select id="category_id" wire:model.defer="category_id" data-helpdesk-select2 class="mt-2 block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select category</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}" @selected((int) $category_id === (int) $category->id)>{{ $category->name }}</option>
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
@@ -156,7 +156,7 @@
<select id="project_id" wire:model.defer="project_id" data-helpdesk-select2 class="mt-2 block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select project</option>
@foreach ($projects as $project)
<option value="{{ $project->id }}" @selected((int) $project_id === (int) $project->id)>{{ $project->title ?? $project->name }}</option>
<option value="{{ $project->id }}">{{ $project->title ?? $project->name }}</option>
@endforeach
</select>
@@ -173,7 +173,7 @@
<select id="team_id" wire:model.defer="team_id" data-helpdesk-select2 class="mt-2 block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
<option value="">Select team</option>
@foreach ($teams as $team)
<option value="{{ $team->id }}" @selected((int) $team_id === (int) $team->id)>{{ $team->name }}</option>
<option value="{{ $team->id }}">{{ $team->name }}</option>
@endforeach
</select>
@@ -188,7 +188,7 @@
<select id="tag_ids" wire:model.defer="tag_ids" data-helpdesk-select2 multiple class="mt-2 block w-full rounded-xl border-zinc-700 bg-zinc-950 text-zinc-100 shadow-sm focus:border-blue-500 focus:ring-blue-500">
@foreach ($tags as $tag)
<option value="{{ $tag->id }}" @selected(in_array((int) $tag->id, array_map('intval', $tag_ids), true))>{{ $tag->name }}</option>
<option value="{{ $tag->id }}">{{ $tag->name }}</option>
@endforeach
</select>
@@ -8,8 +8,14 @@
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Search, triage and manage support requests across companies, projects, teams, priorities and statuses.</p>
</div>
<div class="flex flex-wrap gap-2">
<flux:button :href="route('helpdesk.index')" wire:navigate variant="ghost" icon="chart-bar">Dashboard</flux:button>
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate icon="plus" variant="primary" class="!bg-green-600 !text-white hover:!bg-green-700 focus:!ring-green-500">New ticket</flux:button>
<a href="{{ route('helpdesk.index') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-slate-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-slate-400/60">
<i class="fa-solid fa-chart-bar" aria-hidden="true"></i>
<span>Dashboard</span>
</a>
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="inline-flex items-center gap-2 rounded-xl bg-green-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400/60">
<i class="fa-solid fa-plus" aria-hidden="true"></i>
<span>+ New Ticket</span>
</a>
</div>
</div>
</div>
@@ -68,7 +68,7 @@
@endforeach
</flux:select>
<div class="flex items-end">
<flux:button type="button" wire:click="applyCannedResponse" variant="ghost" icon="document-duplicate">Insert response</flux:button>
<flux:button type="button" wire:click="applyCannedResponse" variant="ghost" icon="document-duplicate" class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">Insert response</flux:button>
</div>
</div>
@endif
@@ -88,7 +88,7 @@
</div>
<div class="mt-5 flex justify-end">
<flux:button dusk="post-comment" type="submit" variant="primary" icon="chat-bubble-left-right" class="!bg-emerald-600 !text-white hover:!bg-emerald-700" wire:loading.attr="disabled">
<flux:button dusk="post-comment" type="submit" variant="primary" icon="chat-bubble-left-right" class="!bg-emerald-600 !text-white hover:!bg-emerald-700 !bg-emerald-600 !text-white hover:!bg-emerald-700 focus:!ring-emerald-400/60" wire:loading.attr="disabled">
<span wire:loading.remove>Post reply</span>
<span wire:loading>Posting...</span>
</flux:button>
@@ -185,14 +185,14 @@
<option value="{{ $status->id }}">{{ $status->name }}</option>
@endforeach
</flux:select>
<flux:button dusk="update-status" wire:click="updateStatus" class="mt-4 w-full" variant="primary" icon="check">Update status</flux:button>
<flux:button dusk="update-status" wire:click="updateStatus" class="mt-4 w-full !bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60" variant="primary" icon="check">Update status</flux:button>
<div class="mt-5">
<flux:select wire:model="priority_id" label="Priority">
@foreach ($priorities as $priority)
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
@endforeach
</flux:select>
<flux:button dusk="update-priority" wire:click="updatePriority" class="mt-4 w-full" variant="ghost" icon="flag">Update priority</flux:button>
<flux:button dusk="update-priority" wire:click="updatePriority" class="mt-4 w-full !bg-amber-500 !text-white hover:!bg-amber-600 focus:!ring-amber-300/60" variant="ghost" icon="flag">Update priority</flux:button>
</div>
</div>
@endcan
@@ -39,7 +39,7 @@
@endif
<div class="mt-3 flex justify-end">
<flux:button size="sm" variant="ghost" wire:click="startReply({{ $comment->id }})">Reply</flux:button>
<flux:button size="sm" variant="ghost" wire:click="startReply({{ $comment- class="!bg-blue-600 !text-white hover:!bg-blue-700 focus:!ring-blue-400/60">id }})">Reply</flux:button>
</div>
@if (($reply_parent_id ?? null) === $comment->id)
@@ -47,8 +47,8 @@
<x-helpdesk::html-editor wire:key="reply-editor-{{ $comment->id }}" wire:model="replyBody" label="Reply" rows="4" placeholder="Write a reply..." />
<div class="mt-4 flex justify-end gap-2">
<flux:button type="button" wire:click="cancelReply" variant="ghost">Cancel</flux:button>
<flux:button type="submit" variant="primary" class="!bg-emerald-600 !text-white hover:!bg-emerald-700">Post reply</flux:button>
<flux:button type="button" wire:click="cancelReply" variant="ghost" class="!bg-slate-600 !text-white hover:!bg-slate-700 focus:!ring-slate-400/60">Cancel</flux:button>
<flux:button type="submit" variant="primary" class="!bg-emerald-600 !text-white hover:!bg-emerald-700 !bg-emerald-600 !text-white hover:!bg-emerald-700 focus:!ring-emerald-400/60">Post reply</flux:button>
</div>
</form>
@endif
+2 -6
View File
@@ -34,14 +34,10 @@ final class TicketService
{
return DB::transaction(function () use ($user, $data, $files): Ticket {
$defaultStatusSlug = (string) config('helpdesk.default_status_slug', 'new');
$status = ! empty($data['status_id'])
? Status::query()->whereKey((int) $data['status_id'])->first()
$status = Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')
? Status::query()->where('is_default', true)->orderBy('sort_order')->first()
: null;
if (! $status && Schema::hasTable('helpdesk_statuses') && Schema::hasColumn('helpdesk_statuses', 'is_default')) {
$status = Status::query()->where('is_default', true)->orderBy('sort_order')->first();
}
if (! $status) {
$status = Status::query()->where('slug', $defaultStatusSlug)->first();
}
+11 -5
View File
@@ -121,10 +121,16 @@ versions:
- Confirmed ticket tags can be assigned on create and edit and are synced to the ticket-tag pivot.
- Kept module.json version in sync with changelog.yaml.
"1.2.7":
"1.2.8":
changed:
- Changed Helpdesk New ticket actions to use green action styling.
- Hardened ticket creation defaults so configured company, status, priority, category, team and project defaults are applied before rendering and when saving.
- Added explicit selected states for ticket create dropdowns so Select2 displays loaded defaults correctly.
- Allowed TicketService to respect an explicitly supplied default status id while preserving fallback status behaviour.
- Replaced white Flux action buttons on the Helpdesk dashboard, ticket list, Kanban board and settings header with explicit high-contrast coloured action buttons.
- Standardised + New Ticket actions as green buttons across Helpdesk entry points.
- Kept module.json version in sync with changelog.yaml.
"1.2.9":
changed:
- Applied explicit Helpdesk action colour conventions across dashboard, tickets, Kanban and settings pages.
- Coloured settings section navigation tiles by domain so Statuses, Priorities, Categories, Teams, Projects, Boards, Workflow, SLA, Tags, Products and Defaults are visually distinct.
- Coloured save, reset, edit, delete, settings, ticket, Kanban, reply and update action buttons consistently.
- Kept module.json version in sync with changelog.yaml.
+1 -1
View File
@@ -22,5 +22,5 @@
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
],
"files": [],
"version": "1.2.7"
"version": "1.2.9"
}