125 lines
6.1 KiB
TypeScript
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>
|
|
)
|
|
}
|