346 lines
19 KiB
TypeScript
346 lines
19 KiB
TypeScript
import React from 'react';
|
||
import { db } from '@/lib/db';
|
||
import {
|
||
TrendingUp,
|
||
TrendingDown,
|
||
Users,
|
||
Wallet,
|
||
CheckCircle2,
|
||
Calendar,
|
||
ArrowUpRight,
|
||
Search,
|
||
ShieldCheck
|
||
} 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 mQueryText = isUUID
|
||
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
|
||
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
|
||
|
||
const mResult = await db.query(mQueryText, [identifier]);
|
||
const merchant = mResult.rows[0];
|
||
|
||
if (!merchant) return null;
|
||
|
||
const id = merchant.id; // Always use UUID for internal lookups
|
||
|
||
// Fetch merchant transactions
|
||
const tResult = await db.query(
|
||
'SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC',
|
||
[id]
|
||
);
|
||
const transactions = tResult.rows;
|
||
|
||
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);
|
||
}
|
||
});
|
||
|
||
// Fetch merchant balances
|
||
const bResult = await db.query(
|
||
'SELECT network, token, balance, withdrawn FROM merchant_balances WHERE merchant_id = $1',
|
||
[id]
|
||
);
|
||
const balances = bResult.rows.map(r => ({
|
||
network: r.network,
|
||
token: r.token,
|
||
amount: parseFloat(r.balance) - parseFloat(r.withdrawn)
|
||
}));
|
||
|
||
return {
|
||
merchant,
|
||
transactions,
|
||
totalRevenue,
|
||
successfulCount,
|
||
successRate,
|
||
totalCount,
|
||
chartData,
|
||
balances
|
||
};
|
||
}
|
||
|
||
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, balances } = 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>
|
||
<div className="flex items-center gap-3 mt-1">
|
||
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest">Firma Yönetim Paneli</p>
|
||
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-600 text-[9px] font-black rounded-lg border border-emerald-100 uppercase tracking-tight">
|
||
Komisyon: %{merchant.fee_percent || '1.0'}
|
||
</span>
|
||
</div>
|
||
</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>
|
||
|
||
{/* Balances & Vaults Section */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
{/* Crypto Balances */}
|
||
<div className="lg:col-span-1 bg-gray-900 rounded-[40px] p-8 shadow-2xl relative overflow-hidden flex flex-col justify-between">
|
||
<div className="relative z-10">
|
||
<div className="flex items-center gap-3 mb-6">
|
||
<div className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center text-emerald-400">
|
||
<Wallet size={20} />
|
||
</div>
|
||
<h3 className="text-lg font-black text-white uppercase tracking-tight">Mevcut Bakiyeleriniz</h3>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{balances && balances.length > 0 ? balances.map((b: any, i: number) => (
|
||
<div key={i} className="flex items-center justify-between p-4 bg-white/5 rounded-2xl border border-white/5 group hover:bg-white/10 transition-colors">
|
||
<div className="flex items-center gap-3">
|
||
<div className={`w-2 h-2 rounded-full ${b.network === 'SOLANA' ? 'bg-emerald-400' : b.network === 'POLYGON' ? 'bg-purple-400' : b.network === 'TRON' ? 'bg-red-400' : 'bg-orange-400'}`}></div>
|
||
<div>
|
||
<p className="text-[9px] font-black text-white/40 uppercase tracking-widest">{b.network}</p>
|
||
<p className="text-sm font-black text-white uppercase">{b.token}</p>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="text-lg font-black text-white tabular-nums">{b.amount.toFixed(4)}</p>
|
||
<p className="text-[9px] font-black text-emerald-400 uppercase tracking-tighter">Çekilebilir</p>
|
||
</div>
|
||
</div>
|
||
)) : (
|
||
<div className="py-10 text-center">
|
||
<p className="text-xs font-bold text-white/20 uppercase tracking-widest">Henüz birikmiş bakiye yok</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 pt-6 border-t border-white/10 relative z-10">
|
||
<p className="text-[11px] font-black text-gray-500 uppercase tracking-widest mb-2">Dönüşüm Özeti</p>
|
||
<h4 className="text-3xl font-black text-emerald-400">
|
||
{totalRevenue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })} <span className="text-base">₺</span>
|
||
</h4>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Vault Addresses */}
|
||
<div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-8">
|
||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex flex-col justify-between group hover:border-blue-500 transition-all">
|
||
<div>
|
||
<div className="w-16 h-16 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600 mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||
<ShieldCheck size={28} />
|
||
</div>
|
||
<h3 className="text-xl font-black text-gray-900 mb-2">EVM Kasanız</h3>
|
||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mb-6">Polygon / BSC / Ethereum Ödemeleri İçin</p>
|
||
</div>
|
||
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-100">
|
||
<p className="text-[9px] font-black text-gray-400 uppercase tracking-widest mb-2">Adres</p>
|
||
<p className="font-mono text-xs font-bold text-gray-900 break-all leading-relaxed">
|
||
{merchant.evm_vault_address || 'Henüz Oluşturulmadı'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex flex-col justify-between group hover:border-emerald-500 transition-all">
|
||
<div>
|
||
<div className="w-16 h-16 bg-emerald-50 rounded-2xl flex items-center justify-center text-emerald-600 mb-6 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
|
||
<ShieldCheck size={28} />
|
||
</div>
|
||
<h3 className="text-xl font-black text-gray-900 mb-2">Solana Kasanız</h3>
|
||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mb-6">Solana / SPL Token Ödemeleri İçin</p>
|
||
</div>
|
||
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-100">
|
||
<p className="text-[9px] font-black text-gray-400 uppercase tracking-widest mb-2">Adres</p>
|
||
<p className="font-mono text-xs font-bold text-gray-900 break-all leading-relaxed">
|
||
{merchant.sol_vault_address || 'Henüz Oluşturulmadı'}
|
||
</p>
|
||
</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">
|
||
<TrendingUp 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>
|
||
);
|
||
}
|