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:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
import { format } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
import Link from 'next/link';
|
||||
import TransactionChart from '@/components/admin/TransactionChart';
|
||||
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
||||
|
||||
async function getStats() {
|
||||
async function getStats(rangeDays: number = 30) {
|
||||
const { data: transactions, error } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
@@ -34,11 +36,11 @@ async function getStats() {
|
||||
.map(t => t.customer_name || t.customer_phone)
|
||||
).size;
|
||||
|
||||
// Last 30 days chart data
|
||||
const last30Days = Array.from({ length: 30 }, (_, i) => {
|
||||
// Dynamic chart data based on range
|
||||
const chartData = Array.from({ length: rangeDays }, (_, i) => {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() - (29 - i));
|
||||
d.setDate(d.getDate() - (rangeDays - 1 - i));
|
||||
return {
|
||||
date: d.toISOString().split('T')[0],
|
||||
displayDate: format(d, 'd MMM', { locale: tr }),
|
||||
@@ -48,7 +50,7 @@ async function getStats() {
|
||||
|
||||
successfulTransactions.forEach(t => {
|
||||
const dateStr = new Date(t.created_at).toISOString().split('T')[0];
|
||||
const dayMatch = last30Days.find(d => d.date === dateStr);
|
||||
const dayMatch = chartData.find(d => d.date === dateStr);
|
||||
if (dayMatch) {
|
||||
dayMatch.amount += Number(t.amount);
|
||||
}
|
||||
@@ -62,12 +64,16 @@ async function getStats() {
|
||||
successRate,
|
||||
totalCount,
|
||||
uniqueCustomers,
|
||||
chartData: last30Days
|
||||
chartData
|
||||
};
|
||||
}
|
||||
|
||||
export default async function AdminDashboard() {
|
||||
const stats = await getStats();
|
||||
export default async function AdminDashboard(props: {
|
||||
searchParams: Promise<{ range?: string }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const range = searchParams.range ? parseInt(searchParams.range) : 30;
|
||||
const stats = await getStats(range);
|
||||
|
||||
if (!stats) {
|
||||
return <div className="p-10 font-bold text-gray-500 font-sans tracking-tight uppercase">Henüz bir işlem verisi bulunamadı.</div>;
|
||||
@@ -96,163 +102,65 @@ export default async function AdminDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Customers */}
|
||||
{/* Successful Transactions */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Toplam Müşteri</p>
|
||||
<div className="p-3 bg-indigo-50 rounded-xl text-indigo-600">
|
||||
<Users size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-black text-gray-900">{stats.uniqueCustomers.toLocaleString('tr-TR')}</h3>
|
||||
<div className="flex items-center gap-1 mt-2 text-emerald-500 font-bold text-xs uppercase tracking-tighter">
|
||||
<TrendingUp size={14} />
|
||||
<span>{stats.totalCount} <span className="text-gray-400 font-medium lowercase">toplam işlem kaydı</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Payments */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Bekleyen Ödemeler</p>
|
||||
<div className="p-3 bg-orange-50 rounded-xl text-orange-600">
|
||||
<ClipboardList size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-black text-gray-900">{stats.pendingCount}</h3>
|
||||
<div className="flex items-center gap-1 mt-2 text-orange-500 font-bold text-xs uppercase tracking-tighter">
|
||||
<ClipboardList size={14} />
|
||||
<span>İşlem Bekliyor <span className="text-gray-400 font-medium lowercase">onay aşamasında</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success Rate */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Başarı Oranı</p>
|
||||
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">İş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-3xl font-black text-gray-900">{stats.successRate.toFixed(1)}%</h3>
|
||||
<div className="flex items-center gap-1 mt-2 text-emerald-500 font-bold text-xs uppercase tracking-tighter">
|
||||
<TrendingUp size={14} />
|
||||
<span>Optimized <span className="text-gray-400 font-medium lowercase">ödeme dönüşüm oranı</span></span>
|
||||
<h3 className="text-3xl font-black text-gray-900">{stats.successfulCount}</h3>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-2">Tamamlanan Ödeme</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conversion Rate */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Başarı Oranı</p>
|
||||
<div className="p-3 bg-orange-50 rounded-xl text-orange-600">
|
||||
<TrendingUp size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-black text-gray-900">%{stats.successRate.toFixed(1)}</h3>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-2">{stats.totalCount} Toplam İstek</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unique Customers */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm space-y-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<p className="text-sm font-bold text-gray-400 uppercase tracking-wider">Tekil Müşteri</p>
|
||||
<div className="p-3 bg-purple-50 rounded-xl text-purple-600">
|
||||
<Users size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-3xl font-black text-gray-900">{stats.uniqueCustomers}</h3>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-2">Farklı Ödeme Kaynağı</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle Section: Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Transaction Volume Line Chart */}
|
||||
<div className="lg:col-span-2 bg-white p-8 rounded-3xl border border-gray-100 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-gray-900 leading-none">İşlem Hacmi</h3>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-wider mt-2">Son 30 günlük toplam hacim</p>
|
||||
</div>
|
||||
<select className="bg-gray-50 border-none rounded-xl text-[10px] font-black uppercase tracking-widest px-4 py-2 outline-none">
|
||||
<option>Son 30 Gün</option>
|
||||
<option>Son 7 Gün</option>
|
||||
</select>
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-gray-900 leading-none">İşlem Hacmi</h3>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-wider mt-2">Son {range} günlük toplam hacim</p>
|
||||
</div>
|
||||
|
||||
<div className="h-64 relative flex items-end justify-between px-4 pb-12">
|
||||
{/* Dynamic SVG Chart */}
|
||||
{(() => {
|
||||
const maxAmount = Math.max(...stats.chartData.map(d => d.amount), 100);
|
||||
const points = stats.chartData.map((d, i) => ({
|
||||
x: (i / 29) * 100, // 0 to 100%
|
||||
y: 100 - (d.amount / maxAmount) * 80 - 10 // 10 to 90% (lower y is higher value)
|
||||
}));
|
||||
|
||||
const dLine = points.reduce((acc, p, i) =>
|
||||
i === 0 ? `M 0 ${p.y}` : `${acc} L ${p.x} ${p.y}`, ''
|
||||
);
|
||||
|
||||
const dArea = `${dLine} L 100 100 L 0 100 Z`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
className="absolute inset-0 w-full h-full text-blue-500 overflow-visible px-4 pt-10 pb-12"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<path
|
||||
d={dLine}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d={dArea}
|
||||
fill="url(#chartGradient)"
|
||||
stroke="none"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="currentColor" stopOpacity="0.2" />
|
||||
<stop offset="100%" stopColor="currentColor" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 flex justify-between text-[10px] font-bold text-gray-300 uppercase px-8 pb-4 border-t border-gray-50 pt-4">
|
||||
<span>{stats.chartData[0].displayDate}</span>
|
||||
<span>{stats.chartData[10].displayDate}</span>
|
||||
<span>{stats.chartData[20].displayDate}</span>
|
||||
<span>Bugün</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<QueryRangeSelector />
|
||||
<p className="text-[9px] text-gray-400 font-bold uppercase tracking-tighter">
|
||||
{range} günlük veri gösteriliyor
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revenue by Source Donut Chart */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm flex flex-col">
|
||||
<h3 className="text-lg font-black text-gray-900 leading-none mb-10 uppercase tracking-tight">Kaynak Bazlı Ciro</h3>
|
||||
|
||||
<div className="flex-1 flex flex-col items-center justify-center space-y-8">
|
||||
<div className="relative w-48 h-48">
|
||||
<svg className="w-full h-full -rotate-90">
|
||||
<circle cx="96" cy="96" r="80" stroke="#F1F5F9" strokeWidth="24" fill="none" />
|
||||
<circle cx="96" cy="96" r="80" stroke="#2563EB" strokeWidth="24" fill="none" strokeDasharray="502" strokeDashoffset="200" strokeLinecap="round" />
|
||||
<circle cx="96" cy="96" r="80" stroke="#60A5FA" strokeWidth="24" fill="none" strokeDasharray="502" strokeDashoffset="400" strokeLinecap="round" />
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-sans">
|
||||
<p className="text-2xl font-black text-gray-900 truncate max-w-[120px] text-center">{stats.totalRevenue.toLocaleString('tr-TR', { maximumFractionDigits: 0 })} ₺</p>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Toplam Ciro</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4 w-full px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-600"></div>
|
||||
<span className="text-xs font-bold text-gray-900">Kart (60%)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-400"></div>
|
||||
<span className="text-xs font-bold text-gray-900">Havale (20%)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-gray-200"></div>
|
||||
<span className="text-xs font-bold text-gray-900">Cüzdan (15%)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-gray-100"></div>
|
||||
<span className="text-xs font-bold text-gray-400 text-center uppercase tracking-tighter">Diğer (5%)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TransactionChart data={stats.chartData} />
|
||||
</div>
|
||||
|
||||
{/* Bottom Section: Recent Transactions Table */}
|
||||
@@ -280,7 +188,7 @@ export default async function AdminDashboard() {
|
||||
<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-sm font-black text-gray-900">#{t.stripe_pi_id.slice(-8).toUpperCase()}</span>
|
||||
<span className="text-sm font-black text-gray-900">#{t.stripe_pi_id?.slice(-8).toUpperCase() || 'EXTERNAL'}</span>
|
||||
<span className="text-[10px] text-gray-400 font-bold uppercase tracking-wider mt-1">{t.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user