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

204 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { supabaseAdmin, listAuthUsers, listCustomerOptions } from '@/lib/supabaseAdmin'
import { updateLicenseAction, removeActivationAction } from '../actions'
import DeleteLicenseButton from './DeleteLicenseButton'
interface LicenseRow {
id: string
customer_id: string | null
plan: string
status: string
max_devices: number
expires_at: string | null
created_at: string
}
interface ActivationRow {
id: string
device_id: string
device_name: string | null
platform: string | null
app_version: string | null
last_seen_at: string | null
}
function toDateInputValue(iso: string | null) {
if (!iso) return ''
return new Date(iso).toISOString().slice(0, 10)
}
function formatDateTime(iso: string | null) {
if (!iso) return '—'
return new Date(iso).toLocaleString('tr-TR', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })
}
export default async function LicenseEditPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ error?: string; success?: string }>
}) {
const { id } = await params
const { error, success } = await searchParams
const [{ data: license }, { data: profiles }, authUsers, customers, { data: activations }] = await Promise.all([
supabaseAdmin.from('licenses').select('id, customer_id, plan, status, max_devices, expires_at, created_at').eq('id', id).maybeSingle(),
supabaseAdmin.from('profiles').select('id, full_name'),
listAuthUsers(),
listCustomerOptions(),
supabaseAdmin.from('license_activations').select('id, device_id, device_name, platform, app_version, last_seen_at').eq('license_id', id).order('last_seen_at', { ascending: false }),
])
if (!license) notFound()
const l = license as LicenseRow
const profileNameById = new Map((profiles || []).map((p: { id: string; full_name: string | null }) => [p.id, p.full_name]))
const currentEmail = l.customer_id ? authUsers.get(l.customer_id)?.email || '' : ''
const currentName = l.customer_id ? profileNameById.get(l.customer_id) : null
return (
<div className="space-y-6 max-w-2xl">
<div>
<Link href="/admin/licenses" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white"> Lisanslar</Link>
<h2 className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white mt-2">Lisans Düzenle</h2>
<p className="text-gray-500 dark:text-gray-400 mt-1 font-mono text-xs">{l.id}</p>
</div>
{error && (
<div className="rounded-md bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-900 px-4 py-3 text-sm text-red-700 dark:text-red-400">
{error}
</div>
)}
{success && (
<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">
Değişiklikler kaydedildi.
</div>
)}
<form action={updateLicenseAction} className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm p-6 space-y-5">
<input type="hidden" name="id" value={l.id} />
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Müşteri</label>
<select
name="customer_email"
defaultValue={currentEmail}
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value=""> Bağlantı yok </option>
{currentEmail && !customers.some((c) => c.email === currentEmail) && (
<option value={currentEmail}>{currentName || '(isimsiz)'} {currentEmail} (profil listede yok)</option>
)}
{customers.map((c) => (
<option key={c.id} value={c.email}>
{c.name || '(isimsiz)'} {c.email}
</option>
))}
</select>
<p className="mt-1 text-xs text-gray-400">
{currentName ? `Şu an: ${currentName} (${currentEmail})` : currentEmail ? `Şu an: ${currentEmail}` : 'Şu an bağlı kullanıcı yok.'}
{' '}Kullanıcılar listesinden seçin. "Bağlantı yok" seçilirse bağlantı kaldırılır.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Plan</label>
<input
type="text"
name="plan"
defaultValue={l.plan}
list="plan-options"
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<datalist id="plan-options">
<option value="trial" />
<option value="starter" />
<option value="professional" />
<option value="enterprise" />
</datalist>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Durum</label>
<select
name="status"
defaultValue={l.status}
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="active">active</option>
<option value="expired">expired</option>
<option value="revoked">revoked</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Maks. Cihaz</label>
<input
type="number"
name="max_devices"
min={1}
defaultValue={l.max_devices}
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Bitiş Tarihi</label>
<input
type="date"
name="expires_at"
defaultValue={toDateInputValue(l.expires_at)}
className="w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
</div>
</div>
<div className="flex items-center gap-3 pt-2">
<button
type="submit"
className="rounded-md bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium px-4 py-2 transition-colors"
>
Kaydet
</button>
<Link href="/admin/licenses" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-white">
Vazgeç
</Link>
</div>
</form>
<div className="rounded-lg bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-800 shadow-sm p-6">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">
Aktif Cihazlar ({(activations as ActivationRow[] | null)?.length || 0} / {l.max_devices})
</h3>
{!activations || activations.length === 0 ? (
<p className="text-sm text-gray-500">Bu lisansta aktive edilmiş cihaz yok.</p>
) : (
<ul className="divide-y divide-gray-100 dark:divide-gray-800">
{(activations as ActivationRow[]).map((a) => (
<li key={a.id} className="py-2.5 flex items-center justify-between gap-4 text-sm">
<div className="min-w-0">
<div className="text-gray-900 dark:text-white truncate">{a.device_name || a.platform || 'Bilinmeyen cihaz'} <span className="text-gray-400 font-normal">· v{a.app_version || '?'}</span></div>
<div className="text-xs text-gray-400 font-mono truncate">{a.device_id} · son görülme {formatDateTime(a.last_seen_at)}</div>
</div>
<form action={removeActivationAction}>
<input type="hidden" name="activation_id" value={a.id} />
<input type="hidden" name="license_id" value={l.id} />
<button type="submit" className="text-red-600 dark:text-red-400 hover:underline whitespace-nowrap">Kaldır</button>
</form>
</li>
))}
</ul>
)}
</div>
<div className="rounded-lg bg-white dark:bg-gray-950 border border-red-200 dark:border-red-900 shadow-sm p-6">
<h3 className="text-sm font-semibold text-red-700 dark:text-red-400 uppercase tracking-wide mb-3">Tehlikeli Bölge</h3>
<DeleteLicenseButton id={l.id} />
</div>
</div>
)
}