feat(ticketing): implement mobile-responsive inbox UX
This commit is contained in:
@@ -1,39 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen overflow-hidden bg-slate-100 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
|
<div class="flex h-screen overflow-hidden bg-gray-100 text-gray-900">
|
||||||
|
|
||||||
<!-- Bootstrap / first-run state -->
|
<!-- Bootstrap / first-run state -->
|
||||||
<div v-if="isBootstrap" class="flex flex-col items-center justify-center w-full h-full text-center px-6">
|
<div v-if="isBootstrap" class="flex flex-col items-center justify-center w-full h-full text-center px-6">
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
<div class="text-5xl mb-4">🎫</div>
|
<div class="text-5xl mb-4">🎫</div>
|
||||||
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2">Ticketing isn’t set up yet</h2>
|
<h2 class="text-2xl font-bold text-gray-800 mb-2">Ticketing isn’t set up yet</h2>
|
||||||
<p class="text-slate-500 dark:text-slate-400 mb-6">Create your first group in Settings to get started. Default priorities will be seeded automatically.</p>
|
<p class="text-gray-500 mb-6">Create your first group in Settings to get started. Default priorities will be seeded automatically.</p>
|
||||||
<Link :href="route('ticketing.settings')" class="inline-block bg-indigo-600 text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:bg-indigo-700 transition">Go to Settings</Link>
|
<Link :href="route('ticketing.settings')" class="inline-block bg-indigo-600 text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:bg-indigo-700 transition">Go to Settings</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<aside class="hidden w-72 shrink-0 border-r border-slate-200 bg-white/90 backdrop-blur dark:border-slate-800 dark:bg-slate-900/90 lg:flex lg:flex-col">
|
<aside
|
||||||
<div class="border-b border-slate-200 p-5 dark:border-slate-800">
|
class="w-full shrink-0 border-r border-gray-200 bg-white/90 backdrop-blur lg:flex lg:w-72 lg:flex-col"
|
||||||
|
:class="showSidebar ? 'flex flex-col' : 'hidden'"
|
||||||
|
>
|
||||||
|
<div class="border-b border-gray-200 p-5">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-400">Shared inbox</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-gray-400">Shared inbox</p>
|
||||||
<h1 class="mt-1 text-2xl font-semibold tracking-tight">Ticketing</h1>
|
<h1 class="mt-1 text-2xl font-semibold tracking-tight">Ticketing</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex h-10 items-center justify-center rounded-xl border border-gray-200 px-4 text-sm font-medium text-gray-600 transition hover:bg-gray-100 lg:hidden"
|
||||||
|
@click="showSidebar = false"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
<Link
|
<Link
|
||||||
:href="route('ticketing.create')"
|
:href="route('ticketing.create')"
|
||||||
class="inline-flex h-10 items-center justify-center rounded-xl bg-slate-900 px-4 text-sm font-medium text-white transition hover:bg-slate-700 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
class="inline-flex h-10 items-center justify-center rounded-xl bg-gray-900 px-4 text-sm font-medium text-white transition hover:bg-gray-700:bg-indigo-400"
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 space-y-6 overflow-y-auto p-5">
|
<div class="flex-1 space-y-6 overflow-y-auto p-5">
|
||||||
<section class="space-y-2">
|
<section class="space-y-2">
|
||||||
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Group switcher</label>
|
<label class="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Group switcher</label>
|
||||||
<select
|
<select
|
||||||
v-model="activeGroupId"
|
v-model="activeGroupId"
|
||||||
class="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm shadow-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:border-slate-700 dark:bg-slate-800 dark:focus:border-indigo-500 dark:focus:ring-indigo-500/20"
|
class="w-full rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm shadow-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100:border-indigo-500:ring-indigo-500/20"
|
||||||
@change="applyFilters({ group_id: normalizeNullable(activeGroupId) })"
|
@change="applyFilters({ group_id: normalizeNullable(activeGroupId) })"
|
||||||
>
|
>
|
||||||
<option :value="null">All groups</option>
|
<option :value="null">All groups</option>
|
||||||
@@ -43,8 +55,8 @@
|
|||||||
|
|
||||||
<section class="space-y-2">
|
<section class="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Views</h2>
|
<h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Views</h2>
|
||||||
<span class="text-xs text-slate-400">{{ filteredTickets.length }}</span>
|
<span class="text-xs text-gray-400">{{ filteredTickets.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -53,14 +65,14 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between rounded-2xl px-3 py-3 text-sm transition"
|
class="flex w-full items-center justify-between rounded-2xl px-3 py-3 text-sm transition"
|
||||||
:class="currentFilter === view.key
|
:class="currentFilter === view.key
|
||||||
? 'bg-slate-900 text-white shadow-sm dark:bg-indigo-500'
|
? 'bg-gray-900 text-white shadow-sm'
|
||||||
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'"
|
: 'text-gray-600 hover:bg-gray-100:bg-gray-800'"
|
||||||
@click="applyFilters({ filter: view.key === 'all' ? 'all' : view.key })"
|
@click="applyFilters({ filter: view.key === 'all' ? 'all' : view.key })"
|
||||||
>
|
>
|
||||||
<span>{{ view.label }}</span>
|
<span>{{ view.label }}</span>
|
||||||
<span
|
<span
|
||||||
class="rounded-full px-2 py-0.5 text-xs"
|
class="rounded-full px-2 py-0.5 text-xs"
|
||||||
:class="currentFilter === view.key ? 'bg-white/15 text-white' : 'bg-slate-200 text-slate-600 dark:bg-slate-800 dark:text-slate-300'"
|
:class="currentFilter === view.key ? 'bg-white/15 text-white' : 'bg-gray-200 text-gray-600'"
|
||||||
>
|
>
|
||||||
{{ viewCount(view.key) }}
|
{{ viewCount(view.key) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -69,11 +81,11 @@
|
|||||||
|
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Priority</h2>
|
<h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Priority</h2>
|
||||||
<button
|
<button
|
||||||
v-if="filters.priority_id"
|
v-if="filters.priority_id"
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xs text-slate-400 transition hover:text-slate-600 dark:hover:text-slate-200"
|
class="text-xs text-gray-400 transition hover:text-gray-600:text-gray-200"
|
||||||
@click="applyFilters({ priority_id: undefined })"
|
@click="applyFilters({ priority_id: undefined })"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
@@ -86,7 +98,7 @@
|
|||||||
:key="priority.id"
|
:key="priority.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition"
|
class="inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition"
|
||||||
:class="Number(filters.priority_id) === priority.id ? 'shadow-sm' : 'border-slate-200 text-slate-600 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800'"
|
:class="Number(filters.priority_id) === priority.id ? 'shadow-sm' : 'border-gray-200 text-gray-600 hover:bg-gray-100:bg-gray-800'"
|
||||||
:style="priorityChipStyle(priority, Number(filters.priority_id) === priority.id)"
|
:style="priorityChipStyle(priority, Number(filters.priority_id) === priority.id)"
|
||||||
@click="togglePriority(priority.id)"
|
@click="togglePriority(priority.id)"
|
||||||
>
|
>
|
||||||
@@ -98,11 +110,11 @@
|
|||||||
|
|
||||||
<section class="space-y-3">
|
<section class="space-y-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Projects</h2>
|
<h2 class="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Projects</h2>
|
||||||
<button
|
<button
|
||||||
v-if="filters.project_id"
|
v-if="filters.project_id"
|
||||||
type="button"
|
type="button"
|
||||||
class="text-xs text-slate-400 transition hover:text-slate-600 dark:hover:text-slate-200"
|
class="text-xs text-gray-400 transition hover:text-gray-600:text-gray-200"
|
||||||
@click="applyFilters({ project_id: undefined })"
|
@click="applyFilters({ project_id: undefined })"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
@@ -113,11 +125,11 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-sm transition"
|
class="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-sm transition"
|
||||||
:class="!filters.project_id ? 'bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-100' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'"
|
:class="!filters.project_id ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-100:bg-gray-800'"
|
||||||
@click="applyFilters({ project_id: undefined })"
|
@click="applyFilters({ project_id: undefined })"
|
||||||
>
|
>
|
||||||
<span>All projects</span>
|
<span>All projects</span>
|
||||||
<span class="text-xs text-slate-400">{{ totalProjectCount }}</span>
|
<span class="text-xs text-gray-400">{{ totalProjectCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -125,29 +137,51 @@
|
|||||||
:key="project.id"
|
:key="project.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-sm transition"
|
class="flex w-full items-center justify-between rounded-2xl px-3 py-2.5 text-sm transition"
|
||||||
:class="Number(filters.project_id) === project.id ? 'bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-slate-100' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'"
|
:class="Number(filters.project_id) === project.id ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-100:bg-gray-800'"
|
||||||
@click="applyFilters({ project_id: Number(filters.project_id) === project.id ? undefined : project.id })"
|
@click="applyFilters({ project_id: Number(filters.project_id) === project.id ? undefined : project.id })"
|
||||||
>
|
>
|
||||||
<span class="truncate">{{ project.name }}</span>
|
<span class="truncate">{{ project.name }}</span>
|
||||||
<span class="rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-600 dark:bg-slate-700 dark:text-slate-300">{{ project.ticket_count ?? 0 }}</span>
|
<span class="rounded-full bg-gray-200 px-2 py-0.5 text-xs text-gray-600">{{ project.ticket_count ?? 0 }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="flex min-w-0 flex-1 border-r border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-925 xl:max-w-[34rem]">
|
<main
|
||||||
|
class="min-w-0 flex-1 border-r border-gray-200 bg-white xl:max-w-[34rem]"
|
||||||
|
:class="showList ? 'flex' : 'hidden lg:flex'"
|
||||||
|
>
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
<div class="border-b border-slate-200 p-5 dark:border-slate-800">
|
<div class="border-b border-gray-200 p-5">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div class="min-w-0 flex-1">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400">Queue</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-gray-400">Queue</p>
|
||||||
<h2 class="mt-1 text-xl font-semibold tracking-tight">{{ currentViewTitle }}</h2>
|
<h2 class="mt-1 text-xl font-semibold tracking-tight">{{ currentViewTitle }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-full border border-slate-200 px-3 py-1 text-xs text-slate-500 dark:border-slate-700 dark:text-slate-300">
|
<div class="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex h-10 items-center justify-center rounded-xl border border-gray-200 px-4 text-sm font-medium text-gray-600 transition hover:bg-gray-100 lg:hidden"
|
||||||
|
@click="showSidebar = true"
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
:href="route('ticketing.create')"
|
||||||
|
class="inline-flex h-10 items-center justify-center rounded-xl bg-gray-900 px-4 text-sm font-medium text-white transition hover:bg-gray-700 lg:hidden"
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Link>
|
||||||
|
<div class="hidden rounded-full border border-gray-200 px-3 py-1 text-xs text-gray-500 sm:block">
|
||||||
{{ tickets.total }} total
|
{{ tickets.total }} total
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 text-xs text-gray-500 sm:hidden">
|
||||||
|
{{ tickets.total }} total
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
|
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
|
||||||
<div class="relative flex-1">
|
<div class="relative flex-1">
|
||||||
@@ -155,12 +189,12 @@
|
|||||||
v-model="search"
|
v-model="search"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search tickets, people, or numbers"
|
placeholder="Search tickets, people, or numbers"
|
||||||
class="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:border-slate-700 dark:bg-slate-800 dark:focus:border-indigo-500 dark:focus:ring-indigo-500/20"
|
class="w-full rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100:border-indigo-500:ring-indigo-500/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
v-model="sortBy"
|
v-model="sortBy"
|
||||||
class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:border-slate-700 dark:bg-slate-800 dark:focus:border-indigo-500 dark:focus:ring-indigo-500/20"
|
class="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100:border-indigo-500:ring-indigo-500/20"
|
||||||
>
|
>
|
||||||
<option value="newest">Newest first</option>
|
<option value="newest">Newest first</option>
|
||||||
<option value="oldest">Oldest first</option>
|
<option value="oldest">Oldest first</option>
|
||||||
@@ -172,9 +206,9 @@
|
|||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||||
<div v-if="filteredTickets.length === 0" class="flex h-full flex-col items-center justify-center px-6 text-center">
|
<div v-if="filteredTickets.length === 0" class="flex h-full flex-col items-center justify-center px-6 text-center">
|
||||||
<div class="rounded-3xl border border-dashed border-slate-300 px-8 py-10 dark:border-slate-700">
|
<div class="rounded-3xl border border-dashed border-gray-300 px-8 py-10">
|
||||||
<p class="text-sm font-medium text-slate-500 dark:text-slate-300">Nothing matches the current filters.</p>
|
<p class="text-sm font-medium text-gray-500">Nothing matches the current filters.</p>
|
||||||
<p class="mt-2 text-sm text-slate-400">Try a different view, search, or priority chip.</p>
|
<p class="mt-2 text-sm text-gray-400">Try a different view, search, or priority chip.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,24 +216,24 @@
|
|||||||
v-for="ticket in filteredTickets"
|
v-for="ticket in filteredTickets"
|
||||||
:key="ticket.id"
|
:key="ticket.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full gap-4 border-b border-slate-100 px-5 py-4 text-left transition hover:bg-slate-50 dark:border-slate-800 dark:hover:bg-slate-900/60"
|
class="flex w-full gap-4 border-b border-gray-100 px-4 py-4 text-left transition hover:bg-gray-50:bg-gray-900/60 sm:px-5"
|
||||||
:class="selectedTicketId === ticket.id ? 'bg-indigo-50/80 dark:bg-indigo-500/10' : ''"
|
:class="selectedTicketId === ticket.id ? 'bg-indigo-50/80' : ''"
|
||||||
@click="selectTicket(ticket.id)"
|
@click="selectTicket(ticket.id)"
|
||||||
>
|
>
|
||||||
<div class="w-1 self-stretch rounded-full" :style="{ backgroundColor: ticket.priority?.color || ticket.group?.color || '#cbd5e1' }"></div>
|
<div class="w-1 self-stretch rounded-full" :style="{ backgroundColor: ticket.priority?.color || ticket.group?.color || '#cbd5e1' }"></div>
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">{{ ticket.number }}</span>
|
<span class="text-xs font-semibold uppercase tracking-[0.12em] text-gray-400">{{ ticket.number }}</span>
|
||||||
<span :class="badgeClass(ticket.status)" class="inline-flex rounded-full px-2 py-0.5 text-[11px] font-semibold">{{ statusLabel(ticket.status) }}</span>
|
<span :class="badgeClass(ticket.status)" class="inline-flex rounded-full px-2 py-0.5 text-[11px] font-semibold">{{ statusLabel(ticket.status) }}</span>
|
||||||
<span v-if="hasUnread(ticket)" class="h-2.5 w-2.5 rounded-full bg-sky-500"></span>
|
<span v-if="hasUnread(ticket)" class="h-2.5 w-2.5 rounded-full bg-sky-500"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mt-2 truncate text-sm font-semibold text-slate-900 dark:text-slate-100">{{ ticket.title }}</p>
|
<p class="mt-2 truncate text-sm font-semibold text-gray-900">{{ ticket.title }}</p>
|
||||||
|
|
||||||
<div class="mt-3 flex items-center gap-3 text-xs text-slate-500 dark:text-slate-400">
|
<div class="mt-3 flex items-center gap-3 text-xs text-gray-500">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="avatar-ring bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-100">{{ initials(ticket.submitter?.name || ticket.submitter?.email || '?') }}</span>
|
<span class="avatar-ring bg-gray-200 text-gray-700">{{ initials(ticket.submitter?.name || ticket.submitter?.email || '?') }}</span>
|
||||||
<span class="truncate">{{ ticket.submitter?.name || ticket.submitter?.email || 'Unknown' }}</span>
|
<span class="truncate">{{ ticket.submitter?.name || ticket.submitter?.email || 'Unknown' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -208,12 +242,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex shrink-0 flex-col items-end justify-between gap-3">
|
<div class="flex shrink-0 flex-col items-end justify-between gap-3">
|
||||||
<div class="text-xs font-medium text-slate-500 dark:text-slate-400">{{ ticket.priority?.name || 'No priority' }}</div>
|
<div class="text-xs font-medium text-gray-500">{{ ticket.priority?.name || 'No priority' }}</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[11px] uppercase tracking-[0.16em] text-slate-400">Assignee</span>
|
<span class="text-[11px] uppercase tracking-[0.16em] text-gray-400">Assignee</span>
|
||||||
<span
|
<span
|
||||||
class="avatar-ring"
|
class="avatar-ring"
|
||||||
:class="ticket.assignee ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-200' : 'bg-slate-200 text-slate-500 dark:bg-slate-700 dark:text-slate-300'"
|
:class="ticket.assignee ? 'bg-indigo-100 text-indigo-700' : 'bg-gray-200 text-gray-500'"
|
||||||
>
|
>
|
||||||
{{ ticket.assignee ? initials(ticket.assignee.name || ticket.assignee.email) : '—' }}
|
{{ ticket.assignee ? initials(ticket.assignee.name || ticket.assignee.email) : '—' }}
|
||||||
</span>
|
</span>
|
||||||
@@ -222,87 +256,100 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="tickets.last_page > 1" class="flex items-center justify-between border-t border-slate-200 px-5 py-3 text-sm dark:border-slate-800">
|
<div v-if="tickets.last_page > 1" class="flex items-center justify-between border-t border-gray-200 px-5 py-3 text-sm">
|
||||||
<span class="text-slate-500 dark:text-slate-400">Page {{ tickets.current_page }} of {{ tickets.last_page }}</span>
|
<span class="text-gray-500">Page {{ tickets.current_page }} of {{ tickets.last_page }}</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
v-if="tickets.prev_page_url"
|
v-if="tickets.prev_page_url"
|
||||||
:href="tickets.prev_page_url"
|
:href="tickets.prev_page_url"
|
||||||
class="rounded-xl border border-slate-200 px-3 py-1.5 text-slate-600 transition hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
|
class="rounded-xl border border-gray-200 px-3 py-1.5 text-gray-600 transition hover:bg-gray-100:bg-gray-800"
|
||||||
>Prev</Link>
|
>Prev</Link>
|
||||||
<Link
|
<Link
|
||||||
v-if="tickets.next_page_url"
|
v-if="tickets.next_page_url"
|
||||||
:href="tickets.next_page_url"
|
:href="tickets.next_page_url"
|
||||||
class="rounded-xl border border-slate-200 px-3 py-1.5 text-slate-600 transition hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
|
class="rounded-xl border border-gray-200 px-3 py-1.5 text-gray-600 transition hover:bg-gray-100:bg-gray-800"
|
||||||
>Next</Link>
|
>Next</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<section class="flex min-w-0 flex-1 flex-col bg-slate-50 dark:bg-slate-950/80">
|
<section
|
||||||
|
class="min-w-0 flex-1 flex-col bg-gray-50"
|
||||||
|
:class="showDetail ? 'flex' : 'hidden lg:flex'"
|
||||||
|
>
|
||||||
<div v-if="!ticketDetail" class="flex h-full items-center justify-center p-8">
|
<div v-if="!ticketDetail" class="flex h-full items-center justify-center p-8">
|
||||||
<div class="max-w-md rounded-3xl border border-dashed border-slate-300 bg-white p-10 text-center shadow-sm dark:border-slate-700 dark:bg-slate-900">
|
<div class="max-w-md rounded-3xl border border-dashed border-gray-300 bg-white p-10 text-center shadow-sm">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-400">Conversation</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-400">Conversation</p>
|
||||||
<h3 class="mt-3 text-2xl font-semibold tracking-tight">Pick a ticket</h3>
|
<h3 class="mt-3 text-2xl font-semibold tracking-tight">Pick a ticket</h3>
|
||||||
<p class="mt-3 text-sm leading-6 text-slate-500 dark:text-slate-400">
|
<p class="mt-3 text-sm leading-6 text-gray-500">
|
||||||
The right panel becomes the full thread view: metadata, internal notes, and reply composer included. Miracles do happen.
|
The right panel becomes the full thread view: metadata, internal notes, and reply composer included. Miracles do happen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="border-b border-slate-200 bg-white px-6 py-5 dark:border-slate-800 dark:bg-slate-900">
|
<div class="border-b border-gray-200 bg-white px-4 py-5 sm:px-6">
|
||||||
|
<div class="mb-4 lg:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center rounded-xl border border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-100"
|
||||||
|
@click="backToList"
|
||||||
|
>
|
||||||
|
← Back to list
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold text-white" :style="{ backgroundColor: ticketDetail.group?.color || '#6366f1' }">{{ ticketDetail.number }}</span>
|
<span class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold text-white" :style="{ backgroundColor: ticketDetail.group?.color || '#6366f1' }">{{ ticketDetail.number }}</span>
|
||||||
<span :class="badgeClass(ticketDetail.status)" class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold">{{ statusLabel(ticketDetail.status) }}</span>
|
<span :class="badgeClass(ticketDetail.status)" class="inline-flex rounded-full px-2.5 py-1 text-xs font-semibold">{{ statusLabel(ticketDetail.status) }}</span>
|
||||||
<span v-if="ticketDetail.priority" class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-2.5 py-1 text-xs font-medium text-slate-600 dark:border-slate-700 dark:text-slate-300">
|
<span v-if="ticketDetail.priority" class="inline-flex items-center gap-2 rounded-full border border-gray-200 px-2.5 py-1 text-xs font-medium text-gray-600">
|
||||||
<span class="h-2.5 w-2.5 rounded-full" :style="{ backgroundColor: ticketDetail.priority.color || '#94a3b8' }"></span>
|
<span class="h-2.5 w-2.5 rounded-full" :style="{ backgroundColor: ticketDetail.priority.color || '#94a3b8' }"></span>
|
||||||
{{ ticketDetail.priority.name }}
|
{{ ticketDetail.priority.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 flex items-start gap-3">
|
<div class="mt-3 flex items-start gap-3">
|
||||||
<h3 class="min-w-0 flex-1 text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-100">{{ ticketDetail.title }}</h3>
|
<h3 class="min-w-0 flex-1 text-2xl font-semibold tracking-tight text-gray-900">{{ ticketDetail.title }}</h3>
|
||||||
<Link :href="route('ticketing.show', { ticket: ticketDetail.id })" class="text-sm font-medium text-indigo-600 transition hover:text-indigo-500 dark:text-indigo-300">Open full page</Link>
|
<Link :href="route('ticketing.show', { ticket: ticketDetail.id })" class="text-sm font-medium text-indigo-600 transition hover:text-indigo-500">Open full page</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 grid gap-3 md:grid-cols-5">
|
<div class="mt-5 grid gap-3 md:grid-cols-5">
|
||||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
||||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">Assignee</div>
|
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Assignee</div>
|
||||||
<div class="mt-2 flex items-center gap-2 text-sm font-medium">
|
<div class="mt-2 flex items-center gap-2 text-sm font-medium">
|
||||||
<span class="avatar-ring bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-200">{{ ticketDetail.assignee ? initials(ticketDetail.assignee.name || ticketDetail.assignee.email) : '—' }}</span>
|
<span class="avatar-ring bg-indigo-100 text-indigo-700">{{ ticketDetail.assignee ? initials(ticketDetail.assignee.name || ticketDetail.assignee.email) : '—' }}</span>
|
||||||
<span class="truncate">{{ ticketDetail.assignee?.name || ticketDetail.assignee?.email || 'Unassigned' }}</span>
|
<span class="truncate">{{ ticketDetail.assignee?.name || ticketDetail.assignee?.email || 'Unassigned' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
||||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">Due date</div>
|
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Due date</div>
|
||||||
<div class="mt-2 text-sm font-medium">{{ formatDate(ticketDetail.due_date, 'No due date') }}</div>
|
<div class="mt-2 text-sm font-medium">{{ formatDate(ticketDetail.due_date, 'No due date') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
||||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">Project</div>
|
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Project</div>
|
||||||
<div class="mt-2 truncate text-sm font-medium">{{ ticketDetail.project?.name || 'No project' }}</div>
|
<div class="mt-2 truncate text-sm font-medium">{{ ticketDetail.project?.name || 'No project' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
||||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">Submitter</div>
|
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Submitter</div>
|
||||||
<div class="mt-2 truncate text-sm font-medium">{{ ticketDetail.submitter?.name || ticketDetail.submitter?.email || 'Unknown' }}</div>
|
<div class="mt-2 truncate text-sm font-medium">{{ ticketDetail.submitter?.name || ticketDetail.submitter?.email || 'Unknown' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
|
<div class="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
||||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">Opened</div>
|
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Opened</div>
|
||||||
<div class="mt-2 text-sm font-medium">{{ ageLabel(ticketDetail.created_at) }}</div>
|
<div class="mt-2 text-sm font-medium">{{ ageLabel(ticketDetail.created_at) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap items-center gap-3 text-sm text-slate-500 dark:text-slate-400">
|
<div class="mt-4 flex flex-wrap items-center gap-3 text-sm text-gray-500">
|
||||||
<span>Group: <strong class="text-slate-700 dark:text-slate-200">{{ ticketDetail.group?.name || 'Unknown' }}</strong></span>
|
<span>Group: <strong class="text-gray-700">{{ ticketDetail.group?.name || 'Unknown' }}</strong></span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{{ ticketDetail.messages?.length || 0 }} messages</span>
|
<span>{{ ticketDetail.messages?.length || 0 }} messages</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -328,13 +375,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto px-6 py-6">
|
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-6 sm:px-6">
|
||||||
<div v-if="ticketDetail.description" class="mb-6 rounded-3xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
<div v-if="ticketDetail.description" class="mb-6 rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||||
<div class="mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-400">Description</div>
|
<div class="mb-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Description</div>
|
||||||
<p class="whitespace-pre-wrap text-sm leading-6 text-slate-700 dark:text-slate-300">{{ ticketDetail.description }}</p>
|
<p class="whitespace-pre-wrap text-sm leading-6 text-gray-700">{{ ticketDetail.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!ticketDetail.messages?.length" class="rounded-3xl border border-dashed border-slate-300 bg-white p-8 text-center text-sm text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
|
<div v-if="!ticketDetail.messages?.length" class="rounded-3xl border border-dashed border-gray-300 bg-white p-8 text-center text-sm text-gray-500">
|
||||||
No messages yet.
|
No messages yet.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -348,7 +395,7 @@
|
|||||||
<div class="max-w-3xl rounded-3xl px-5 py-4 shadow-sm" :class="messageCardClass(message)">
|
<div class="max-w-3xl rounded-3xl px-5 py-4 shadow-sm" :class="messageCardClass(message)">
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs font-medium opacity-80">
|
<div class="flex flex-wrap items-center gap-2 text-xs font-medium opacity-80">
|
||||||
<span class="inline-flex items-center gap-2">
|
<span class="inline-flex items-center gap-2">
|
||||||
<span class="avatar-ring bg-white/70 text-slate-700 dark:bg-slate-900/60 dark:text-slate-100">{{ initials(messageAuthor(message)) }}</span>
|
<span class="avatar-ring bg-white/70 text-gray-700">{{ initials(messageAuthor(message)) }}</span>
|
||||||
{{ messageRoleLabel(message) }}
|
{{ messageRoleLabel(message) }}
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -364,7 +411,7 @@
|
|||||||
v-for="attachment in message.attachments"
|
v-for="attachment in message.attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
:href="route('ticketing.attachments.show', { attachment: attachment.id })"
|
:href="route('ticketing.attachments.show', { attachment: attachment.id })"
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-current/20 px-3 py-1.5 text-xs font-medium transition hover:bg-white/30 dark:hover:bg-slate-900/30"
|
class="inline-flex items-center gap-2 rounded-full border border-current/20 px-3 py-1.5 text-xs font-medium transition hover:bg-white/30:bg-gray-900/30"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
📎 {{ attachment.filename }}
|
📎 {{ attachment.filename }}
|
||||||
@@ -375,18 +422,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-slate-200 bg-white px-6 py-5 dark:border-slate-800 dark:bg-slate-900">
|
<div class="border-t border-gray-200 bg-white px-4 py-5 sm:px-6">
|
||||||
<div v-if="isAgent" class="mb-4 inline-flex rounded-2xl bg-slate-100 p-1 dark:bg-slate-800">
|
<div v-if="isAgent" class="mb-4 inline-flex rounded-2xl bg-gray-100 p-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-2xl px-4 py-2 text-sm font-medium transition"
|
class="rounded-2xl px-4 py-2 text-sm font-medium transition"
|
||||||
:class="replyMode === 'reply' ? 'bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white' : 'text-slate-500 dark:text-slate-300'"
|
:class="replyMode === 'reply' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500'"
|
||||||
@click="replyMode = 'reply'"
|
@click="replyMode = 'reply'"
|
||||||
>Reply</button>
|
>Reply</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-2xl px-4 py-2 text-sm font-medium transition"
|
class="rounded-2xl px-4 py-2 text-sm font-medium transition"
|
||||||
:class="replyMode === 'internal' ? 'bg-amber-500 text-white shadow-sm' : 'text-slate-500 dark:text-slate-300'"
|
:class="replyMode === 'internal' ? 'bg-amber-500 text-white shadow-sm' : 'text-gray-500'"
|
||||||
@click="replyMode = 'internal'"
|
@click="replyMode = 'internal'"
|
||||||
>Internal note</button>
|
>Internal note</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -398,20 +445,20 @@
|
|||||||
required
|
required
|
||||||
class="w-full rounded-3xl border px-4 py-3 text-sm outline-none transition focus:ring-2"
|
class="w-full rounded-3xl border px-4 py-3 text-sm outline-none transition focus:ring-2"
|
||||||
:class="replyMode === 'internal'
|
:class="replyMode === 'internal'
|
||||||
? 'border-amber-300 bg-amber-50 text-amber-950 focus:border-amber-400 focus:ring-amber-100 dark:border-amber-700 dark:bg-amber-950/30 dark:text-amber-100 dark:focus:ring-amber-500/20'
|
? 'border-amber-300 bg-amber-50 text-amber-950 focus:border-amber-400 focus:ring-amber-100:ring-amber-500/20'
|
||||||
: 'border-slate-200 bg-slate-50 text-slate-900 focus:border-indigo-400 focus:ring-indigo-100 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:focus:border-indigo-500 dark:focus:ring-indigo-500/20'"
|
: 'border-gray-200 bg-gray-50 text-gray-900 focus:border-indigo-400 focus:ring-indigo-100:border-indigo-500:ring-indigo-500/20'"
|
||||||
:placeholder="replyMode === 'internal' ? 'Internal note for agents only…' : 'Reply to the thread…'"
|
:placeholder="replyMode === 'internal' ? 'Internal note for agents only…' : 'Reply to the thread…'"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="text-xs text-slate-400">
|
<div class="text-xs text-gray-400">
|
||||||
{{ replyMode === 'internal' ? 'Only agents can see internal notes.' : 'Replies are visible in the ticket thread.' }}
|
{{ replyMode === 'internal' ? 'Only agents can see internal notes.' : 'Replies are visible in the ticket thread.' }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="messageForm.processing"
|
:disabled="messageForm.processing"
|
||||||
class="inline-flex items-center rounded-2xl px-4 py-2 text-sm font-medium text-white transition disabled:cursor-not-allowed disabled:opacity-60"
|
class="inline-flex items-center rounded-2xl px-4 py-2 text-sm font-medium text-white transition disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
:class="replyMode === 'internal' ? 'bg-amber-500 hover:bg-amber-600' : 'bg-slate-900 hover:bg-slate-700 dark:bg-indigo-500 dark:hover:bg-indigo-400'"
|
:class="replyMode === 'internal' ? 'bg-amber-500 hover:bg-amber-600' : 'bg-gray-900 hover:bg-gray-700:bg-indigo-400'"
|
||||||
>
|
>
|
||||||
{{ messageForm.processing ? 'Sending…' : replyMode === 'internal' ? 'Save note' : 'Send reply' }}
|
{{ messageForm.processing ? 'Sending…' : replyMode === 'internal' ? 'Save note' : 'Send reply' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -445,6 +492,7 @@ const search = ref('')
|
|||||||
const sortBy = ref('newest')
|
const sortBy = ref('newest')
|
||||||
const activeGroupId = ref(props.filters?.group_id ? Number(props.filters.group_id) : null)
|
const activeGroupId = ref(props.filters?.group_id ? Number(props.filters.group_id) : null)
|
||||||
const replyMode = ref('reply')
|
const replyMode = ref('reply')
|
||||||
|
const showSidebar = ref(!props.ticketDetail)
|
||||||
|
|
||||||
const visibleViews = computed(() => {
|
const visibleViews = computed(() => {
|
||||||
const base = [
|
const base = [
|
||||||
@@ -462,6 +510,8 @@ const currentFilter = computed(() => props.filters?.filter || 'all')
|
|||||||
const selectedTicketId = computed(() => props.ticketDetail?.id ?? (props.filters?.detail ? Number(props.filters.detail) : null))
|
const selectedTicketId = computed(() => props.ticketDetail?.id ?? (props.filters?.detail ? Number(props.filters.detail) : null))
|
||||||
const currentViewTitle = computed(() => visibleViews.value.find((view) => view.key === currentFilter.value)?.label || 'Tickets')
|
const currentViewTitle = computed(() => visibleViews.value.find((view) => view.key === currentFilter.value)?.label || 'Tickets')
|
||||||
const totalProjectCount = computed(() => props.projects.reduce((sum, project) => sum + Number(project.ticket_count || 0), 0))
|
const totalProjectCount = computed(() => props.projects.reduce((sum, project) => sum + Number(project.ticket_count || 0), 0))
|
||||||
|
const showList = computed(() => !props.ticketDetail)
|
||||||
|
const showDetail = computed(() => !!props.ticketDetail)
|
||||||
|
|
||||||
const filteredTickets = computed(() => {
|
const filteredTickets = computed(() => {
|
||||||
const term = search.value.trim().toLowerCase()
|
const term = search.value.trim().toLowerCase()
|
||||||
@@ -523,6 +573,7 @@ watch(
|
|||||||
() => props.ticketDetail,
|
() => props.ticketDetail,
|
||||||
(detail) => {
|
(detail) => {
|
||||||
replyMode.value = 'reply'
|
replyMode.value = 'reply'
|
||||||
|
showSidebar.value = !detail
|
||||||
metaForm.defaults({
|
metaForm.defaults({
|
||||||
status: detail?.status ?? 'open',
|
status: detail?.status ?? 'open',
|
||||||
priority_id: detail?.priority_id ?? null,
|
priority_id: detail?.priority_id ?? null,
|
||||||
@@ -584,6 +635,15 @@ function selectTicket(ticketId) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backToList() {
|
||||||
|
router.get(route('ticketing.index'), buildParams({ detail: undefined }), {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
replace: true,
|
||||||
|
only: ['ticketDetail', 'detailAgents', 'filters'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function saveMeta() {
|
function saveMeta() {
|
||||||
if (!props.ticketDetail) return
|
if (!props.ticketDetail) return
|
||||||
|
|
||||||
@@ -631,12 +691,12 @@ function initials(value) {
|
|||||||
|
|
||||||
function badgeClass(status) {
|
function badgeClass(status) {
|
||||||
return {
|
return {
|
||||||
open: 'bg-sky-100 text-sky-700 dark:bg-sky-500/20 dark:text-sky-200',
|
open: 'bg-sky-100 text-sky-700',
|
||||||
in_progress: 'bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-200',
|
in_progress: 'bg-violet-100 text-violet-700',
|
||||||
pending: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-200',
|
pending: 'bg-amber-100 text-amber-700',
|
||||||
resolved: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-200',
|
resolved: 'bg-emerald-100 text-emerald-700',
|
||||||
closed: 'bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
|
closed: 'bg-gray-200 text-gray-700',
|
||||||
}[status] || 'bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-200'
|
}[status] || 'bg-gray-200 text-gray-700'
|
||||||
}
|
}
|
||||||
|
|
||||||
function priorityChipStyle(priority, active) {
|
function priorityChipStyle(priority, active) {
|
||||||
@@ -703,14 +763,14 @@ function messageAlignment(message) {
|
|||||||
|
|
||||||
function messageCardClass(message) {
|
function messageCardClass(message) {
|
||||||
if (message.is_internal) {
|
if (message.is_internal) {
|
||||||
return 'border border-amber-200 bg-amber-50 text-amber-950 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100'
|
return 'border border-amber-200 bg-amber-50 text-amber-950'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSubmitterMessage(message)) {
|
if (isSubmitterMessage(message)) {
|
||||||
return 'border border-slate-200 bg-white text-slate-800 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'
|
return 'border border-gray-200 bg-white text-gray-800'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'bg-slate-900 text-white dark:bg-indigo-500'
|
return 'bg-gray-900 text-white'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSubmitterMessage(message) {
|
function isSubmitterMessage(message) {
|
||||||
@@ -733,6 +793,6 @@ function messageAuthor(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.meta-select {
|
.meta-select {
|
||||||
@apply w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:focus:border-indigo-500 dark:focus:ring-indigo-500/20;
|
@apply w-full rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 focus:ring-indigo-500/20;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user