95 lines
4.6 KiB
TypeScript
95 lines
4.6 KiB
TypeScript
import { auth } from '@/lib/auth'
|
||
import { supabaseAdmin } from '@/lib/supabaseAdmin'
|
||
import Link from 'next/link'
|
||
|
||
function timeAgo(iso: string) {
|
||
const diffMs = Date.now() - new Date(iso).getTime()
|
||
const mins = Math.floor(diffMs / 60000)
|
||
if (mins < 1) return 'az önce'
|
||
if (mins < 60) return `${mins} dk önce`
|
||
const hours = Math.floor(mins / 60)
|
||
if (hours < 24) return `${hours} sa önce`
|
||
const days = Math.floor(hours / 24)
|
||
return `${days} gün önce`
|
||
}
|
||
|
||
export default async function AdminDashboardPage() {
|
||
const session = await auth()
|
||
|
||
const now = new Date().toISOString()
|
||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
|
||
|
||
const [
|
||
{ count: totalUsers },
|
||
{ count: activeLicenses },
|
||
{ count: trialExpiringSoon },
|
||
{ count: crashes24h },
|
||
{ data: recentCrashes },
|
||
{ data: recentLicenseEvents },
|
||
] = await Promise.all([
|
||
supabaseAdmin.from('profiles').select('id', { count: 'exact', head: true }),
|
||
supabaseAdmin.from('licenses').select('id', { count: 'exact', head: true }).eq('status', 'active'),
|
||
supabaseAdmin.from('profiles').select('id', { count: 'exact', head: true }).eq('license_status', 'trial').gte('trial_end_date', now).lte('trial_end_date', new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString()),
|
||
supabaseAdmin.from('crash_reports').select('id', { count: 'exact', head: true }).gte('created_at', oneDayAgo),
|
||
supabaseAdmin.from('crash_reports').select('id, error_message, severity, app_version, created_at').order('created_at', { ascending: false }).limit(5),
|
||
supabaseAdmin.from('license_events').select('id, event_type, license_id, created_at').order('created_at', { ascending: false }).limit(5),
|
||
])
|
||
|
||
const activity = [
|
||
...(recentCrashes || []).map((c) => ({ id: c.id, text: `Hata: ${c.error_message || c.severity} (v${c.app_version})`, created_at: c.created_at, tone: 'danger' as const })),
|
||
...(recentLicenseEvents || []).map((e) => ({ id: e.id, text: `Lisans olayı: ${e.event_type}`, created_at: e.created_at, tone: 'neutral' as const })),
|
||
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()).slice(0, 8)
|
||
|
||
const stats = [
|
||
{ name: 'Toplam Kullanıcı', stat: totalUsers ?? 0, href: '/admin/users' },
|
||
{ name: 'Aktif Lisans', stat: activeLicenses ?? 0, href: '/admin/licenses' },
|
||
{ name: 'Denemesi 3 Gün İçinde Bitecek', stat: trialExpiringSoon ?? 0, href: '/admin/users' },
|
||
{ name: 'Son 24 Saatte Hata', stat: crashes24h ?? 0, href: '/admin/logs' },
|
||
]
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Dashboard</h2>
|
||
<p className="text-gray-500 dark:text-gray-400 mt-2">
|
||
Hoş geldiniz, {session?.user?.name || session?.user?.email}. İşte AyrisLegal'ın canlı durumu.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||
{stats.map((item) => (
|
||
<Link
|
||
key={item.name}
|
||
href={item.href}
|
||
className="overflow-hidden rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 px-4 py-5 shadow-sm sm:p-6 hover:border-gray-300 dark:hover:border-gray-700 transition-colors"
|
||
>
|
||
<dt className="truncate text-sm font-medium text-gray-500 dark:text-gray-400">{item.name}</dt>
|
||
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
|
||
{item.stat}
|
||
</dd>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
|
||
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm">
|
||
<div className="p-6">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Son Aktiviteler</h3>
|
||
<div className="mt-4 border-t border-gray-100 dark:border-gray-800">
|
||
{activity.length === 0 ? (
|
||
<div className="py-4 text-sm text-gray-500">Henüz aktivite kaydı bulunmuyor.</div>
|
||
) : (
|
||
activity.map((a) => (
|
||
<div key={a.id} className="py-3 border-b border-gray-100 dark:border-gray-800 last:border-0 flex items-center justify-between gap-4">
|
||
<span className={`text-sm ${a.tone === 'danger' ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-gray-300'}`}>{a.text}</span>
|
||
<span className="text-xs text-gray-400 whitespace-nowrap">{timeAgo(a.created_at)}</span>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|