Files
nextjs-boilerplate/app/[locale]/admin/licenses/page.tsx
T

125 lines
6.1 KiB
TypeScript

import Link from 'next/link'
import { supabaseAdmin, listAuthUsers } from '@/lib/supabaseAdmin'
interface LicenseRow {
id: string
customer_id: string
plan: string
status: string
max_devices: number
expires_at: string | null
created_at: string
}
function statusBadge(status: string) {
const map: Record<string, string> = {
active: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/40 dark:text-green-400 dark:border-green-900',
expired: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/40 dark:text-red-400 dark:border-red-900',
revoked: 'bg-gray-50 text-gray-600 border-gray-200 dark:bg-gray-900 dark:text-gray-400 dark:border-gray-800',
}
const cls = map[status] || 'bg-gray-50 text-gray-600 border-gray-200 dark:bg-gray-900 dark:text-gray-400 dark:border-gray-800'
return <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${cls}`}>{status}</span>
}
function formatDate(iso: string | null) {
if (!iso) return '—'
return new Date(iso).toLocaleDateString('tr-TR', { day: 'numeric', month: 'short', year: 'numeric' })
}
export default async function LicensesPage({ searchParams }: { searchParams: Promise<{ deleted?: string }> }) {
const { deleted } = await searchParams
const [{ data: licenses }, { data: activations }, { data: profiles }, authUsers] = await Promise.all([
supabaseAdmin.from('licenses').select('id, customer_id, plan, status, max_devices, expires_at, created_at').order('created_at', { ascending: false }),
supabaseAdmin.from('license_activations').select('id, license_id, device_name, platform, last_seen_at'),
supabaseAdmin.from('profiles').select('id, full_name'),
listAuthUsers(),
])
const profileNameById = new Map((profiles || []).map((p: { id: string; full_name: string | null }) => [p.id, p.full_name]))
const activationsByLicense = new Map<string, { id: string; device_name: string | null; platform: string | null; last_seen_at: string | null }[]>()
for (const a of activations || []) {
const list = activationsByLicense.get(a.license_id) || []
list.push(a)
activationsByLicense.set(a.license_id, list)
}
const rows = (licenses as LicenseRow[] | null) || []
return (
<div className="space-y-6">
{deleted && (
<div className="rounded-md bg-green-50 dark:bg-green-950/40 border border-green-200 dark:border-green-900 px-4 py-3 text-sm text-green-700 dark:text-green-400">
Lisans silindi.
</div>
)}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Lisanslar</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">Toplam {rows.length} lisans.</p>
</div>
<Link
href="/admin/licenses/new"
className="rounded-md bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium px-4 py-2 transition-colors whitespace-nowrap"
>
+ Yeni Lisans Oluştur
</Link>
</div>
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
{['Müşteri', 'Plan', 'Durum', 'Cihazlar', 'Bitiş', 'Oluşturma', ''].map((h) => (
<th key={h} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
{rows.map((l) => {
const devices = activationsByLicense.get(l.id) || []
const customerName = l.customer_id ? profileNameById.get(l.customer_id) : null
const customerEmail = l.customer_id ? authUsers.get(l.customer_id)?.email : null
return (
<tr key={l.id} className="hover:bg-gray-50 dark:hover:bg-gray-900/50">
<td className="px-4 py-3 whitespace-nowrap">
{l.customer_id ? (
<>
<div className="text-sm font-medium text-gray-900 dark:text-white">{customerName || '(isimsiz)'}</div>
<div className="text-xs text-gray-500">{customerEmail || l.customer_id}</div>
</>
) : (
<span className="text-sm text-gray-400"></span>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">{l.plan}</td>
<td className="px-4 py-3 whitespace-nowrap">{statusBadge(l.status)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{devices.length} / {l.max_devices}
{devices.length > 0 && (
<div className="text-xs text-gray-400 mt-0.5">{devices.map((d) => d.platform).filter(Boolean).join(', ')}</div>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">{formatDate(l.expires_at)}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">{formatDate(l.created_at)}</td>
<td className="px-4 py-3 whitespace-nowrap text-right text-sm">
<Link href={`/admin/licenses/${l.id}`} className="text-indigo-600 dark:text-indigo-400 hover:underline">
Düzenle
</Link>
</td>
</tr>
)
})}
{rows.length === 0 && (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-sm text-gray-500">Henüz lisans yok.</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
)
}