feat: implement merchant dashboard, secure auth, and short_id system
- Added dedicated merchant dashboard with analytics and transactions - Implemented API Key based authentication for merchants - Introduced 8-character Short IDs for merchants to use in URLs - Refactored checkout and payment intent APIs to support multi-gateway - Enhanced Landing Page with Merchant Portal access and marketing copy - Fixed Next.js 15 async params build issues - Updated internal branding to P2CGateway - Added AyrisTech credits to footer
This commit is contained in:
159
app/merchant/[id]/(dashboard)/integration/page.tsx
Normal file
159
app/merchant/[id]/(dashboard)/integration/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import {
|
||||
Terminal,
|
||||
Copy,
|
||||
Check,
|
||||
Globe,
|
||||
Webhook,
|
||||
Zap,
|
||||
ShieldCheck,
|
||||
Code2
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
async function getMerchant(identifier: string) {
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||
|
||||
const query = supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('*');
|
||||
|
||||
if (isUUID) {
|
||||
query.eq('id', identifier);
|
||||
} else {
|
||||
query.eq('short_id', identifier);
|
||||
}
|
||||
|
||||
const { data, error } = await query.single();
|
||||
return data;
|
||||
}
|
||||
|
||||
export default async function MerchantIntegrationPage(props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const resolvedParams = await props.params;
|
||||
const identifier = resolvedParams.id;
|
||||
const merchant = await getMerchant(identifier);
|
||||
const cookieStore = await cookies();
|
||||
|
||||
if (!merchant) return null;
|
||||
|
||||
if (!cookieStore.get(`merchant_auth_${merchant.id}`)) {
|
||||
redirect(`/merchant/${identifier}/login`);
|
||||
}
|
||||
|
||||
const checkoutUrl = `https://p2cgateway.com/checkout?merchant_id=${merchant.short_id || merchant.id}&amount=100¤cy=TRY&ref_id=SİPARİŞ_123&callback_url=https://siteniz.com/basarili`;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-20">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Teknik Entegrasyon</h1>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-2 px-1">Ödeme sistemini sitenize nasıl bağlarsınız?</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Start Card */}
|
||||
<div className="bg-gray-900 rounded-[40px] p-12 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center">
|
||||
<Zap size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Hızlı Ödeme Linki</h2>
|
||||
</div>
|
||||
<p className="text-gray-400 text-lg leading-relaxed font-medium">
|
||||
Entegrasyonun en basit yolu, müşterilerinizi aşağıdaki URL yapısını kullanarak ödeme sayfasına yönlendirmektir.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/5 p-6 rounded-3xl border border-white/10 space-y-4">
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest">Sizin Hazır Linkiniz</p>
|
||||
<div className="bg-black p-4 rounded-xl border border-white/5 font-mono text-[10px] text-blue-400 break-all leading-relaxed">
|
||||
{checkoutUrl}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Credentials */}
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
|
||||
<ShieldCheck size={24} />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-gray-900">Kimlik Bilgileri</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Merchant ID (Firma Kimliği)</label>
|
||||
<div className="flex items-center justify-between gap-3 bg-white p-4 rounded-xl border border-gray-200">
|
||||
<code className="text-xs font-mono font-bold text-gray-600 truncate">{merchant.id}</code>
|
||||
<Copy size={14} className="text-gray-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">API Secret Key</label>
|
||||
<div className="flex items-center justify-between gap-3 bg-white p-4 rounded-xl border border-gray-200">
|
||||
<code className="text-xs font-mono font-bold text-gray-600">••••••••••••••••••••••••</code>
|
||||
<button className="text-[10px] font-black text-blue-600 uppercase">Göster</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhooks */}
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-purple-50 rounded-2xl flex items-center justify-center text-purple-600">
|
||||
<Webhook size={24} />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-gray-900">Webhook Yapılandırması</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500 font-medium leading-relaxed">
|
||||
Ödeme başarılı olduğunda sistemimiz belirtilen adrese bir POST isteği gönderir.
|
||||
</p>
|
||||
|
||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Mevcut Webhook URL</span>
|
||||
<span className={`text-[10px] font-black px-2 py-0.5 rounded-md ${merchant.webhook_url ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600'}`}>
|
||||
{merchant.webhook_url ? 'AKTİF' : 'AYARLANMAMIŞ'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl border border-gray-200">
|
||||
<code className="text-xs font-bold text-gray-600 truncate block">
|
||||
{merchant.webhook_url || 'https://henuz-bir-adres-tanimlanmadi.com'}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase text-center mt-2 leading-relaxed">
|
||||
Webook URL adresinizi değiştirmek için <br /> destek ekibi ile iletişime geçin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ title: 'API Referansı', icon: Code2, color: 'blue' },
|
||||
{ title: 'Hazır Kütüphaneler', icon: Terminal, color: 'emerald' },
|
||||
{ title: 'Teknik Destek', icon: Globe, color: 'purple' },
|
||||
].map((r) => (
|
||||
<div key={r.title} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-6 hover:border-gray-300 transition-colors cursor-pointer group">
|
||||
<div className={`w-12 h-12 bg-${r.color}-50 rounded-2xl flex items-center justify-center text-${r.color}-600 group-hover:bg-${r.color}-600 group-hover:text-white transition-all`}>
|
||||
<r.icon size={22} />
|
||||
</div>
|
||||
<span className="text-sm font-black text-gray-900 leading-tight">{r.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
app/merchant/[id]/(dashboard)/layout.tsx
Normal file
100
app/merchant/[id]/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import {
|
||||
ExternalLink,
|
||||
Building2,
|
||||
ShieldCheck
|
||||
} from 'lucide-react';
|
||||
import MerchantSidebar from '@/components/merchant/MerchantSidebar';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
|
||||
export default async function MerchantLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const resolvedParams = await params;
|
||||
const identifier = resolvedParams.id;
|
||||
const cookieStore = await cookies();
|
||||
|
||||
// 1. Resolve actual merchant ID
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||
let resolvedId = identifier;
|
||||
|
||||
if (!isUUID) {
|
||||
const { data: merchant } = await supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('id')
|
||||
.eq('short_id', identifier)
|
||||
.single();
|
||||
if (merchant) {
|
||||
resolvedId = merchant.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Auth Check
|
||||
const isAuth = cookieStore.get(`merchant_auth_${resolvedId}`);
|
||||
const isShortAuth = cookieStore.get(`merchant_auth_${identifier}`);
|
||||
|
||||
// If visiting login page, don't check auth or redirect loop
|
||||
// But layout handles all subpages. Subpage specific logic?
|
||||
// In Next.js, layout can't easily know the current sub-segment without hooks.
|
||||
// However, the login page itself has its own layout or is a sibling?
|
||||
// If the login page is AT /merchant/[id]/login, it is INSIDE this layout.
|
||||
// We should allow the login page to show.
|
||||
|
||||
// Note: To truly exclude the login page from this check in a layout,
|
||||
// we would usually check the URL, but Server Components layout don't have URL.
|
||||
// A better approach is to use Middleware for redirs or just check in page.tsx.
|
||||
|
||||
// For now, let's keep it simple:
|
||||
// Since I can't check URL here easily, I will implement the check in the individual pages
|
||||
// or just assume this layout is only for protected pages.
|
||||
// Wait, let's make the login page NOT use this layout if possible.
|
||||
|
||||
// Actually, I'll just check if auth exists. If not, the pages will handle redirection
|
||||
// or we can use a "RequireAuth" wrapper.
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#F8FAFC]">
|
||||
{/* Sidebar */}
|
||||
<MerchantSidebar merchantId={identifier} />
|
||||
|
||||
{/* Main Container */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Top Bar */}
|
||||
<header className="h-24 bg-white border-b border-gray-100 flex items-center justify-between px-10">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-xl font-black text-gray-900 tracking-tight uppercase">Dashboard</h2>
|
||||
<div className="h-6 w-px bg-gray-100 mx-2"></div>
|
||||
<span className="px-3 py-1 bg-blue-50 text-blue-600 rounded-lg text-[10px] font-black uppercase tracking-widest border border-blue-100">
|
||||
{identifier}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-emerald-50 rounded-xl border border-emerald-100">
|
||||
<ShieldCheck size={14} className="text-emerald-600" />
|
||||
<span className="text-[10px] font-black text-emerald-600 uppercase tracking-widest">Güvenli Oturum</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-gray-100"></div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gray-900 flex items-center justify-center text-white font-bold uppercase tracking-tighter shadow-lg shadow-gray-200">
|
||||
{identifier.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content Area */}
|
||||
<main className="flex-1 overflow-y-auto p-10">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
255
app/merchant/[id]/(dashboard)/page.tsx
Normal file
255
app/merchant/[id]/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Users,
|
||||
Wallet,
|
||||
CheckCircle2,
|
||||
Calendar,
|
||||
ArrowUpRight,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import TransactionChart from '@/components/admin/TransactionChart';
|
||||
|
||||
async function getMerchantData(identifier: string) {
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||
|
||||
// Fetch merchant details
|
||||
const query = supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('*');
|
||||
|
||||
if (isUUID) {
|
||||
query.eq('id', identifier);
|
||||
} else {
|
||||
query.eq('short_id', identifier);
|
||||
}
|
||||
|
||||
const { data: merchant, error: mError } = await query.single();
|
||||
|
||||
if (mError || !merchant) return null;
|
||||
|
||||
const id = merchant.id; // Always use UUID for internal lookups
|
||||
|
||||
// Fetch merchant transactions
|
||||
const { data: transactions, error: tError } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.eq('merchant_id', id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (tError) return null;
|
||||
|
||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||
const successfulCount = successfulTransactions.length;
|
||||
const totalCount = transactions.length;
|
||||
const successRate = totalCount > 0 ? (successfulCount / totalCount) * 100 : 0;
|
||||
|
||||
// Last 30 days chart data
|
||||
const chartData = Array.from({ length: 30 }, (_, i) => {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() - (29 - i));
|
||||
return {
|
||||
date: d.toISOString().split('T')[0],
|
||||
displayDate: format(d, 'd MMM', { locale: tr }),
|
||||
amount: 0
|
||||
};
|
||||
});
|
||||
|
||||
successfulTransactions.forEach(t => {
|
||||
const dateStr = new Date(t.created_at).toISOString().split('T')[0];
|
||||
const dayMatch = chartData.find(d => d.date === dateStr);
|
||||
if (dayMatch) {
|
||||
dayMatch.amount += Number(t.amount);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
merchant,
|
||||
transactions,
|
||||
totalRevenue,
|
||||
successfulCount,
|
||||
successRate,
|
||||
totalCount,
|
||||
chartData
|
||||
};
|
||||
}
|
||||
|
||||
export default async function MerchantDashboardPage(props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const resolvedParams = await props.params;
|
||||
const identifier = resolvedParams.id;
|
||||
const data = await getMerchantData(identifier);
|
||||
const cookieStore = await cookies();
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-[60vh] text-center space-y-4">
|
||||
<div className="w-20 h-20 bg-red-50 rounded-[32px] flex items-center justify-center text-red-500 mb-4">
|
||||
<Search size={40} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-gray-900 uppercase tracking-tight">Firma Bulunamadı</h1>
|
||||
<p className="text-gray-400 font-bold uppercase tracking-widest text-xs max-w-xs leading-relaxed">
|
||||
Erişmeye çalıştığınız firma ID'si geçersiz veya yetkiniz yok.
|
||||
</p>
|
||||
<Link href="/" className="px-8 py-3 bg-gray-900 text-white rounded-2xl text-xs font-black uppercase tracking-widest">Geri Dön</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check Authentication
|
||||
const isAuth = cookieStore.get(`merchant_auth_${data.merchant.id}`) || cookieStore.get(`merchant_auth_${identifier}`);
|
||||
if (!isAuth) {
|
||||
redirect(`/merchant/${identifier}/login`);
|
||||
}
|
||||
|
||||
const { merchant, transactions, totalRevenue, successfulCount, successRate, totalCount, chartData } = data;
|
||||
const recentTransactions = transactions.slice(0, 8);
|
||||
|
||||
return (
|
||||
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-20">
|
||||
{/* Merchant Info Header */}
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-8">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-20 h-20 bg-blue-600 rounded-[28px] flex items-center justify-center text-white font-black text-2xl shadow-xl shadow-blue-100">
|
||||
{merchant.name.substring(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-gray-900 tracking-tight">{merchant.name}</h1>
|
||||
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Hoş Geldiniz, İşlemlerinizi Buradan Takip Edebilirsiniz</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="px-6 py-4 bg-gray-50 rounded-2xl border border-gray-100">
|
||||
<p className="text-[9px] text-gray-400 font-black uppercase tracking-[0.2em] mb-1 text-center">Durum</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<span className="text-xs font-black text-gray-900 uppercase">Aktif</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-6 group hover:border-blue-500 transition-colors">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Toplam Ciro</p>
|
||||
<div className="p-3 bg-blue-50 rounded-xl text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||
<Wallet size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-black text-gray-900">{totalRevenue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })} <span className="text-lg">₺</span></h3>
|
||||
<p className="text-[10px] text-emerald-500 font-black uppercase tracking-tighter mt-3 flex items-center gap-1">
|
||||
<TrendingUp size={14} /> Başarılı İşlemlerden Gelen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">İşlem Sayısı</p>
|
||||
<div className="p-3 bg-emerald-50 rounded-xl text-emerald-600">
|
||||
<CheckCircle2 size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-black text-gray-900">{successfulCount} <span className="text-lg text-gray-300">/ {totalCount}</span></h3>
|
||||
<p className="text-[10px] text-gray-400 font-black uppercase tracking-tighter mt-3 flex items-center gap-1">
|
||||
Ödeme Girişi Denemesi
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Başarı Oranı</p>
|
||||
<div className="p-3 bg-orange-50 rounded-xl text-orange-600">
|
||||
<ArrowUpRight size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-4xl font-black text-gray-900">%{successRate.toFixed(1)}</h3>
|
||||
<div className="w-full bg-gray-50 h-2 rounded-full mt-4 overflow-hidden">
|
||||
<div className="bg-orange-500 h-full rounded-full transition-all" style={{ width: `${successRate}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm">
|
||||
<div className="mb-10">
|
||||
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Günlük Gelir Grafiği</h3>
|
||||
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Son 30 günlük işlem hacmi</p>
|
||||
</div>
|
||||
<TransactionChart data={chartData} />
|
||||
</div>
|
||||
|
||||
{/* Recent Transactions Table */}
|
||||
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden overflow-x-auto">
|
||||
<div className="p-8 border-b border-gray-50 flex justify-between items-center px-10">
|
||||
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tight">Son İşlemler</h2>
|
||||
<Link href={`/merchant/${identifier}/transactions`} className="text-xs font-black text-blue-600 uppercase tracking-widest hover:underline">
|
||||
Bütün İşlemleri Gör
|
||||
</Link>
|
||||
</div>
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-gray-50/50 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
|
||||
<th className="px-10 py-6">İşlem No</th>
|
||||
<th className="px-10 py-6">Referans / Müşteri</th>
|
||||
<th className="px-10 py-6">Tarih</th>
|
||||
<th className="px-10 py-6 text-right">Tutar</th>
|
||||
<th className="px-10 py-6 text-center">Durum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{recentTransactions.map((t) => (
|
||||
<tr key={t.id} className="group hover:bg-gray-50/50 transition-colors">
|
||||
<td className="px-10 py-8">
|
||||
<span className="text-xs font-black text-gray-900 font-mono">#{t.stripe_pi_id?.slice(-8).toUpperCase() || 'EXT-' + t.id.slice(0, 4)}</span>
|
||||
</td>
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-black text-gray-900 uppercase">{t.customer_name || t.source_ref_id || 'SİSTEM'}</span>
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase mt-1">{t.customer_phone || 'İletişim Yok'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8 text-xs font-bold text-gray-400 uppercase">
|
||||
{format(new Date(t.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })}
|
||||
</td>
|
||||
<td className="px-10 py-8 text-right font-black text-gray-900 text-sm">
|
||||
{Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })} ₺
|
||||
</td>
|
||||
<td className="px-10 py-8 text-center">
|
||||
<span className={`inline-flex px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${t.status === 'succeeded' ? 'bg-emerald-50 text-emerald-600' :
|
||||
t.status === 'failed' ? 'bg-red-50 text-red-600' : 'bg-orange-50 text-orange-600'
|
||||
}`}>
|
||||
{t.status === 'succeeded' ? 'Başarılı' : t.status === 'failed' ? 'Hatalı' : 'Bekliyor'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{recentTransactions.length === 0 && (
|
||||
<div className="p-20 text-center space-y-4">
|
||||
<Wallet className="w-12 h-12 text-gray-200 mx-auto" />
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Henüz bir işlem bulunmuyor</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
app/merchant/[id]/(dashboard)/transactions/page.tsx
Normal file
150
app/merchant/[id]/(dashboard)/transactions/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { format } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
import {
|
||||
CreditCard,
|
||||
Search,
|
||||
Filter,
|
||||
Download,
|
||||
Calendar
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
async function getMerchantTransactions(identifier: string) {
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||
|
||||
const query = supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('id')
|
||||
|
||||
if (isUUID) {
|
||||
query.eq('id', identifier);
|
||||
} else {
|
||||
query.eq('short_id', identifier);
|
||||
}
|
||||
|
||||
const { data: merchant } = await query.single();
|
||||
if (!merchant) return null;
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.eq('merchant_id', merchant.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
export default async function MerchantTransactionsPage(props: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const resolvedParams = await props.params;
|
||||
const identifier = resolvedParams.id;
|
||||
const transactions = await getMerchantTransactions(identifier);
|
||||
const cookieStore = await cookies();
|
||||
|
||||
if (!transactions) return <div className="p-10 text-gray-400 font-black uppercase text-xs animate-pulse">İşlemler yükleniyor...</div>;
|
||||
|
||||
// Resolve merchant UUID briefly for auth check
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
|
||||
let resolvedId = identifier;
|
||||
if (!isUUID) {
|
||||
const { data: merchant } = await supabaseAdmin.from('merchants').select('id').eq('short_id', identifier).single();
|
||||
if (merchant) resolvedId = merchant.id;
|
||||
}
|
||||
|
||||
if (!cookieStore.get(`merchant_auth_${resolvedId}`)) {
|
||||
redirect(`/merchant/${identifier}/login`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-20">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
|
||||
<CreditCard size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-gray-900">İşlem Listesi</h2>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-0.5">Tüm ödeme hareketleri</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button className="flex items-center gap-2 px-6 py-3 bg-gray-50 text-gray-600 rounded-xl text-xs font-bold hover:bg-gray-100 transition">
|
||||
<Download size={16} />
|
||||
Dışa Aktar (.CSV)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-gray-50/50 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
|
||||
<th className="px-10 py-6">İşlem No</th>
|
||||
<th className="px-10 py-6">Müşteri / Referans</th>
|
||||
<th className="px-10 py-6">Tarih</th>
|
||||
<th className="px-10 py-6 text-right">Tutar</th>
|
||||
<th className="px-10 py-6 text-center">Durum</th>
|
||||
<th className="px-10 py-6 text-right">Gateway</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{transactions.map((t) => (
|
||||
<tr key={t.id} className="group hover:bg-gray-50/50 transition-colors">
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-black text-gray-900 font-mono">#{t.stripe_pi_id?.slice(-8).toUpperCase() || 'EXT-' + t.id.slice(0, 4)}</span>
|
||||
<span className="text-[9px] text-gray-300 font-bold uppercase mt-1">{t.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-black text-gray-900 uppercase">{t.customer_name || t.source_ref_id || 'SİSTEM'}</span>
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase mt-1">{t.customer_phone || 'İletişim Yok'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8 text-xs font-bold text-gray-400 uppercase">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar size={12} />
|
||||
{format(new Date(t.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8 text-right font-black text-gray-900 text-sm">
|
||||
{Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })} ₺
|
||||
</td>
|
||||
<td className="px-10 py-8 text-center">
|
||||
<span className={`inline-flex px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${t.status === 'succeeded' ? 'bg-emerald-50 text-emerald-600' :
|
||||
t.status === 'failed' ? 'bg-red-50 text-red-600' : 'bg-orange-50 text-orange-600'
|
||||
}`}>
|
||||
{t.status === 'succeeded' ? 'Başarılı' : t.status === 'failed' ? 'Hatalı' : 'Bekliyor'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-10 py-8 text-right">
|
||||
<span className="text-[9px] font-black text-blue-600 bg-blue-50 px-2 py-1 rounded-md uppercase tracking-widest">
|
||||
{t.provider || 'STRIPE'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{transactions.length === 0 && (
|
||||
<div className="p-20 text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-gray-50 rounded-2xl flex items-center justify-center text-gray-200 mx-auto">
|
||||
<CreditCard size={32} />
|
||||
</div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Henüz bir işlem kaydedilmemiş</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
app/merchant/[id]/login/page.tsx
Normal file
103
app/merchant/[id]/login/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { ShieldCheck, ArrowRight, Lock, Building2 } from 'lucide-react';
|
||||
|
||||
export default function MerchantLoginPage() {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// We use an API route to verify the key and set a cookie
|
||||
const response = await fetch(`/api/merchants/auth`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier: id, apiKey })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
router.push(`/merchant/${id}`);
|
||||
router.refresh();
|
||||
} else {
|
||||
setError(data.error || 'Geçersiz anahtar.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Bir hata oluştu. Lütfen tekrar deneyin.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col items-center justify-center p-6">
|
||||
<div className="w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-20 h-20 bg-blue-600 rounded-[32px] flex items-center justify-center text-white mx-auto shadow-2xl shadow-blue-100 mb-6">
|
||||
<Building2 size={32} />
|
||||
</div>
|
||||
<h1 className="text-3xl font-black text-gray-900 tracking-tight uppercase">Firma Girişi</h1>
|
||||
<p className="text-gray-400 font-bold uppercase tracking-widest text-[10px]">P2CGateway Güvenli Erişim Paneli</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-xl shadow-gray-200/50 space-y-8">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-black text-gray-900 leading-tight">Yönetim anahtarınızı girin</p>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Size özel tanımlanan API Secret Key ile giriş yapın.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={18} />
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="••••••••••••••••"
|
||||
className="w-full pl-14 pr-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-mono font-bold text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="text-[10px] text-red-500 font-black uppercase tracking-widest pl-2">{error}</p>}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-5 bg-gray-900 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:bg-black transition shadow-xl disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{isLoading ? 'Doğrulanıyor...' : 'Paneli Aç'}
|
||||
{!isLoading && <ArrowRight size={16} />}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-4">
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="text-[10px] font-black text-gray-400 uppercase tracking-widest hover:text-gray-900 transition"
|
||||
>
|
||||
Ana Sayfaya Dön
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-20 flex items-center gap-3 px-6 py-2 bg-blue-50 rounded-full border border-blue-100">
|
||||
<ShieldCheck size={14} className="text-blue-600" />
|
||||
<span className="text-[9px] font-black text-blue-600 uppercase tracking-widest">SSL 256-bit Uçtan Uca Şifreleme</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user