253 lines
15 KiB
PHP
Executable File
253 lines
15 KiB
PHP
Executable File
<div class="hd-helpdesk-surface hd-mobile-ready space-y-6" dusk="helpdesk-ticket-index">
|
||
<div
|
||
class="overflow-hidden rounded-3xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||
<div class="hd-page-hero 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
|
||
projects, teams, priorities and statuses.</p>
|
||
</div>
|
||
<div class="flex flex-wrap gap-2">
|
||
<a href="{{ route('helpdesk.index') }}" wire:navigate class="hd-btn hd-btn-blue">
|
||
<i class="fa-solid fa-chart-bar" aria-hidden="true"></i>
|
||
<span>Dashboard</span>
|
||
</a>
|
||
<a href="{{ route('helpdesk.tickets.create') }}" wire:navigate class="hd-btn hd-btn-green">
|
||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||
<span>New Ticket</span>
|
||
</a>
|
||
<a href="{{ route('helpdesk.tickets.archived') }}" wire:navigate class="hd-btn hd-btn-amber">
|
||
<i class="fa-solid fa-box-archive" aria-hidden="true"></i>
|
||
<span>Archived</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="hd-filter-panel rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||
<div class="grid gap-4 md:grid-cols-4">
|
||
<div class="hd-ticket-search-wrap">
|
||
<i class="fa-solid fa-magnifying-glass hd-ticket-search-icon" aria-hidden="true"></i>
|
||
<input
|
||
type="search"
|
||
class="hd-ticket-search"
|
||
wire:model.live.debounce.350ms="search"
|
||
placeholder="Search reference, subject or body"
|
||
aria-label="Search tickets"
|
||
>
|
||
</div>
|
||
|
||
<div class="hd-select-wrap">
|
||
<select class="hd-select" wire:model.live="scope" aria-label="Ticket scope">
|
||
<option value="open">Open tickets</option>
|
||
<option value="closed">Closed tickets</option>
|
||
<option value="all">All tickets</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="hd-select-wrap">
|
||
<select class="hd-select" wire:model.live="status" aria-label="Ticket status">
|
||
<option value="">All statuses</option>
|
||
@foreach ($statuses as $status)
|
||
<option value="{{ $status->id }}">{{ $status->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
|
||
<div class="hd-select-wrap">
|
||
<select class="hd-select" wire:model.live="priority" aria-label="Ticket priority">
|
||
<option value="">All priorities</option>
|
||
@foreach ($priorities as $priority)
|
||
<option value="{{ $priority->id }}">{{ $priority->name }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
|
||
<div wire:loading.flex
|
||
class="items-center gap-2 border-b border-zinc-200 bg-blue-50 px-4 py-3 text-sm text-blue-800 dark:border-zinc-700 dark:bg-blue-950 dark:text-blue-200">
|
||
<flux:icon.arrow-path class="size-4 animate-spin" /> Refreshing tickets...
|
||
</div>
|
||
|
||
<div class="overflow-x-auto">
|
||
<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">Reference</th>
|
||
<th class="px-4 py-3">Subject</th>
|
||
<th class="px-4 py-3">Status</th>
|
||
<th class="px-4 py-3">Priority</th>
|
||
<th class="px-4 py-3">Agent</th>
|
||
<th class="px-4 py-3 text-center">Files</th>
|
||
<th class="px-4 py-3">Last activity</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 ($tickets as $ticket)
|
||
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-800/70">
|
||
<td class="whitespace-nowrap px-4 py-3 font-mono text-xs">
|
||
<a href="{{ route('helpdesk.tickets.show', $ticket) }}" wire:navigate
|
||
class="font-semibold text-blue-700 hover:underline dark:text-blue-300">
|
||
{{ $ticket->reference }}
|
||
</a>
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<div class="font-medium text-zinc-900 dark:text-zinc-100">{{ $ticket->subject }}</div>
|
||
<div class="text-xs text-zinc-500">
|
||
{{ $ticket->category?->name ?? 'No category' }}
|
||
@if ($ticket->project)
|
||
· {{ $ticket->project->title }}
|
||
@endif
|
||
@if ($ticket->team)
|
||
· {{ $ticket->team->name }}
|
||
@endif
|
||
</div>
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<x-helpdesk.colour-badge :color="$ticket->status?->color ?? 'zinc'">
|
||
{{ $ticket->status?->name ?? 'No status' }}
|
||
</x-helpdesk.colour-badge>
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<x-helpdesk.colour-badge :color="$ticket->priority?->color ?? 'zinc'">
|
||
{{ $ticket->priority?->name ?? 'No priority' }}
|
||
</x-helpdesk.colour-badge>
|
||
</td>
|
||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">
|
||
{{ $ticket->agent_name ?? 'Unassigned' }}
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-center">
|
||
@if (($ticket->attachments_count ?? 0) > 0)
|
||
<span
|
||
class="inline-flex items-center justify-center gap-1 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-black text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
|
||
title="{{ $ticket->attachments_count }} attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}">
|
||
<i class="fa-solid fa-paperclip" aria-hidden="true"></i>
|
||
<span>{{ $ticket->attachments_count }}</span>
|
||
<span
|
||
class="sr-only">attachment{{ $ticket->attachments_count === 1 ? '' : 's' }}</span>
|
||
</span>
|
||
@else
|
||
<span
|
||
class="inline-flex items-center justify-center rounded-full border border-zinc-200 bg-zinc-50 px-2.5 py-1 text-xs font-bold text-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-500"
|
||
title="No attachments">
|
||
<i class="fa-regular fa-circle" aria-hidden="true"></i>
|
||
<span class="sr-only">No attachments</span>
|
||
</span>
|
||
@endif
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-zinc-500">
|
||
{{ $ticket->last_activity_at?->diffForHumans() ?? $ticket->created_at->diffForHumans() }}
|
||
</td>
|
||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||
@can('delete', $ticket)
|
||
<button type="button" wire:click="askToArchiveTicket({{ $ticket->id }})"
|
||
wire:loading.attr="disabled" wire:target="askToArchiveTicket({{ $ticket->id }})"
|
||
class="hd-btn hd-btn-sm hd-btn-amber disabled:cursor-not-allowed disabled:opacity-60"
|
||
title="Archive {{ $ticket->reference }}" dusk="delete-ticket-{{ $ticket->id }}">
|
||
<i class="fa-solid fa-box-archive" aria-hidden="true"></i>
|
||
<span>Archive</span>
|
||
</button>
|
||
@endcan
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="8" class="px-4 py-12 text-center text-zinc-500">No tickets matched your
|
||
filters.</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="flex flex-col gap-3 border-t border-zinc-200 bg-zinc-50/70 px-4 py-4 dark:border-zinc-700 dark:bg-zinc-950/35 sm:flex-row sm:items-center sm:justify-between">
|
||
<div class="text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
||
Showing
|
||
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $tickets->firstItem() ?? 0 }}</span>
|
||
–
|
||
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $tickets->lastItem() ?? 0 }}</span>
|
||
of
|
||
<span class="font-semibold text-zinc-800 dark:text-zinc-200">{{ $tickets->total() }}</span> tickets
|
||
</div>
|
||
|
||
<div class="min-w-0">
|
||
@if ($tickets->hasPages())
|
||
{{ $tickets->onEachSide(1)->links() }}
|
||
@else
|
||
<span class="inline-flex rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-xs font-semibold text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900">Page 1 of 1</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if ($archiveModalOpen)
|
||
<div class="fixed inset-0 z-[9998] flex items-center justify-center bg-black/75 px-4 py-6 backdrop-blur-sm"
|
||
role="dialog" aria-modal="true" aria-labelledby="archive-ticket-modal-title"
|
||
wire:key="ticket-archive-modal-{{ $pendingArchiveTicketId ?? 'none' }}"
|
||
dusk="archive-ticket-confirmation-modal">
|
||
<div
|
||
class="w-full max-w-lg overflow-hidden rounded-3xl border border-amber-500/30 bg-zinc-950 text-zinc-100 shadow-2xl">
|
||
<div class="border-b border-amber-500/20 bg-amber-950/35 px-6 py-5">
|
||
<div class="flex items-start gap-4">
|
||
<span
|
||
class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-amber-500 text-zinc-950 shadow-lg shadow-amber-950/30">
|
||
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
||
</span>
|
||
|
||
<div class="min-w-0">
|
||
<h2 id="archive-ticket-modal-title" class="text-xl font-black tracking-tight text-white">
|
||
Archive ticket?
|
||
</h2>
|
||
|
||
<p class="mt-1 text-sm leading-6 text-amber-100/90">
|
||
This will move <span
|
||
class="font-mono font-bold">{{ $pendingArchiveTicketReference }}</span> to the ticket archive.
|
||
It can be restored later.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-3 px-6 py-5">
|
||
@if ($pendingArchiveTicketSubject)
|
||
<div class="rounded-2xl border border-zinc-800 bg-zinc-900 px-4 py-3">
|
||
<p class="text-xs font-bold uppercase tracking-[0.22em] text-zinc-500">Ticket subject</p>
|
||
<p class="mt-1 text-sm font-semibold text-zinc-100">{{ $pendingArchiveTicketSubject }}</p>
|
||
</div>
|
||
@endif
|
||
|
||
<p class="text-sm leading-6 text-zinc-300">
|
||
Archived tickets are removed from active ticket lists and the Kanban board until restored.
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
class="flex flex-col-reverse gap-3 border-t border-zinc-800 bg-zinc-900/80 px-6 py-4 sm:flex-row sm:justify-end">
|
||
<button type="button" wire:click="cancelTicketArchive" class="hd-btn hd-btn-sm hd-btn-blue"
|
||
dusk="cancel-archive-ticket">
|
||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||
<span>Cancel</span>
|
||
</button>
|
||
|
||
<button type="button" wire:click="confirmTicketArchive" wire:loading.attr="disabled"
|
||
wire:target="confirmTicketArchive"
|
||
class="hd-btn hd-btn-sm hd-btn-amber disabled:cursor-not-allowed disabled:opacity-60"
|
||
dusk="confirm-archive-ticket">
|
||
<i class="fa-solid fa-box-archive" aria-hidden="true"></i>
|
||
<span wire:loading.remove wire:target="confirmTicketArchive">Archive ticket</span>
|
||
<span wire:loading wire:target="confirmTicketArchive">Archiving...</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|