new helpdesk module
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
@props([
|
||||
'label' => 'Details',
|
||||
'rows' => 10,
|
||||
'placeholder' => 'Write your message...',
|
||||
])
|
||||
|
||||
@php($model = $attributes->wire('model')->value())
|
||||
|
||||
<div
|
||||
{{ $attributes->except(['wire:model'])->merge(['class' => 'space-y-2']) }}
|
||||
x-data="{
|
||||
value: @entangle($model),
|
||||
command(action, value = null) {
|
||||
document.execCommand(action, false, value);
|
||||
this.sync();
|
||||
},
|
||||
sync() {
|
||||
this.value = this.$refs.editor.innerHTML;
|
||||
},
|
||||
init() {
|
||||
this.$refs.editor.innerHTML = this.value || '';
|
||||
|
||||
this.$watch('value', (nextValue) => {
|
||||
if (document.activeElement !== this.$refs.editor && this.$refs.editor.innerHTML !== (nextValue || '')) {
|
||||
this.$refs.editor.innerHTML = nextValue || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}"
|
||||
>
|
||||
<label class="block text-sm font-medium text-zinc-700 dark:text-zinc-200">{{ $label }}</label>
|
||||
|
||||
<input type="hidden" x-model="value" />
|
||||
|
||||
<div class="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 wire:ignore>
|
||||
<div
|
||||
x-ref="editor"
|
||||
contenteditable="true"
|
||||
x-on:input.debounce.500ms="sync"
|
||||
x-on:blur="sync"
|
||||
x-on:keydown.enter.stop
|
||||
class="prose prose-sm min-h-[{{ (int) $rows * 28 }}px] 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-placeholder="{{ $placeholder }}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@error($model)
|
||||
<div class="text-sm text-red-600">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
@props(['items'])
|
||||
|
||||
<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 tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Repository</th>
|
||||
<th class="px-4 py-3">Project</th>
|
||||
<th class="px-4 py-3">Provider</th>
|
||||
<th class="px-4 py-3">Branch</th>
|
||||
<th class="px-4 py-3">Commits</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 ($items as $item)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $item->name }}</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $item->project?->title ?? 'Global' }}</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $item->provider }}</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $item->default_branch }}</td>
|
||||
<td class="px-4 py-3"><flux:badge color="zinc">{{ $item->commits_count }}</flux:badge></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<flux:button size="sm" variant="ghost" 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>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-zinc-500">No repositories yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,22 +1,46 @@
|
||||
<x-layouts.app :title="__('Helpdesk')">
|
||||
<x-app-layout :title="__('Helpdesk')">
|
||||
<div class="space-y-6">
|
||||
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-8 text-white">
|
||||
<div class="grid gap-6 lg:grid-cols-[1fr_320px] lg:items-center">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Support operations</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Helpdesk</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage tickets, priorities, statuses, categories, company teams and company projects from one professional workspace.</p>
|
||||
<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">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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus">New ticket</flux:button>
|
||||
<flux:button :href="route('helpdesk.settings')" wire:navigate variant="ghost" icon="cog-6-tooth">Settings</flux:button>
|
||||
<div class="rounded-2xl border border-white/10 bg-white/10 p-5 shadow-2xl backdrop-blur">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
@foreach (range(1, 9) as $cell)
|
||||
<div class="h-14 rounded-xl bg-white/15"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl bg-white/10 p-4">
|
||||
<div class="h-2 rounded-full bg-blue-300/80"></div>
|
||||
<div class="mt-3 h-2 w-3/4 rounded-full bg-emerald-300/80"></div>
|
||||
<div class="mt-3 h-2 w-1/2 rounded-full bg-rose-300/80"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<livewire:helpdesk.ticket-index />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<livewire:helpdesk.dashboard-widget />
|
||||
</div>
|
||||
</x-layouts.app>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -1,38 +1,86 @@
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900" dusk="helpdesk-dashboard-widget">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Helpdesk</h2>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">Your current support workload.</p>
|
||||
<div class="space-y-6" dusk="helpdesk-dashboard-widget">
|
||||
<div class="grid gap-4 md:grid-cols-4">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm font-medium text-zinc-500">Total tickets</div>
|
||||
<div class="mt-3 text-4xl font-semibold text-zinc-950 dark:text-white">{{ $totalCount }}</div>
|
||||
<div class="mt-3 h-2 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-2 rounded-full bg-blue-500" style="width: 100%"></div></div>
|
||||
</div>
|
||||
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate size="sm" variant="ghost">View all</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-xl bg-blue-50 p-4 dark:bg-blue-950/40">
|
||||
<div class="text-2xl font-semibold text-blue-700 dark:text-blue-200">{{ $openCount }}</div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-blue-700/70 dark:text-blue-200/70">Open</div>
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm font-medium text-zinc-500">Open tickets</div>
|
||||
<div class="mt-3 text-4xl font-semibold text-blue-700 dark:text-blue-300">{{ $openCount }}</div>
|
||||
<div class="mt-3 h-2 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-2 rounded-full bg-blue-500" style="width: {{ $openPercent }}%"></div></div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-red-50 p-4 dark:bg-red-950/40">
|
||||
<div class="text-2xl font-semibold text-red-700 dark:text-red-200">{{ $criticalCount }}</div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-red-700/70 dark:text-red-200/70">Critical</div>
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm font-medium text-zinc-500">Closed tickets</div>
|
||||
<div class="mt-3 text-4xl font-semibold text-emerald-700 dark:text-emerald-300">{{ $closedCount }}</div>
|
||||
<div class="mt-3 h-2 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-2 rounded-full bg-emerald-500" style="width: {{ $closedPercent }}%"></div></div>
|
||||
</div>
|
||||
<div class="rounded-xl bg-green-50 p-4 dark:bg-green-950/40">
|
||||
<div class="text-2xl font-semibold text-green-700 dark:text-green-200">{{ $closedCount }}</div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-green-700/70 dark:text-green-200/70">Closed</div>
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="text-sm font-medium text-zinc-500">High priority</div>
|
||||
<div class="mt-3 text-4xl font-semibold text-red-700 dark:text-red-300">{{ $criticalCount }}</div>
|
||||
<div class="mt-3 text-xs text-zinc-500">Open urgent/high/critical workload.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 space-y-2">
|
||||
@forelse ($recentTickets as $ticket)
|
||||
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="block rounded-xl border border-zinc-100 p-3 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</span>
|
||||
<flux:badge color="{{ $ticket->priority->color }}" size="sm">{{ $ticket->priority->name }}</flux:badge>
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 lg:col-span-2">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Performance</h2>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">Closure rate and remaining open workload.</p>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-500">{{ $ticket->reference }} · {{ $ticket->status->name }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No helpdesk tickets yet.</div>
|
||||
@endforelse
|
||||
<flux:badge color="emerald">{{ $closedPercent }}% closed</flux:badge>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<div class="mb-2 flex justify-between text-sm"><span>Closed</span><span>{{ $closedPercent }}%</span></div>
|
||||
<div class="h-4 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-4 rounded-full bg-emerald-500" style="width: {{ $closedPercent }}%"></div></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2 flex justify-between text-sm"><span>Open</span><span>{{ $openPercent }}%</span></div>
|
||||
<div class="h-4 rounded-full bg-zinc-100 dark:bg-zinc-800"><div class="h-4 rounded-full bg-blue-500" style="width: {{ $openPercent }}%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@forelse ($recentCommits as $commit)
|
||||
<div class="rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
|
||||
<div class="font-mono text-xs text-zinc-500">{{ $commit->short_hash }} · {{ $commit->repository?->name }}</div>
|
||||
<div class="mt-1 truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $commit->message }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No commits tracked yet.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@forelse ($recentTickets as $ticket)
|
||||
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="block rounded-xl border border-zinc-100 p-3 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</span>
|
||||
<flux:badge color="{{ $ticket->priority->color }}" size="sm">{{ $ticket->priority->name }}</flux:badge>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-500">{{ $ticket->reference }} · {{ $ticket->status->name }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No helpdesk tickets yet.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<div class="mx-auto max-w-7xl space-y-6" dusk="helpdesk-kanban-board">
|
||||
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Helpdesk workflow</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Kanban board</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Move tickets through your configured statuses, keep priorities visible and trigger workflow notifications when work changes state.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate variant="primary" icon="plus">New ticket</flux:button>
|
||||
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost" icon="list-bullet">List view</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-4 overflow-x-auto lg:grid-cols-{{ max(1, min(5, $statuses->count())) }}">
|
||||
@foreach ($statuses as $status)
|
||||
@php($tickets = $ticketsByStatus[$status->id] ?? collect())
|
||||
|
||||
<section class="min-w-[280px] rounded-2xl border border-zinc-200 bg-zinc-50 shadow-sm dark:border-zinc-700 dark:bg-zinc-950" dusk="kanban-column-{{ $status->slug }}">
|
||||
<header class="flex items-center justify-between border-b border-zinc-200 bg-white px-4 py-3 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{{ $status->name }}</h2>
|
||||
<p class="text-xs text-zinc-500">{{ $tickets->count() }} tickets</p>
|
||||
</div>
|
||||
<span class="inline-flex h-3 w-3 rounded-full" style="background-color: {{ $status->color }}"></span>
|
||||
</header>
|
||||
|
||||
<div class="space-y-3 p-3">
|
||||
@forelse ($tickets as $ticket)
|
||||
<article class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-800 dark:bg-zinc-900" dusk="kanban-ticket-{{ $ticket->id }}">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate class="text-sm font-semibold text-blue-700 hover:underline dark:text-blue-300">
|
||||
{{ $ticket->reference }}
|
||||
</a>
|
||||
<h3 class="mt-1 line-clamp-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</h3>
|
||||
</div>
|
||||
@if ($ticket->priority)
|
||||
<span class="rounded-full px-2 py-1 text-xs font-semibold text-white" style="background-color: {{ $ticket->priority->color }}">
|
||||
{{ $ticket->priority->name }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<div>Owner: {{ $ticket->owner?->name ?? 'Unknown' }}</div>
|
||||
@if ($ticket->project)
|
||||
<div>Project: {{ $ticket->project->title }}</div>
|
||||
@endif
|
||||
@if ($ticket->team)
|
||||
<div>Team: {{ $ticket->team->name }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
@foreach ($statuses as $targetStatus)
|
||||
@continue($targetStatus->id === $status->id)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="moveTicket({{ $ticket->id }}, {{ $targetStatus->id }})"
|
||||
class="rounded-lg border border-zinc-200 px-2 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
{{ $targetStatus->name }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-300 bg-white p-6 text-center text-sm text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
No tickets in {{ $status->name }}.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Helpdesk administration</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Support configuration</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage the lookup data used by tickets, including statuses, priorities, categories, company teams and company projects.</p>
|
||||
<p class="mt-2 max-w-3xl text-sm text-blue-100/90">Manage the lookup data used by tickets, including statuses, priorities, categories, company teams, company projects, Git repositories and workflow messages.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -20,13 +20,15 @@
|
||||
<flux:callout variant="success" icon="check-circle">{{ session('success') }}</flux:callout>
|
||||
@endif
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-5">
|
||||
<div class="grid gap-3 md:grid-cols-7">
|
||||
@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()],
|
||||
'git' => ['label' => 'Git', 'count' => $repositories->count()],
|
||||
'workflow' => ['label' => 'Workflow', 'count' => $workflowTemplates->count()],
|
||||
] 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' }}">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide opacity-70">{{ $item['label'] }}</div>
|
||||
@@ -114,4 +116,107 @@
|
||||
<x-helpdesk::project-table :items="$projects" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
@if ($section === 'git')
|
||||
<x-helpdesk::settings-section title="Git repositories" description="Configure repositories that can be linked to projects and tickets. Commits can then be stored against tickets for traceability.">
|
||||
<x-slot:form>
|
||||
<form wire:submit="saveRepository" class="grid gap-4 md:grid-cols-6">
|
||||
<flux:select wire:model="repositoryForm.company_id" label="Company">
|
||||
<option value="">Global</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:select wire:model="repositoryForm.project_id" label="Project">
|
||||
<option value="">No project</option>
|
||||
@foreach ($projects as $project)
|
||||
<option value="{{ $project->id }}">{{ $project->title }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:input wire:model="repositoryForm.name" label="Name" />
|
||||
<flux:input wire:model="repositoryForm.provider" label="Provider" placeholder="git, github, gitlab" />
|
||||
<flux:input wire:model="repositoryForm.default_branch" label="Default branch" />
|
||||
<flux:input wire:model="repositoryForm.webhook_secret" label="Webhook secret" />
|
||||
<div class="md:col-span-6">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</x-slot:form>
|
||||
<x-helpdesk::repository-table :items="$repositories" />
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($section === 'workflow')
|
||||
<x-helpdesk::settings-section title="Workflow messages" description="Define the professional email messages sent when tickets are raised, updated, replied to and closed.">
|
||||
<x-slot:form>
|
||||
<form wire:submit="saveWorkflowTemplate" class="grid gap-4 md:grid-cols-6">
|
||||
<flux:input wire:model="workflowForm.name" label="Name" />
|
||||
<flux:select wire:model="workflowForm.event" label="Event">
|
||||
<option value="ticket.created">Ticket raised</option>
|
||||
<option value="ticket.updated">Ticket updated</option>
|
||||
<option value="ticket.commented">Response added</option>
|
||||
<option value="ticket.reply_created">Reply added</option>
|
||||
<option value="ticket.closed">Ticket closed</option>
|
||||
</flux:select>
|
||||
<flux:select wire:model="workflowForm.to_role" label="Send to">
|
||||
<option value="requester">Requester</option>
|
||||
<option value="agent">Assigned agent</option>
|
||||
<option value="actor">User making change</option>
|
||||
</flux:select>
|
||||
<flux:input wire:model="workflowForm.cc" label="CC" placeholder="Optional comma-separated emails" />
|
||||
<flux:input wire:model="workflowForm.bcc" label="BCC" placeholder="Optional comma-separated emails" />
|
||||
<label class="flex items-end gap-2 pb-2 text-sm text-zinc-700 dark:text-zinc-200"><input type="checkbox" wire:model="workflowForm.is_active" class="rounded border-zinc-300"> Active</label>
|
||||
<div class="md:col-span-6">
|
||||
<flux:input wire:model="workflowForm.subject" label="Subject" placeholder="Ticket @{{ ticket.reference }} has been raised" />
|
||||
</div>
|
||||
<div class="md:col-span-6">
|
||||
<flux:textarea wire:model="workflowForm.html_body" label="HTML email body" rows="8" placeholder="Use tokens like @{{ ticket.reference }}, @{{ ticket.subject }}, @{{ ticket.status }}, @{{ requester.name }}, @{{ ticket.url }}" />
|
||||
</div>
|
||||
<div class="md:col-span-6">
|
||||
<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>
|
||||
</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 tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-300">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Name</th>
|
||||
<th class="px-4 py-3">Event</th>
|
||||
<th class="px-4 py-3">Recipient</th>
|
||||
<th class="px-4 py-3">Active</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 ($workflowTemplates as $template)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-zinc-900 dark:text-zinc-100">{{ $template->name }}</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">{{ $template->event }}</td>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-zinc-500">No workflow messages yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-helpdesk::settings-section>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,70 +1,176 @@
|
||||
<div class="mx-auto max-w-5xl space-y-6" dusk="helpdesk-ticket-create">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">Create ticket</h1>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">Raise a support request with optional attachments.</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('helpdesk::partials.page-header', [
|
||||
'title' => 'Create Ticket',
|
||||
'subtitle' => 'Raise a new support request with priority, project, category and full details.',
|
||||
])
|
||||
|
||||
<form wire:submit="save" class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<div class="space-y-6 md:col-span-2">
|
||||
<flux:input wire:model="subject" label="Subject" placeholder="Short summary" required />
|
||||
<flux:textarea wire:model="body" label="Details" rows="10" placeholder="What happened? What did you expect? Include steps to reproduce where useful." required />
|
||||
<form wire:submit.prevent="save" class="space-y-6">
|
||||
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm">
|
||||
<div class="border-b border-slate-200 px-6 py-4">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Ticket details</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 p-6 lg:grid-cols-2">
|
||||
<div class="lg:col-span-2">
|
||||
<label class="block text-sm font-semibold text-slate-700" for="title">
|
||||
Title
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
wire:model.defer="title"
|
||||
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
|
||||
@error('title')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700" for="priority_id">
|
||||
Priority
|
||||
</label>
|
||||
|
||||
<flux:select wire:model="company_id" label="Company">
|
||||
<option value="">Default company</option>
|
||||
@foreach ($companies as $company)
|
||||
<option value="{{ $company->id }}">{{ $company->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model="team_id" label="Team">
|
||||
<option value="">No team</option>
|
||||
@foreach ($teams as $team)
|
||||
<option value="{{ $team->id }}">{{ $team->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model="priority_id" label="Priority" required>
|
||||
<select
|
||||
id="priority_id"
|
||||
wire:model.defer="priority_id"
|
||||
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select priority</option>
|
||||
|
||||
@foreach ($priorities as $priority)
|
||||
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</select>
|
||||
|
||||
<flux:select wire:model="category_id" label="Category">
|
||||
@error('priority_id')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700" for="category_id">
|
||||
Category
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="category_id"
|
||||
wire:model.defer="category_id"
|
||||
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select category</option>
|
||||
|
||||
@foreach ($categories as $category)
|
||||
<option value="{{ $category->id }}">{{ $category->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</select>
|
||||
|
||||
@error('category_id')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700" for="project_id">
|
||||
Project
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="project_id"
|
||||
wire:model.defer="project_id"
|
||||
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select project</option>
|
||||
|
||||
<flux:select wire:model="project_id" label="Project">
|
||||
<option value="">No project</option>
|
||||
@foreach ($projects as $project)
|
||||
<option value="{{ $project->id }}">{{ $project->title }}</option>
|
||||
<option value="{{ $project->id }}">{{ $project->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</select>
|
||||
|
||||
<flux:input wire:model="due_at" type="date" label="Due date" />
|
||||
@error('project_id')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-200">Attachments</label>
|
||||
<input wire:model="attachments" type="file" multiple class="block w-full rounded-lg border border-zinc-300 bg-white p-2 text-sm dark:border-zinc-700 dark:bg-zinc-950" />
|
||||
<div class="mt-2 text-xs text-zinc-500">Allowed: {{ implode(', ', config('helpdesk.allowed_mimes')) }}. Max {{ config('helpdesk.max_upload_kb') }}KB each.</div>
|
||||
@error('attachments.*') <div class="mt-2 text-sm text-red-600">{{ $message }}</div> @enderror
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-slate-700" for="team_id">
|
||||
Team
|
||||
</label>
|
||||
|
||||
<select
|
||||
id="team_id"
|
||||
wire:model.defer="team_id"
|
||||
class="mt-2 block w-full rounded-xl border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select team</option>
|
||||
|
||||
@foreach ($teams as $team)
|
||||
<option value="{{ $team->id }}">{{ $team->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
@error('team_id')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<label class="block text-sm font-semibold text-slate-700" for="details_editor">
|
||||
Details
|
||||
</label>
|
||||
|
||||
<div wire:ignore class="mt-2">
|
||||
<textarea id="details_editor">{{ $details }}</textarea>
|
||||
</div>
|
||||
|
||||
@error('details')
|
||||
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-end gap-3 border-t border-zinc-200 pt-6 dark:border-zinc-700">
|
||||
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate variant="ghost">Cancel</flux:button>
|
||||
<flux:button type="submit" variant="primary" icon="paper-airplane" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove>Create ticket</span>
|
||||
<span wire:loading>Creating...</span>
|
||||
</flux:button>
|
||||
<div class="flex items-center justify-end gap-3 border-t border-slate-200 px-6 py-4">
|
||||
<a
|
||||
href="{{ route('helpdesk.tickets.index') }}"
|
||||
class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<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"
|
||||
>
|
||||
Create Ticket
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('livewire:init', function () {
|
||||
const textarea = document.getElementById('details_editor');
|
||||
|
||||
if (!textarea || textarea.dataset.initialised === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.dataset.initialised = '1';
|
||||
|
||||
const editor = new EasyMDE({
|
||||
element: textarea,
|
||||
spellChecker: false,
|
||||
status: false,
|
||||
forceSync: true
|
||||
});
|
||||
|
||||
editor.codemirror.on('change', function () {
|
||||
@this.set('details', editor.value(), false);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<div class="space-y-6" dusk="helpdesk-ticket-index">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">Helpdesk</h1>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">Search, triage and manage support tickets.</p>
|
||||
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-blue-200">Ticket management</p>
|
||||
<h1 class="mt-2 text-3xl font-semibold tracking-tight">Active tickets</h1>
|
||||
<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">New ticket</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:button :href="route('helpdesk.tickets.create')" wire:navigate icon="plus" variant="primary">
|
||||
New ticket
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if (session('success'))
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<div class="space-y-6" dusk="helpdesk-ticket-show">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-xs text-zinc-500">{{ $ticket->reference }}</span>
|
||||
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge>
|
||||
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge>
|
||||
<div class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="bg-gradient-to-br from-slate-950 via-slate-900 to-blue-950 px-6 py-7 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-xs text-blue-100">{{ $ticket->reference }}</span>
|
||||
<flux:badge color="{{ $ticket->status->color }}">{{ $ticket->status->name }}</flux:badge>
|
||||
<flux:badge color="{{ $ticket->priority->color }}">{{ $ticket->priority->name }}</flux:badge>
|
||||
</div>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">{{ $ticket->subject }}</h1>
|
||||
<p class="mt-2 text-sm text-blue-100/90">Raised by {{ $ticket->owner?->name ?? 'Unknown user' }} · Assigned to {{ $ticket->agent?->name ?? 'Unassigned' }}</p>
|
||||
</div>
|
||||
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate icon="arrow-left" variant="ghost">Back</flux:button>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">{{ $ticket->subject }}</h1>
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Raised by {{ $ticket->owner?->name ?? 'Unknown user' }} · Assigned to {{ $ticket->agent?->name ?? 'Unassigned' }}
|
||||
</p>
|
||||
</div>
|
||||
<flux:button :href="route('helpdesk.tickets.index')" wire:navigate icon="arrow-left" variant="ghost">Back</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="space-y-6 lg:col-span-2">
|
||||
<article class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<div class="prose max-w-none dark:prose-invert">
|
||||
{!! nl2br(e($ticket->body)) !!}
|
||||
{!! $ticket->body !!}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -39,37 +41,16 @@
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Conversation</h2>
|
||||
|
||||
@forelse ($ticket->comments as $comment)
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900 {{ $comment->is_internal ? 'ring-2 ring-amber-300' : '' }}">
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $comment->user?->name ?? 'Unknown user' }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ $comment->created_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
@if ($comment->is_internal)
|
||||
<flux:badge color="amber">Internal note</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
<div class="prose max-w-none text-sm dark:prose-invert">{!! nl2br(e($comment->body)) !!}</div>
|
||||
|
||||
@if ($comment->attachments->isNotEmpty())
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@foreach ($comment->attachments as $attachment)
|
||||
<a href="{{ $attachment->url() }}" target="_blank" class="rounded-full bg-zinc-100 px-3 py-1 text-xs text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200">
|
||||
{{ $attachment->original_name }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-zinc-500 dark:border-zinc-700">No comments yet.</div>
|
||||
@endforelse
|
||||
@if ($ticket->comments->isNotEmpty())
|
||||
@include('helpdesk::partials.comment-thread', ['comments' => $ticket->comments, 'level' => 0])
|
||||
@else
|
||||
<div class="rounded-2xl border border-dashed border-zinc-300 p-8 text-center text-zinc-500 dark:border-zinc-700">No responses yet.</div>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<form wire:submit="addComment" class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Add reply</h2>
|
||||
<flux:textarea wire:model="commentBody" label="Comment" rows="5" required />
|
||||
<x-helpdesk::html-editor dusk="ticket-comment-editor" wire:model="commentBody" label="Comment" rows="5" placeholder="Write a professional response..." />
|
||||
|
||||
@can('update', $ticket)
|
||||
<div class="mt-4">
|
||||
@@ -84,7 +65,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="chat-bubble-left-right" wire:loading.attr="disabled">
|
||||
<flux:button dusk="post-comment" type="submit" variant="primary" icon="chat-bubble-left-right" wire:loading.attr="disabled">
|
||||
<span wire:loading.remove>Post reply</span>
|
||||
<span wire:loading>Posting...</span>
|
||||
</flux:button>
|
||||
@@ -98,11 +79,26 @@
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Category</dt><dd>{{ $ticket->category?->name ?? 'None' }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Project</dt><dd>{{ $ticket->project?->title ?? 'None' }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Team</dt><dd>{{ $ticket->team?->name ?? 'None' }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Created</dt><dd>{{ $ticket->created_at->format('d M Y H:i') }}</dd></div>
|
||||
<div class="flex justify-between gap-4"><dt class="text-zinc-500">Due</dt><dd>{{ $ticket->due_at?->format('d M Y') ?? 'No due date' }}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Git commits</h2>
|
||||
<div class="space-y-2">
|
||||
@forelse ($ticket->gitCommits as $commit)
|
||||
<a href="{{ $commit->url ?: '#' }}" target="_blank" class="block rounded-xl border border-zinc-100 p-3 dark:border-zinc-800">
|
||||
<div class="font-mono text-xs text-zinc-500">{{ $commit->short_hash }} · {{ $commit->repository?->name }}</div>
|
||||
<div class="mt-1 text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ $commit->message }}</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed border-zinc-300 p-4 text-center text-sm text-zinc-500 dark:border-zinc-700">No commits linked.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@can('update', $ticket)
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<h2 class="mb-4 text-base font-semibold text-zinc-900 dark:text-white">Agent actions</h2>
|
||||
@@ -111,7 +107,15 @@
|
||||
<option value="{{ $status->id }}">{{ $status->name }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:button 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" 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>
|
||||
</div>
|
||||
</div>
|
||||
@endcan
|
||||
</aside>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
@props(['comments', 'level' => 0])
|
||||
|
||||
<div class="space-y-3">
|
||||
@foreach ($comments as $comment)
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900" style="margin-left: {{ min((int) $level, 4) * 1.25 }}rem">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $comment->user?->name ?? 'Unknown user' }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ $comment->created_at?->format('d M Y H:i') }}</div>
|
||||
</div>
|
||||
@if ($comment->is_internal)
|
||||
<flux:badge color="amber">Internal</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="prose prose-sm mt-3 max-w-none dark:prose-invert">
|
||||
{!! $comment->body !!}
|
||||
</div>
|
||||
|
||||
@if ($comment->attachments->isNotEmpty())
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
@foreach ($comment->attachments as $attachment)
|
||||
<a href="{{ $attachment->url() }}" target="_blank" class="rounded-lg border border-zinc-200 px-3 py-1 text-xs text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800">
|
||||
{{ $attachment->original_name }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-3 flex justify-end">
|
||||
<flux:button size="sm" variant="ghost" wire:click="startReply({{ $comment->id }})">Reply</flux:button>
|
||||
</div>
|
||||
|
||||
@if (($reply_parent_id ?? null) === $comment->id)
|
||||
<form wire:submit="addReply" class="mt-4 rounded-xl border border-blue-200 bg-blue-50 p-4 dark:border-blue-900 dark:bg-blue-950/40">
|
||||
<x-helpdesk::html-editor 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">Post reply</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if ($comment->replies->isNotEmpty())
|
||||
<div class="mt-4">
|
||||
@include('helpdesk::partials.comment-thread', ['comments' => $comment->replies, 'level' => ((int) $level) + 1])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="mb-8 overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm">
|
||||
<div class="bg-gradient-to-r from-slate-950 via-slate-900 to-blue-950 px-6 py-8 text-white">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-[0.3em] text-blue-200">
|
||||
Helpdesk
|
||||
</p>
|
||||
|
||||
<h1 class="mt-2 text-3xl font-bold tracking-tight">
|
||||
{{ $title ?? 'Helpdesk' }}
|
||||
</h1>
|
||||
|
||||
@isset($subtitle)
|
||||
<p class="mt-2 max-w-3xl text-sm text-slate-300">
|
||||
{{ $subtitle }}
|
||||
</p>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
@isset($actions)
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endisset
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user