204 lines
9.1 KiB
TypeScript
204 lines
9.1 KiB
TypeScript
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>
|
||
)
|
||
}
|