"use client"; import { useCallback, useEffect, useState } from "react"; const card = "rounded-xl border border-[#1e2235] bg-[#0f1120]"; const btn = "rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50"; const btnGhost = "rounded-lg border border-[#262b40] px-3 py-1.5 text-xs font-medium text-gray-300 hover:border-indigo-500"; const inp = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"; const lbl = "mb-1 block text-xs font-medium text-gray-400"; interface ClientApp { id: string; name: string; slug: string; api_key: string; secret?: string; zarinpal_merchant_id?: string | null; zarinpal_sandbox?: boolean | null; allowed_return_origins: string[]; webhook_url?: string | null; is_active: boolean; created_at: string; } interface Txn { id: string; client_slug?: string; status: string; amount_rial: number; currency: string; client_ref?: string | null; ref_id?: string | null; created_at: string; } const toman = (rial: number) => (rial / 10).toLocaleString("fa-IR") + " تومان"; function statusBadge(s: string) { const map: Record = { Paid: "bg-emerald-500/15 text-emerald-300", Pending: "bg-amber-500/15 text-amber-300", Created: "bg-amber-500/15 text-amber-300", Failed: "bg-red-500/15 text-red-300", Cancelled: "bg-red-500/15 text-red-300", Expired: "bg-gray-500/15 text-gray-300", }; return map[s] ?? "bg-gray-500/15 text-gray-300"; } export function PaymentsAdmin() { const [tab, setTab] = useState<"apps" | "txns">("apps"); return (

درگاه پرداخت (ZarinPal)

سرویس پرداخت مشترک روی pay.flatrender.ir. هر سایت (فلت‌رندر، میزی، برگ وصلت) یک اپلیکیشن با کلید اختصاصی می‌گیرد و پرداخت‌ها را از این درگاه عبور می‌دهد.

{tab === "apps" ? : }
); } // ── Client apps tab ──────────────────────────────────────────────────────────── function ClientApps() { const [clients, setClients] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [creating, setCreating] = useState(false); const [revealed, setRevealed] = useState(null); const reload = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch("/api/admin/pay/clients", { cache: "no-store" }); const data = await res.json(); if (!res.ok) throw new Error(data?.error ?? "بارگذاری ناموفق بود"); setClients(Array.isArray(data?.data) ? data.data : []); } catch (e) { setError(e instanceof Error ? e.message : "بارگذاری ناموفق بود"); } finally { setLoading(false); } }, []); useEffect(() => { reload(); }, [reload]); const rotate = async (id: string) => { if (!confirm("کلید مخفی جدید ساخته شود؟ کلید قبلی باطل می‌شود.")) return; const res = await fetch(`/api/admin/pay/clients/${id}/rotate-secret`, { method: "POST" }); const data = await res.json(); if (res.ok) setRevealed(data); else setError(data?.error ?? "خطا"); }; const remove = async (id: string) => { if (!confirm("این اپلیکیشن حذف شود؟")) return; const res = await fetch(`/api/admin/pay/clients/${id}`, { method: "DELETE" }); if (res.ok || res.status === 204) reload(); else setError("حذف ناموفق بود (ممکن است تراکنش داشته باشد)"); }; const toggleActive = async (c: ClientApp) => { await fetch(`/api/admin/pay/clients/${c.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: c.name, zarinpal_merchant_id: c.zarinpal_merchant_id, zarinpal_sandbox: c.zarinpal_sandbox, allowed_return_origins: c.allowed_return_origins, webhook_url: c.webhook_url, is_active: !c.is_active, }), }); reload(); }; return (
{error &&

{error}

} {creating && ( { setCreating(false); setRevealed(c); reload(); }} onError={setError} /> )} {revealed && setRevealed(null)} />} {loading ? (

در حال بارگذاری…

) : clients.length === 0 ? (

هنوز اپلیکیشنی ثبت نشده است.

) : (
{clients.map((c) => (
{c.name} {c.slug} {c.zarinpal_sandbox && ( sandbox )} {!c.is_active && ( غیرفعال )}
{c.api_key}
{c.webhook_url && (

webhook: {c.webhook_url}

)} {c.allowed_return_origins?.length > 0 && (

origins: {c.allowed_return_origins.join(", ")}

)}
))}
)}
); } function CreateClientForm({ onCreated, onError, }: { onCreated: (c: ClientApp) => void; onError: (m: string) => void; }) { const [form, setForm] = useState({ name: "", slug: "", zarinpal_merchant_id: "", zarinpal_sandbox: false, allowed_return_origins: "", webhook_url: "", }); const [saving, setSaving] = useState(false); const submit = async () => { setSaving(true); try { const res = await fetch("/api/admin/pay/clients", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: form.name, slug: form.slug || undefined, zarinpal_merchant_id: form.zarinpal_merchant_id || null, zarinpal_sandbox: form.zarinpal_sandbox || null, allowed_return_origins: form.allowed_return_origins .split(/[\n,]/) .map((s) => s.trim()) .filter(Boolean), webhook_url: form.webhook_url || null, }), }); const data = await res.json(); if (!res.ok) throw new Error(data?.error ?? "ساخت ناموفق بود"); onCreated(data); } catch (e) { onError(e instanceof Error ? e.message : "ساخت ناموفق بود"); } finally { setSaving(false); } }; return (
setForm({ ...form, name: e.target.value })} placeholder="meezi.ir" />
setForm({ ...form, slug: e.target.value })} placeholder="meezi" />
setForm({ ...form, zarinpal_merchant_id: e.target.value })} placeholder="خالی = مرچنت مشترک" />
setForm({ ...form, webhook_url: e.target.value })} placeholder="https://meezi.ir/api/flatpay/webhook" />