Add recent support tickets section to customer dashboard
Shows the 5 most recent tickets with status/priority chips and relative timestamps. Includes empty state with Create Ticket button. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,12 @@ class DashboardController extends Controller
|
||||
->whereIn('status', ['open', 'in_progress', 'waiting'])
|
||||
->count();
|
||||
|
||||
$recentTickets = SupportTicket::query()
|
||||
->where('user_id', $user->id)
|
||||
->latest()
|
||||
->take(5)
|
||||
->get(['id', 'subject', 'status', 'priority', 'created_at', 'updated_at']);
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'activeServicesCount' => $activeServicesCount,
|
||||
'activeSubscriptionsCount' => $activeSubscriptionsCount,
|
||||
@@ -56,6 +62,7 @@ class DashboardController extends Controller
|
||||
'pendingInvoicesAmount' => number_format((float) $pendingInvoicesAmount, 2, '.', ''),
|
||||
'nextRenewalDate' => $nextRenewalDate,
|
||||
'openTicketsCount' => $openTicketsCount,
|
||||
'recentTickets' => $recentTickets,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Link } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import StatCard from '@/Components/StatCard.vue'
|
||||
import { resolveSubscriptionStatusColor, resolveInvoiceStatusColor, formatPrice } from '@/utils/resolvers'
|
||||
import type { Subscription, Invoice } from '@/types'
|
||||
import { resolveSubscriptionStatusColor, resolveInvoiceStatusColor, resolveTicketStatusColor, resolveTicketPriorityColor, formatPrice } from '@/utils/resolvers'
|
||||
import type { Subscription, Invoice, SupportTicket } from '@/types'
|
||||
|
||||
interface Props {
|
||||
activeServicesCount: number
|
||||
@@ -14,6 +14,7 @@ interface Props {
|
||||
pendingInvoicesAmount: string
|
||||
nextRenewalDate: string | null
|
||||
openTicketsCount: number
|
||||
recentTickets: SupportTicket[]
|
||||
}
|
||||
|
||||
defineOptions({ layout: AccountLayout })
|
||||
@@ -40,6 +41,33 @@ const formattedNextRenewal = computed<string>(() => {
|
||||
return formatDate(props.nextRenewalDate)
|
||||
})
|
||||
|
||||
function formatRelativeTime(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffMinutes = Math.floor(diffMs / 60000)
|
||||
const diffHours = Math.floor(diffMs / 3600000)
|
||||
const diffDays = Math.floor(diffMs / 86400000)
|
||||
|
||||
if (diffMinutes < 1) {
|
||||
return 'Just now'
|
||||
}
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes}m ago`
|
||||
}
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}h ago`
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays}d ago`
|
||||
}
|
||||
return formatDate(dateString)
|
||||
}
|
||||
|
||||
function formatStatus(status: string): string {
|
||||
return status.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
const unpaidInvoices = computed<Invoice[]>(() => {
|
||||
return props.latestInvoices.filter(
|
||||
(invoice: Invoice) => invoice.status === 'pending' || invoice.status === 'overdue',
|
||||
@@ -324,7 +352,98 @@ const unpaidInvoices = computed<Invoice[]>(() => {
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- Row 3: Quick Actions -->
|
||||
<!-- Row 3: Recent Support Tickets -->
|
||||
<VRow class="mb-2">
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
<VCardTitle class="d-flex align-center justify-space-between">
|
||||
<span>Recent Support Tickets</span>
|
||||
<Link
|
||||
href="/tickets"
|
||||
class="text-primary text-body-2 text-decoration-none"
|
||||
>
|
||||
View All
|
||||
</Link>
|
||||
</VCardTitle>
|
||||
|
||||
<VCardText v-if="recentTickets.length === 0">
|
||||
<div class="text-center py-8">
|
||||
<VIcon
|
||||
icon="tabler-ticket-off"
|
||||
size="48"
|
||||
class="text-disabled mb-3"
|
||||
/>
|
||||
<div class="text-body-2 text-medium-emphasis mb-3">
|
||||
No support tickets yet
|
||||
</div>
|
||||
<Link
|
||||
href="/tickets/create"
|
||||
class="text-decoration-none"
|
||||
>
|
||||
<VBtn
|
||||
size="small"
|
||||
color="primary"
|
||||
>
|
||||
Create Ticket
|
||||
</VBtn>
|
||||
</Link>
|
||||
</div>
|
||||
</VCardText>
|
||||
|
||||
<VTable
|
||||
v-else
|
||||
density="comfortable"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Subject</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Last Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="ticket in recentTickets"
|
||||
:key="ticket.id"
|
||||
>
|
||||
<td>
|
||||
<Link
|
||||
:href="`/tickets/${ticket.id}`"
|
||||
class="text-primary text-decoration-none font-weight-medium"
|
||||
>
|
||||
{{ ticket.subject }}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<VChip
|
||||
:color="resolveTicketStatusColor(ticket.status)"
|
||||
size="small"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ formatStatus(ticket.status) }}
|
||||
</VChip>
|
||||
</td>
|
||||
<td>
|
||||
<VChip
|
||||
:color="resolveTicketPriorityColor(ticket.priority)"
|
||||
size="small"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ ticket.priority }}
|
||||
</VChip>
|
||||
</td>
|
||||
<td class="text-body-2">
|
||||
{{ formatRelativeTime(ticket.updated_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<!-- Row 4: Quick Actions -->
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VCard>
|
||||
|
||||
Reference in New Issue
Block a user