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:
Claude Dev
2026-02-09 20:24:09 -05:00
parent 0cdfc986b8
commit cbc706d934
2 changed files with 129 additions and 3 deletions

View File

@@ -48,6 +48,12 @@ class DashboardController extends Controller
->whereIn('status', ['open', 'in_progress', 'waiting']) ->whereIn('status', ['open', 'in_progress', 'waiting'])
->count(); ->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', [ return Inertia::render('Dashboard', [
'activeServicesCount' => $activeServicesCount, 'activeServicesCount' => $activeServicesCount,
'activeSubscriptionsCount' => $activeSubscriptionsCount, 'activeSubscriptionsCount' => $activeSubscriptionsCount,
@@ -56,6 +62,7 @@ class DashboardController extends Controller
'pendingInvoicesAmount' => number_format((float) $pendingInvoicesAmount, 2, '.', ''), 'pendingInvoicesAmount' => number_format((float) $pendingInvoicesAmount, 2, '.', ''),
'nextRenewalDate' => $nextRenewalDate, 'nextRenewalDate' => $nextRenewalDate,
'openTicketsCount' => $openTicketsCount, 'openTicketsCount' => $openTicketsCount,
'recentTickets' => $recentTickets,
]); ]);
} }
} }

View File

@@ -3,8 +3,8 @@ import { Link } from '@inertiajs/vue3'
import { computed } from 'vue' import { computed } from 'vue'
import AccountLayout from '@/Layouts/AccountLayout.vue' import AccountLayout from '@/Layouts/AccountLayout.vue'
import StatCard from '@/Components/StatCard.vue' import StatCard from '@/Components/StatCard.vue'
import { resolveSubscriptionStatusColor, resolveInvoiceStatusColor, formatPrice } from '@/utils/resolvers' import { resolveSubscriptionStatusColor, resolveInvoiceStatusColor, resolveTicketStatusColor, resolveTicketPriorityColor, formatPrice } from '@/utils/resolvers'
import type { Subscription, Invoice } from '@/types' import type { Subscription, Invoice, SupportTicket } from '@/types'
interface Props { interface Props {
activeServicesCount: number activeServicesCount: number
@@ -14,6 +14,7 @@ interface Props {
pendingInvoicesAmount: string pendingInvoicesAmount: string
nextRenewalDate: string | null nextRenewalDate: string | null
openTicketsCount: number openTicketsCount: number
recentTickets: SupportTicket[]
} }
defineOptions({ layout: AccountLayout }) defineOptions({ layout: AccountLayout })
@@ -40,6 +41,33 @@ const formattedNextRenewal = computed<string>(() => {
return formatDate(props.nextRenewalDate) 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[]>(() => { const unpaidInvoices = computed<Invoice[]>(() => {
return props.latestInvoices.filter( return props.latestInvoices.filter(
(invoice: Invoice) => invoice.status === 'pending' || invoice.status === 'overdue', (invoice: Invoice) => invoice.status === 'pending' || invoice.status === 'overdue',
@@ -324,7 +352,98 @@ const unpaidInvoices = computed<Invoice[]>(() => {
</VCol> </VCol>
</VRow> </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> <VRow>
<VCol cols="12"> <VCol cols="12">
<VCard> <VCard>