diff --git a/website/app/Http/Controllers/Admin/CustomerController.php b/website/app/Http/Controllers/Admin/CustomerController.php index 6c24216..5c644b7 100644 --- a/website/app/Http/Controllers/Admin/CustomerController.php +++ b/website/app/Http/Controllers/Admin/CustomerController.php @@ -97,10 +97,16 @@ class CustomerController extends Controller ->get(['id', 'user_id', 'number', 'total', 'status', 'gateway', 'created_at']); $auditLogs = AuditLog::query() - ->where('user_id', $user->id) + ->where(function ($q) use ($user): void { + $q->where('user_id', $user->id) + ->orWhere(function ($q2) use ($user): void { + $q2->where('resource_type', 'user') + ->where('resource_id', $user->id); + }); + }) + ->with(['user:id,name,email', 'admin:id,name,email']) ->latest() - ->limit(20) - ->get(['id', 'action', 'resource_type', 'resource_id', 'ip_address', 'created_at']); + ->paginate(15, ['*'], 'audit_page'); return Inertia::render('Admin/Customers/Show', [ 'customer' => $user, diff --git a/website/resources/ts/Pages/Admin/Customers/Show.vue b/website/resources/ts/Pages/Admin/Customers/Show.vue index 82d2865..1e126b6 100644 --- a/website/resources/ts/Pages/Admin/Customers/Show.vue +++ b/website/resources/ts/Pages/Admin/Customers/Show.vue @@ -3,6 +3,7 @@ import { Link, router, useForm } from '@inertiajs/vue3' import { ref } from 'vue' import AdminLayout from '@/Layouts/AdminLayout.vue' import { resolveInvoiceStatusColor, resolveSubscriptionStatusColor } from '@/utils/resolvers' +import type { AuditLog, PaginatedResponse } from '@/types' interface CustomerProfile { billing_address_line1: string | null @@ -71,20 +72,11 @@ interface CustomerInvoice { created_at: string } -interface CustomerAuditLog { - id: number - action: string - resource_type: string - resource_id: number | null - ip_address: string | null - created_at: string -} - interface Props { customer: Customer subscriptions: CustomerSubscription[] recentInvoices: CustomerInvoice[] - auditLogs: CustomerAuditLog[] + auditLogs: PaginatedResponse } defineOptions({ layout: AdminLayout }) @@ -95,6 +87,7 @@ const activeTab = ref('overview') const suspendForm = useForm({}) const unsuspendForm = useForm({}) +const expandedRows = ref>(new Set()) function handleSuspend(): void { suspendForm.post(`/customers/${props.customer.id}/suspend`, { @@ -164,6 +157,122 @@ function formatBillingAddress(profile: CustomerProfile | null): string { ].filter(Boolean) return parts.length > 0 ? parts.join('\n') : 'No billing address on file' } + +function resolveActionColor(action: string): string { + if (action.startsWith('login') || action === 'login') { + return 'info' + } + if (action.includes('impersonate')) { + return 'warning' + } + if (action.startsWith('create') || action === 'register') { + return 'success' + } + if (action.startsWith('update') || action.startsWith('edit') || action.includes('updated') || action.includes('changed')) { + return 'primary' + } + if (action.startsWith('delete') || action.startsWith('terminate') || action.startsWith('destroy')) { + return 'error' + } + if (action.startsWith('suspend')) { + return 'warning' + } + if (action.startsWith('unsuspend')) { + return 'success' + } + return 'secondary' +} + +function resolveActionIcon(action: string): string { + if (action.startsWith('login') || action === 'login') { + return 'tabler-login' + } + if (action.includes('impersonate')) { + return 'tabler-user-shield' + } + if (action.startsWith('create') || action === 'register') { + return 'tabler-plus' + } + if (action.startsWith('update') || action.startsWith('edit') || action.includes('updated') || action.includes('changed')) { + return 'tabler-pencil' + } + if (action.startsWith('delete') || action.startsWith('terminate') || action.startsWith('destroy')) { + return 'tabler-trash' + } + if (action.startsWith('suspend')) { + return 'tabler-ban' + } + if (action.startsWith('unsuspend')) { + return 'tabler-circle-check' + } + return 'tabler-activity' +} + +function formatRelativeTime(dateStr: string): string { + const date = new Date(dateStr) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffSeconds = Math.floor(diffMs / 1000) + const diffMinutes = Math.floor(diffSeconds / 60) + const diffHours = Math.floor(diffMinutes / 60) + const diffDays = Math.floor(diffHours / 24) + + if (diffSeconds < 60) { + 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(dateStr) +} + +function formatDateTime(dateStr: string): string { + const date = new Date(dateStr) + return date.toLocaleDateString('en-US', { + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +function toggleRow(id: number): void { + if (expandedRows.value.has(id)) { + expandedRows.value.delete(id) + } + else { + expandedRows.value.add(id) + } +} + +function isExpanded(id: number): boolean { + return expandedRows.value.has(id) +} + +function hasChanges(log: AuditLog): boolean { + return log.changes !== null && Object.keys(log.changes).length > 0 +} + +function formatJson(changes: Record | null): string { + if (!changes) { + return '{}' + } + return JSON.stringify(changes, null, 2) +} + +function goToAuditPage(page: number): void { + router.get(`/customers/${props.customer.id}`, { audit_page: page }, { + preserveState: true, + preserveScroll: true, + }) +}