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'])
|
->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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user