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:
mstfyldz
2026-01-20 21:58:41 +03:00
parent af09543374
commit 3562e10713
46 changed files with 3505 additions and 414 deletions

View File

@@ -9,11 +9,13 @@ import {
Smartphone,
Calendar
} from 'lucide-react';
import { supabaseAdmin } from '@/lib/supabase';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { format, subDays } from 'date-fns';
import { tr } from 'date-fns/locale';
import AnalyticsBarChart from '@/components/admin/AnalyticsBarChart';
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
async function getAnalyticsData() {
async function getAnalyticsData(rangeDays: number = 12) {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*')
@@ -25,10 +27,9 @@ async function getAnalyticsData() {
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
const avgOrderValue = successfulTransactions.length > 0 ? totalRevenue / successfulTransactions.length : 0;
// Monthly data for chart (grouped by month or last 12 periods)
// To keep it simple and meaningful, let's show last 12 days for "Gelir Trendi"
const last12Periods = Array.from({ length: 12 }, (_, i) => {
const d = subDays(new Date(), 11 - i);
// Monthly data for chart (grouped by month or last N periods)
const lastPeriods = Array.from({ length: rangeDays }, (_, i) => {
const d = subDays(new Date(), (rangeDays - 1) - i);
return {
date: d.toISOString().split('T')[0],
label: format(d, 'd MMM', { locale: tr }),
@@ -38,7 +39,7 @@ async function getAnalyticsData() {
successfulTransactions.forEach(t => {
const dateStr = new Date(t.created_at).toISOString().split('T')[0];
const periodMatch = last12Periods.find(p => p.date === dateStr);
const periodMatch = lastPeriods.find(p => p.date === dateStr);
if (periodMatch) {
periodMatch.amount += Number(t.amount);
}
@@ -47,14 +48,18 @@ async function getAnalyticsData() {
return {
totalRevenue,
avgOrderValue,
chartData: last12Periods,
chartData: lastPeriods,
totalCount: transactions.length,
successCount: successfulTransactions.length,
};
}
export default async function AnalyticsPage() {
const data = await getAnalyticsData();
export default async function AnalyticsPage(props: {
searchParams: Promise<{ range?: string }>;
}) {
const searchParams = await props.searchParams;
const range = searchParams.range ? parseInt(searchParams.range) : 12;
const data = await getAnalyticsData(range);
if (!data) return <div className="p-10 font-black text-gray-400 uppercase tracking-[0.2em] animate-pulse">Veriler hazırlanıyor...</div>;
@@ -76,10 +81,7 @@ export default async function AnalyticsPage() {
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-2">Sistem performans verileri</p>
</div>
<div className="flex items-center gap-4">
<button className="flex items-center gap-3 px-6 py-4 bg-white border border-gray-100 rounded-2xl text-xs font-black text-gray-600 hover:bg-gray-50 transition uppercase tracking-widest">
<Calendar size={18} className="text-gray-300" />
Son 30 Gün
</button>
<QueryRangeSelector />
</div>
</div>
@@ -113,63 +115,44 @@ export default async function AnalyticsPage() {
</div>
</div>
<div className="h-72 flex items-end justify-between gap-4">
{data.chartData.map((d, i) => {
const h = (d.amount / maxChartAmount) * 90 + 5; // 5% to 95%
return (
<div key={i} className="flex-1 group relative h-full flex flex-col justify-end">
<div
className="w-full bg-blue-500 rounded-t-xl transition-all duration-500 group-hover:bg-blue-600 cursor-pointer relative"
style={{ height: `${h}%` }}
>
<div className="absolute -top-12 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-[10px] font-black py-2 px-3 rounded-lg opacity-0 group-hover:opacity-100 transition shadow-xl pointer-events-none whitespace-nowrap z-20">
{d.amount.toLocaleString('tr-TR')}
</div>
</div>
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 text-[9px] font-black text-gray-300 uppercase tracking-tighter text-center">
{d.label}
<AnalyticsBarChart data={data.chartData} />
</div>
</div>
{/* Breakdown Grid */}
<div className="grid grid-cols-1 gap-8 text-sans tracking-tight">
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm">
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight mb-8">Cihaz Dağılımı</h3>
<div className="space-y-8">
{[
{ label: 'Mobil', value: '64%', icon: Smartphone, color: 'bg-blue-600', width: '64%' },
{ label: 'Masaüstü', value: '28%', icon: Monitor, color: 'bg-indigo-400', width: '28%' },
{ label: 'Tablet', value: '8%', icon: Globe, color: 'bg-indigo-100', width: '8%' },
].map((item, i) => (
<div key={i} className="space-y-3">
<div className="flex items-center justify-between text-xs font-black text-gray-900 uppercase tracking-widest">
<div className="flex items-center gap-3">
<item.icon size={18} className="text-gray-300" />
<span>{item.label}</span>
</div>
<span>{item.value}</span>
</div>
);
})}
<div className="h-3 w-full bg-gray-50 rounded-full overflow-hidden border border-gray-100">
<div className={`h-full ${item.color} rounded-full`} style={{ width: item.width }}></div>
</div>
</div>
))}
</div>
</div>
{/* Breakdown Grid */}
<div className="grid grid-cols-1 gap-8 text-sans tracking-tight">
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm">
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight mb-8">Cihaz Dağılımı</h3>
<div className="space-y-8">
{[
{ label: 'Mobil', value: '64%', icon: Smartphone, color: 'bg-blue-600', width: '64%' },
{ label: 'Masaüstü', value: '28%', icon: Monitor, color: 'bg-indigo-400', width: '28%' },
{ label: 'Tablet', value: '8%', icon: Globe, color: 'bg-indigo-100', width: '8%' },
].map((item, i) => (
<div key={i} className="space-y-3">
<div className="flex items-center justify-between text-xs font-black text-gray-900 uppercase tracking-widest">
<div className="flex items-center gap-3">
<item.icon size={18} className="text-gray-300" />
<span>{item.label}</span>
</div>
<span>{item.value}</span>
</div>
<div className="h-3 w-full bg-gray-50 rounded-full overflow-hidden border border-gray-100">
<div className={`h-full ${item.color} rounded-full`} style={{ width: item.width }}></div>
</div>
</div>
))}
</div>
</div>
<div className="bg-[#2563EB] p-10 rounded-[40px] shadow-2xl shadow-blue-200 text-white relative overflow-hidden group">
<div className="relative z-10 space-y-6">
<h3 className="text-2xl font-black leading-tight">Analizleriniz hazır! <br /> Bu ay başarılı bir grafik çiziyorsunuz.</h3>
<button className="px-8 py-4 bg-white text-blue-600 rounded-2xl font-black text-sm hover:scale-105 transition active:scale-95 shadow-xl uppercase tracking-widest">
Akıllı İpuçlarını
</button>
</div>
<BarChart3 className="absolute -bottom-10 -right-10 w-64 h-64 text-white/10 rotate-12 group-hover:rotate-0 transition-transform duration-700" />
<div className="bg-[#2563EB] p-10 rounded-[40px] shadow-2xl shadow-blue-200 text-white relative overflow-hidden group">
<div className="relative z-10 space-y-6">
<h3 className="text-2xl font-black leading-tight">Analizleriniz hazır! <br /> Bu ay başarılı bir grafik çiziyorsunuz.</h3>
<button className="px-8 py-4 bg-white text-blue-600 rounded-2xl font-black text-sm hover:scale-105 transition active:scale-95 shadow-xl uppercase tracking-widest">
Akıllı İpuçlarını
</button>
</div>
<BarChart3 className="absolute -bottom-10 -right-10 w-64 h-64 text-white/10 rotate-12 group-hover:rotate-0 transition-transform duration-700" />
</div>
</div>
</div>

View File

@@ -8,12 +8,15 @@ import {
MoreHorizontal,
ArrowUpRight
} from 'lucide-react';
import { supabaseAdmin } from '@/lib/supabase';
import { supabaseAdmin } from '@/lib/supabase-admin';
async function getCustomers() {
import CustomerSearch from '@/components/admin/CustomerSearch';
async function getFilteredCustomers(queryText?: string) {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*');
.select('*')
.order('created_at', { ascending: false });
if (error || !transactions) return null;
@@ -21,7 +24,10 @@ async function getCustomers() {
const customerMap = new Map();
transactions.forEach(t => {
const key = t.customer_name || t.customer_phone || 'Unknown';
// We use a combination of name and phone as a key if possible,
// fallback to whichever is available
const key = (t.customer_phone || t.customer_name || 'Unknown').toLowerCase().trim();
if (!customerMap.has(key)) {
customerMap.set(key, {
id: t.id,
@@ -29,27 +35,45 @@ async function getCustomers() {
phone: t.customer_phone || 'Telefon Yok',
spent: 0,
orders: 0,
lastOrder: t.created_at,
status: 'New'
});
}
const c = customerMap.get(key);
c.orders += 1;
if (t.status === 'succeeded') {
c.spent += Number(t.amount);
}
// Update last order date if this transaction is newer
if (new Date(t.created_at) > new Date(c.lastOrder)) {
c.lastOrder = t.created_at;
}
});
const customers = Array.from(customerMap.values()).map(c => {
if (c.orders > 5) c.status = 'High Value';
let customers = Array.from(customerMap.values()).map(c => {
if (c.orders > 5 && c.spent > 1000) c.status = 'High Value';
else if (c.orders > 1) c.status = 'Active';
return c;
});
// Client-side search (since customers are derived)
if (queryText) {
const q = queryText.toLowerCase();
customers = customers.filter(c =>
c.name.toLowerCase().includes(q) ||
c.phone.toLowerCase().includes(q)
);
}
return customers;
}
export default async function CustomersPage() {
const customers = await getCustomers();
export default async function CustomersPage(props: {
searchParams: Promise<{ q?: string }>;
}) {
const searchParams = await props.searchParams;
const customers = await getFilteredCustomers(searchParams.q);
if (!customers) return <div className="p-10 font-black text-gray-400 uppercase tracking-widest animate-pulse">Müşteriler yükleniyor...</div>;
@@ -61,21 +85,21 @@ export default async function CustomersPage() {
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Müşteriler</h1>
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-2">Müşteri portföyünüzü yönetin</p>
</div>
<button className="flex items-center justify-center gap-3 px-8 py-4 bg-[#2563EB] text-white rounded-2xl font-black shadow-xl shadow-blue-100 hover:bg-blue-700 transition active:scale-95 uppercase text-xs tracking-widest">
<Plus size={18} />
Yeni Müşteri Ekle
</button>
<div className="p-3 bg-blue-50/50 rounded-2xl border border-blue-100/50 flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></div>
<span className="text-[10px] font-black text-blue-600 uppercase tracking-widest">Canlı Veritabanı Bağlantısı</span>
</div>
</div>
{/* Stats */}
<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 flex items-center gap-8">
<div className="w-16 h-16 bg-blue-50 rounded-[20px] flex items-center justify-center text-blue-600">
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex items-center gap-8 group hover:border-blue-500 transition-colors">
<div className="w-16 h-16 bg-blue-50 rounded-[20px] flex items-center justify-center text-blue-600 group-hover:scale-110 transition-transform">
<Users size={32} />
</div>
<div>
<p className="text-3xl font-black text-gray-900">{customers.length.toLocaleString('tr-TR')}</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Toplam Müşteri</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Sorgulanan Müşteri</p>
</div>
</div>
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex items-center gap-8">
@@ -83,8 +107,8 @@ export default async function CustomersPage() {
<ArrowUpRight size={32} />
</div>
<div>
<p className="text-3xl font-black text-gray-900">Gerçek</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Canlı Veri</p>
<p className="text-3xl font-black text-gray-900">%{((customers.filter(c => c.status === 'High Value' || c.status === 'Active').length / (customers.length || 1)) * 100).toFixed(0)}</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Bağlılık Oranı</p>
</div>
</div>
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm flex items-center gap-8">
@@ -93,7 +117,7 @@ export default async function CustomersPage() {
</div>
<div>
<p className="text-3xl font-black text-gray-900">{customers.filter(c => c.phone !== 'Telefon Yok').length}</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">Telefon Kayıtlı</p>
<p className="text-[10px] text-gray-400 font-black uppercase tracking-widest mt-1">İletişim Bilgili</p>
</div>
</div>
</div>
@@ -101,17 +125,10 @@ export default async function CustomersPage() {
{/* List */}
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden">
<div className="p-8 border-b border-gray-50 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="text"
placeholder="İsim veya telefon ile ara..."
className="w-full pl-16 pr-6 py-5 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300"
/>
<CustomerSearch />
<div className="bg-gray-50 px-6 py-4 rounded-2xl">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Sıralama: En Son Ödeme</span>
</div>
<button className="text-blue-600 text-xs font-black uppercase tracking-widest hover:underline decoration-2 underline-offset-4 px-6 py-4">
Görünümü Filtrele
</button>
</div>
<div className="overflow-x-auto text-sans tracking-tight">
@@ -122,7 +139,7 @@ export default async function CustomersPage() {
<th className="px-10 py-8">Segment</th>
<th className="px-10 py-8">Sipariş</th>
<th className="px-10 py-8">Toplam Harcama</th>
<th className="px-10 py-8 text-right">Aksiyonlar</th>
<th className="px-10 py-8 text-right">Durum</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
@@ -130,7 +147,7 @@ export default async function CustomersPage() {
<tr key={i} className="group hover:bg-gray-50/50 transition-colors">
<td className="px-10 py-10">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-gray-100 rounded-2xl flex items-center justify-center text-gray-400 font-black text-sm uppercase tracking-tighter">
<div className="w-14 h-14 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center font-black text-sm uppercase tracking-tighter group-hover:bg-blue-600 group-hover:text-white transition-colors">
{customer.name.slice(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
@@ -141,7 +158,7 @@ export default async function CustomersPage() {
</td>
<td className="px-10 py-10">
<span className={`inline-flex items-center px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest ${customer.status === 'Active' ? 'bg-emerald-50 text-emerald-600' :
customer.status === 'High Value' ? 'bg-blue-50 text-blue-600' :
customer.status === 'High Value' ? 'bg-blue-600 text-white' :
customer.status === 'New' ? 'bg-indigo-50 text-indigo-600' :
'bg-gray-50 text-gray-400'
}`}>
@@ -151,7 +168,7 @@ export default async function CustomersPage() {
</span>
</td>
<td className="px-10 py-10">
<span className="text-sm font-black text-gray-900">{customer.orders}</span>
<span className="text-sm font-black text-gray-900">{customer.orders} İşlem</span>
</td>
<td className="px-10 py-10">
<span className="text-sm font-black text-gray-900">
@@ -160,12 +177,12 @@ export default async function CustomersPage() {
</td>
<td className="px-10 py-10 text-right">
<div className="flex items-center justify-end gap-3">
<button className="p-3 bg-gray-50 text-gray-400 rounded-xl hover:text-blue-600 hover:bg-blue-50 transition">
<Phone size={18} />
</button>
<button className="p-3 bg-gray-50 text-gray-400 rounded-xl hover:text-gray-900 hover:bg-gray-100 transition">
<MoreHorizontal size={18} />
</button>
<div className="flex flex-col items-end">
<span className="text-[10px] font-black text-gray-900 uppercase tracking-widest">Son İşlem</span>
<span className="text-[10px] text-gray-400 font-bold mt-1 uppercase">
{new Date(customer.lastOrder).toLocaleDateString('tr-TR')}
</span>
</div>
</div>
</td>
</tr>
@@ -173,6 +190,12 @@ export default async function CustomersPage() {
</tbody>
</table>
</div>
{customers.length === 0 && (
<div className="p-20 text-center">
<Users className="w-12 h-12 text-gray-200 mx-auto mb-4" />
<p className="text-sm font-black text-gray-400 uppercase tracking-widest">Müşteri bulunamadı</p>
</div>
)}
</div>
</div>
);

184
app/admin/docs/page.tsx Normal file
View File

@@ -0,0 +1,184 @@
'use client';
import React from 'react';
import {
Code2,
Terminal,
Globe,
Webhook,
Copy,
Check,
ArrowRight,
Zap,
ShieldCheck,
MessageSquare
} from 'lucide-react';
export default function DocumentationPage() {
const [copied, setCopied] = React.useState<string | null>(null);
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text);
setCopied(id);
setTimeout(() => setCopied(null), 2000);
};
const checkoutUrlCode = `https://p2cgateway.com/checkout?merchant_id=YOUR_MERCHANT_ID&amount=100&currency=TRY&ref_id=ORDER_123&callback_url=https://yoursite.com/success`;
return (
<div className="max-w-5xl mx-auto space-y-12 pb-20 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Header */}
<div>
<h1 className="text-4xl font-black text-gray-900 tracking-tight">API Dokümantasyonu</h1>
<p className="text-gray-400 font-bold uppercase tracking-widest text-xs mt-3 px-1">P2CGateway Entegrasyon Rehberi</p>
</div>
{/* Quick Start Card */}
<div className="bg-blue-600 rounded-[40px] p-12 text-white shadow-xl shadow-blue-100 relative overflow-hidden">
<div className="relative z-10 space-y-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center">
<Zap size={24} />
</div>
<h2 className="text-2xl font-black">Hızlı Başlangıç</h2>
</div>
<p className="text-blue-100 text-lg max-w-2xl leading-relaxed font-medium">
P2CGateway'i projenize entegre etmek için sadece bir URL oluşturmanız yeterlidir.
Karmaşık SDK'lar veya kütüphanelerle uğraşmanıza gerek yok.
</p>
</div>
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl"></div>
</div>
{/* Integration Steps */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Method 1: Checkout Redirect */}
<div className="bg-white rounded-[40px] border border-gray-100 p-10 shadow-sm space-y-8">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-emerald-50 rounded-2xl flex items-center justify-center text-emerald-600">
<Globe size={24} />
</div>
<h3 className="text-xl font-black text-gray-900">1. Ödeme Sayfasına Yönlendirme</h3>
</div>
<p className="text-gray-500 font-medium leading-relaxed">
Müşterinizi ödeme yapması için aşağıdaki URL yapısını kullanarak P2CGateway checkout sayfasına yönlendirin.
</p>
<div className="space-y-4">
<div className="bg-gray-50 p-6 rounded-3xl border border-gray-100 relative group">
<code className="text-xs font-mono text-gray-600 break-all leading-relaxed">
{checkoutUrlCode}
</code>
<button
onClick={() => copyToClipboard(checkoutUrlCode, 'url')}
className="absolute right-4 top-4 p-2 bg-white rounded-xl shadow-sm border border-gray-100 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copied === 'url' ? <Check size={16} className="text-emerald-500" /> : <Copy size={16} className="text-gray-400" />}
</button>
</div>
</div>
<div className="space-y-4">
<h4 className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Parametreler</h4>
<div className="space-y-3">
{[
{ key: 'merchant_id', desc: 'Firma ID\'niz (Firmalar sayfasından alabilirsiniz)' },
{ key: 'amount', desc: 'Ödeme tutarı (Örn: 100.00)' },
{ key: 'currency', desc: 'Para birimi (TRY, USD, EUR)' },
{ key: 'ref_id', desc: 'Sizin sisteminizdeki sipariş numarası' },
{ key: 'callback_url', desc: 'Ödeme sonrası yönlendirilecek adres' },
].map((p) => (
<div key={p.key} className="flex items-start gap-4 p-4 rounded-2xl hover:bg-gray-50 transition-colors">
<span className="text-xs font-mono text-blue-600 font-bold bg-blue-50 px-2 py-1 rounded-md">{p.key}</span>
<span className="text-xs text-gray-500 font-medium">{p.desc}</span>
</div>
))}
</div>
</div>
</div>
{/* Method 2: Webhooks */}
<div className="bg-white rounded-[40px] border border-gray-100 p-10 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">2. Webhook Bildirimleri</h3>
</div>
<p className="text-gray-500 font-medium leading-relaxed">
Ödeme tamamlandığında sistemimiz otomatik olarak firmanıza tanımlı olan Webhook URL'ine bir POST isteği gönderir.
</p>
<div className="bg-gray-900 rounded-3xl p-8 space-y-4">
<div className="flex items-center justify-between">
<span className="text-[10px] font-black text-gray-500 uppercase tracking-widest">JSON Payload Örneği</span>
<div className="flex gap-2">
<div className="w-2 h-2 rounded-full bg-red-500"></div>
<div className="w-2 h-2 rounded-full bg-yellow-500"></div>
<div className="w-2 h-2 rounded-full bg-green-500"></div>
</div>
</div>
<pre className="text-xs font-mono text-blue-400 overflow-x-auto leading-relaxed">
{`{
"status": "succeeded",
"transaction_id": "tx_821...",
"ref_id": "ORDER-123",
"amount": 100.00,
"currency": "TRY",
"customer": {
"name": "Ahmet Yılmaz",
"phone": "555..."
}
}`}
</pre>
</div>
<div className="p-6 bg-orange-50 rounded-3xl border border-orange-100 space-y-3">
<h4 className="text-orange-700 font-black text-xs uppercase tracking-widest flex items-center gap-2">
<ShieldCheck size={14} /> Güvenlik Notu
</h4>
<p className="text-orange-600/80 text-xs font-medium leading-normal">
Webhook isteklerinin P2CGateway'den geldiğini doğrulamak için API Key'inizi HTTP başlığında (X-P2C-Signature) kontrol etmelisiniz.
</p>
</div>
</div>
</div>
{/* API Resources Section */}
<div className="bg-white rounded-[40px] border border-gray-100 p-10 shadow-sm">
<div className="flex items-center gap-4 mb-10">
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
<Terminal size={24} />
</div>
<h3 className="text-2xl font-black text-gray-900">Geliştirici Kaynakları</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<button className="p-8 bg-gray-50 rounded-[32px] text-left hover:bg-blue-50 group transition-all">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-gray-400 group-hover:text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-all mb-4">
<Code2 size={20} />
</div>
<h4 className="font-black text-gray-900 mb-2">SDK & Kütüphaneler</h4>
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest">Çok Yakında</p>
</button>
<button className="p-8 bg-gray-50 rounded-[32px] text-left hover:bg-emerald-50 group transition-all">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-gray-400 group-hover:text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white transition-all mb-4">
<Globe size={20} />
</div>
<h4 className="font-black text-gray-900 mb-2">API Referansı</h4>
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest">Görüntüle</p>
</button>
<button className="p-8 bg-gray-50 rounded-[32px] text-left hover:bg-purple-50 group transition-all">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-gray-400 group-hover:text-purple-600 group-hover:bg-purple-600 group-hover:text-white transition-all mb-4">
<MessageSquare size={20} />
</div>
<h4 className="font-black text-gray-900 mb-2">Teknik Destek</h4>
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest">Geliştirici Topluluğu</p>
</button>
</div>
</div>
</div>
);
}

View File

@@ -14,7 +14,9 @@ import {
Bell,
MessageSquare,
ChevronDown,
Wallet
Wallet,
Building2,
Code2
} from 'lucide-react';
import Image from 'next/image';
import { createClient } from '@/utils/supabase/client'; // Assuming a client-side Supabase client utility
@@ -36,9 +38,11 @@ export default function AdminLayout({
const navItems = [
{ label: 'Genel Bakış', icon: LayoutDashboard, href: '/admin' },
{ label: 'Firmalar', icon: Building2, href: '/admin/merchants' },
{ label: 'İşlemler', icon: CreditCard, href: '/admin/transactions' },
{ label: 'Müşteriler', icon: Users, href: '/admin/customers' },
{ label: 'Analizler', icon: BarChart3, href: '/admin/analytics' },
{ label: 'Dokümantasyon', icon: Code2, href: '/admin/docs' },
{ label: 'Ayarlar', icon: Settings, href: '/admin/settings' },
];
@@ -51,8 +55,8 @@ export default function AdminLayout({
<Wallet size={24} />
</div>
<div>
<h1 className="font-black text-gray-900 leading-tight">froyd Admin</h1>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Yönetim Paneli</p>
<h1 className="font-black text-gray-900 leading-tight text-lg">P2CGateway <span className="text-blue-600">Admin</span></h1>
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-wider">Merkezi Yönetim Paneli</p>
</div>
</div>

View File

@@ -0,0 +1,222 @@
'use client';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import {
ArrowLeft,
Building2,
Globe,
CheckCircle2,
Loader2,
ShieldCheck,
Smartphone,
CreditCard
} from 'lucide-react';
import Link from 'next/link';
export default function NewMerchantPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [name, setName] = useState('');
const [webhookUrl, setWebhookUrl] = useState('');
const [paymentProvider, setPaymentProvider] = useState('stripe');
const [success, setSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const response = await fetch('/api/merchants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
webhook_url: webhookUrl,
payment_provider: paymentProvider
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Firma eklenemedi.');
}
setSuccess(true);
setTimeout(() => {
router.push('/admin/merchants');
router.refresh();
}, 2000);
} catch (err: any) {
alert(err.message || 'Firma eklenirken bir hata oluştu.');
} finally {
setIsLoading(false);
}
};
if (success) {
return (
<div className="min-h-[80vh] flex items-center justify-center">
<div className="text-center space-y-6 animate-in fade-in zoom-in duration-500">
<div className="w-24 h-24 bg-emerald-50 rounded-[40px] flex items-center justify-center text-emerald-500 mx-auto shadow-lg shadow-emerald-100">
<CheckCircle2 size={48} />
</div>
<div className="space-y-2">
<h2 className="text-3xl font-black text-gray-900">Firma Başarıyla Oluşturuldu!</h2>
<p className="text-gray-500 font-medium text-lg">Yönlendiriliyorsunuz...</p>
</div>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto space-y-12 pb-20">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<Link
href="/admin/merchants"
className="w-12 h-12 bg-white rounded-2xl border border-gray-100 flex items-center justify-center text-gray-400 hover:text-gray-900 hover:shadow-md transition-all group"
>
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
</Link>
<div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Yeni Firma Kaydı</h1>
<p className="text-gray-400 font-bold uppercase tracking-widest text-[10px] mt-1">Sisteme yeni bir işletme entegre edin</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Form Column */}
<div className="lg:col-span-2 space-y-8">
<form onSubmit={handleSubmit} className="bg-white rounded-[40px] border border-gray-100 p-10 shadow-sm space-y-8">
{/* Name Input */}
<div className="space-y-3">
<label className="text-[11px] font-black text-gray-400 uppercase tracking-widest ml-1">İşletme / Firma Adı</label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-gray-300 group-focus-within:text-blue-500 transition-colors">
<Building2 size={20} />
</div>
<input
required
type="text"
placeholder="Örn: Global Teknoloji A.Ş."
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full pl-16 pr-8 py-5 bg-gray-50 border-2 border-transparent focus:border-blue-100 focus:bg-white rounded-3xl text-base font-bold text-gray-900 outline-none transition-all placeholder:text-gray-300"
/>
</div>
</div>
{/* Provider Selection */}
<div className="space-y-4">
<label className="text-[11px] font-black text-gray-400 uppercase tracking-widest ml-1">Ödeme Altyapısı (Gateway)</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{[
{ id: 'stripe', name: 'Stripe', icon: CreditCard, desc: 'Global kart ödemeleri' },
{ id: 'cryptomus', name: 'Cryptomus', icon: Globe, desc: 'Kripto Para' },
{ id: 'nuvei', name: 'Nuvei', icon: ShieldCheck, desc: 'E-commerce Experts' },
{ id: 'paykings', name: 'PayKings', icon: ShieldCheck, desc: 'High Risk Specialist' },
{ id: 'seurionpay', name: 'SecurionPay', icon: CreditCard, desc: 'Simple Payments' },
].map((p) => (
<button
key={p.id}
type="button"
onClick={() => setPaymentProvider(p.id)}
className={`p-6 rounded-3xl border-2 text-left transition-all space-y-2 ${paymentProvider === p.id
? 'border-blue-500 bg-blue-50/50'
: 'border-gray-100 bg-gray-50/30 hover:bg-gray-50'}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${paymentProvider === p.id ? 'bg-blue-600 text-white' : 'bg-white text-gray-400 border border-gray-100'}`}>
<p.icon size={20} />
</div>
<div>
<p className="font-black text-sm text-gray-900 leading-tight">{p.name}</p>
<p className="text-[10px] text-gray-400 font-bold uppercase mt-1">{p.desc}</p>
</div>
</button>
))}
</div>
</div>
{/* Webhook Input */}
<div className="space-y-3">
<label className="text-[11px] font-black text-gray-400 uppercase tracking-widest ml-1">Geri Dönüş (Webhook) URL</label>
<div className="relative group">
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-gray-300 group-focus-within:text-blue-500 transition-colors">
<Globe size={20} />
</div>
<input
type="url"
placeholder="https://firma.com/api/callback"
value={webhookUrl}
onChange={(e) => setWebhookUrl(e.target.value)}
className="w-full pl-16 pr-8 py-5 bg-gray-50 border-2 border-transparent focus:border-blue-100 focus:bg-white rounded-3xl text-base font-bold text-gray-900 outline-none transition-all placeholder:text-gray-300"
/>
</div>
<p className="text-[11px] text-gray-400 font-medium leading-relaxed px-1">
Ödeme sonuçlarını gerçek zamanlı almak için firmanın API noktasını buraya girebilirsiniz. (Opsiyonel)
</p>
</div>
{/* Submit Button */}
<div className="pt-4">
<button
type="submit"
disabled={isLoading || !name}
className="w-full bg-[#2563EB] text-white py-6 rounded-3xl font-black text-lg hover:bg-blue-700 transition shadow-xl shadow-blue-100 disabled:opacity-50 disabled:shadow-none flex items-center justify-center gap-3 active:scale-[0.98]"
>
{isLoading ? (
<Loader2 className="animate-spin" size={24} />
) : (
<>
<span>Firmayı Kaydet ve Ödeme Almaya Başla</span>
</>
)}
</button>
</div>
</form>
</div>
{/* Info Column */}
<div className="space-y-6">
<div className="bg-blue-600 rounded-[40px] p-8 text-white shadow-xl shadow-blue-100">
<h4 className="font-black text-xl mb-4">Hızlı Başlangıç</h4>
<ul className="space-y-6">
<li className="flex gap-4">
<div className="w-8 h-8 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<CheckCircle2 size={16} />
</div>
<p className="text-sm font-medium leading-normal opacity-90">Firma ismi sisteme özel bir ID ile kaydedilir.</p>
</li>
<li className="flex gap-4">
<div className="w-8 h-8 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<Smartphone size={16} />
</div>
<p className="text-sm font-medium leading-normal opacity-90">Kayıt sonrası hemen ödeme linki oluşturulur.</p>
</li>
<li className="flex gap-4">
<div className="w-8 h-8 bg-white/20 rounded-lg flex items-center justify-center shrink-0">
<ShieldCheck size={16} />
</div>
<p className="text-sm font-medium leading-normal opacity-90">Tüm işlemler 256-bit SSL ile korunur.</p>
</li>
</ul>
</div>
<div className="bg-white rounded-[40px] border border-gray-100 p-8 space-y-4">
<div className="flex items-center gap-3 text-gray-900">
<CreditCard size={20} className="text-blue-600" />
<h4 className="font-black">Desteklenen Kartlar</h4>
</div>
<p className="text-xs text-gray-400 font-bold leading-relaxed">
Visa, Mastercard, Troy ve tüm yerel banka kartları ile ödeme alabilirsiniz.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,349 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import {
Plus,
Building2,
Copy,
ExternalLink,
MoreVertical,
Globe,
Check,
Pencil,
Trash2,
X
} from 'lucide-react';
export default function MerchantsPage() {
const [merchants, setMerchants] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [copiedId, setCopiedId] = useState<string | null>(null);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editingMerchant, setEditingMerchant] = useState<any>(null);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
fetchMerchants();
}, []);
const fetchMerchants = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/merchants');
const data = await response.json();
if (!response.ok) throw new Error(data.error);
setMerchants(data);
} catch (err) {
console.error('Fetch error:', err);
} finally {
setIsLoading(false);
}
};
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
};
const handleEditClick = (merchant: any) => {
setEditingMerchant({ ...merchant });
setIsEditModalOpen(true);
};
const handleUpdateMerchant = async (e: React.FormEvent) => {
e.preventDefault();
setIsUpdating(true);
try {
const response = await fetch(`/api/merchants/${editingMerchant.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: editingMerchant.name,
webhook_url: editingMerchant.webhook_url,
payment_provider: editingMerchant.payment_provider,
provider_config: editingMerchant.provider_config
})
});
if (!response.ok) throw new Error('Güncelleme başarısız.');
await fetchMerchants();
setIsEditModalOpen(false);
setEditingMerchant(null);
} catch (err: any) {
alert(err.message);
} finally {
setIsUpdating(false);
}
};
const handleDeleteMerchant = async (id: string) => {
if (!confirm('Bu firmayı silmek istediğinize emin misiniz? Bu işlem geri alınamaz.')) return;
try {
const response = await fetch(`/api/merchants/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Silme işlemi başarısız.');
await fetchMerchants();
} catch (err: any) {
alert(err.message);
}
};
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* 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">
<Building2 size={24} />
</div>
<div>
<h2 className="text-xl font-black text-gray-900">Firmalar (Merchants)</h2>
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-0.5">Ödeme alan tüm işletmeler</p>
</div>
</div>
<Link
href="/admin/merchants/new"
className="flex items-center justify-center gap-2 px-6 py-3 bg-[#2563EB] text-white rounded-2xl text-sm font-bold hover:bg-blue-700 transition shadow-lg shadow-blue-100"
>
<Plus size={18} />
Yeni Firma Ekle
</Link>
</div>
{/* Merchant Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
{isLoading ? (
<div className="col-span-full flex justify-center py-20">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : merchants.map((m) => {
const identifier = m.short_id || m.id;
const paymentLink = typeof window !== 'undefined'
? `${window.location.origin}/checkout?merchant_id=${identifier}&amount=0`
: `https://p2cgateway.com/checkout?merchant_id=${identifier}&amount=0`;
return (
<div key={m.id} className="bg-white rounded-[40px] border border-gray-100 p-8 shadow-sm hover:shadow-md transition-shadow group">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gray-50 rounded-2xl flex items-center justify-center text-gray-400 font-black text-xl group-hover:bg-blue-50 group-hover:text-blue-600 transition-colors">
{m.name.substring(0, 1).toUpperCase()}
</div>
<div>
<h3 className="text-lg font-black text-gray-900">{m.name}</h3>
<div className="flex items-center gap-2">
<span className="text-[10px] text-blue-600 font-black uppercase tracking-widest">ID: {identifier}</span>
{m.short_id && <span className="px-1.5 py-0.5 bg-blue-50 text-blue-600 text-[8px] font-black rounded border border-blue-100 uppercase tracking-tighter transition-all group-hover:bg-blue-600 group-hover:text-white group-hover:border-blue-700">Short Link Active</span>}
</div>
</div>
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleEditClick(m)}
className="p-2 text-gray-300 hover:text-blue-600 hover:bg-blue-50 rounded-xl transition-all"
title="Düzenle"
>
<Pencil size={18} />
</button>
<button
onClick={() => handleDeleteMerchant(m.id)}
className="p-2 text-gray-300 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all"
title="Sil"
>
<Trash2 size={18} />
</button>
</div>
</div>
<div className="space-y-4">
{/* API Key Section */}
<div className="p-4 bg-gray-50 rounded-2xl space-y-2 border border-transparent hover:border-gray-100 transition-colors">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">API Anahtarı (Secret Key)</label>
<div className="flex items-center justify-between gap-3">
<div className="flex-1 bg-white px-4 py-2 rounded-xl text-[11px] font-mono text-gray-400 truncate border border-gray-100/50">
{m.api_key || '••••••••••••••••'}
</div>
<button
onClick={() => copyToClipboard(m.api_key, m.id + '-key')}
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
>
{copiedId === m.id + '-key' ? <Check size={14} /> : <Copy size={14} />}
</button>
</div>
</div>
{/* Webhook Section */}
<div className="p-4 bg-gray-50 rounded-2xl space-y-2 border border-transparent hover:border-gray-100 transition-colors">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Webhook URL</label>
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-bold text-gray-600 truncate px-1">{m.webhook_url || 'Ayarlanmamış'}</span>
<Globe size={14} className="text-gray-300 shrink-0" />
</div>
</div>
{/* Payment Link Section */}
<div className="p-4 bg-blue-50/30 rounded-2xl border border-blue-50 space-y-2">
<label className="text-[10px] font-black text-blue-600 uppercase tracking-widest">Ödeme Linki</label>
<div className="flex items-center gap-3">
<div className="flex-1 bg-white px-4 py-2 rounded-xl text-[11px] font-mono text-gray-400 truncate border border-blue-100/30">
{paymentLink}
</div>
<button
onClick={() => copyToClipboard(paymentLink, m.id)}
className="p-2 bg-white text-blue-600 rounded-xl border border-blue-100 hover:bg-blue-600 hover:text-white transition shadow-sm shrink-0"
>
{copiedId === m.id ? <Check size={16} /> : <Copy size={16} />}
</button>
</div>
<p className="text-[10px] text-gray-400 font-medium">Bu linki tutar ve referans ekleyerek değiştirebilirsiniz.</p>
</div>
</div>
<div className="mt-8 pt-6 border-t border-gray-50 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Aktif</span>
</div>
<a
href={`/admin/transactions?merchant_id=${m.id}`}
className="flex items-center gap-2 text-xs font-black text-[#2563EB] hover:underline"
>
Tüm İşlemleri Gör
<ExternalLink size={12} />
</a>
<Link
href={`/merchant/${identifier}`}
className="flex items-center gap-2 text-xs font-black text-emerald-600 hover:underline"
>
Firma Paneli
<ExternalLink size={12} />
</Link>
</div>
</div>
);
})}
{!isLoading && merchants.length === 0 && (
<div className="col-span-full p-20 bg-white rounded-[40px] border border-gray-100 text-center space-y-6">
<div className="w-20 h-20 bg-gray-50 rounded-3xl flex items-center justify-center mx-auto text-gray-200">
<Building2 size={40} />
</div>
<div className="max-w-xs mx-auto">
<p className="text-lg font-black text-gray-900">Henüz firma bulunmuyor</p>
<p className="text-gray-400 text-sm mt-1 font-bold">İlk firmanızı ekleyerek ödeme almaya başlayın.</p>
</div>
<Link
href="/admin/merchants/new"
className="inline-flex items-center gap-2 px-8 py-4 bg-gray-900 text-white rounded-2xl text-sm font-black hover:bg-gray-800 transition shadow-xl shadow-gray-200"
>
<Plus size={20} />
Firma Ekleyerek Başlayın
</Link>
</div>
)}
</div>
{/* Edit Modal */}
{isEditModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-6 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white w-full max-w-lg rounded-[40px] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300">
<div className="p-10">
<div className="flex justify-between items-center mb-10">
<div>
<h2 className="text-2xl font-black text-gray-900">Firmayı Düzenle</h2>
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-2 px-1">Firma bilgilerini güncelle</p>
</div>
<button
onClick={() => setIsEditModalOpen(false)}
className="p-3 hover:bg-gray-50 rounded-2xl text-gray-400 transition"
>
<X size={24} />
</button>
</div>
<form onSubmit={handleUpdateMerchant} className="space-y-8">
<div className="space-y-6">
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Firma Adı</label>
<div className="relative">
<Building2 className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="text"
required
value={editingMerchant?.name || ''}
onChange={(e) => setEditingMerchant({ ...editingMerchant, name: e.target.value })}
placeholder="Örn: Ayris Teknoloji"
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-bold text-gray-900 placeholder:text-gray-300"
/>
</div>
</div>
<div className="space-y-4">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Ödeme Altyapısı (Gateway)</label>
<div className="grid grid-cols-2 gap-3">
{[
{ id: 'stripe', name: 'Stripe' },
{ id: 'cryptomus', name: 'Cryptomus' },
{ id: 'nuvei', name: 'Nuvei' },
{ id: 'paykings', name: 'PayKings' },
{ id: 'securionpay', name: 'SecurionPay' },
].map((p) => (
<button
key={p.id}
type="button"
onClick={() => setEditingMerchant({ ...editingMerchant, payment_provider: p.id })}
className={`px-4 py-3 rounded-xl border text-xs font-bold transition-all ${editingMerchant?.payment_provider === p.id
? 'border-blue-500 bg-blue-50 text-blue-600'
: 'border-gray-100 bg-gray-50 text-gray-400 hover:bg-gray-100'}`}
>
{p.name}
</button>
))}
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Webhook URL (Geri Dönüş)</label>
<div className="relative">
<Globe className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="url"
value={editingMerchant?.webhook_url || ''}
onChange={(e) => setEditingMerchant({ ...editingMerchant, webhook_url: e.target.value })}
placeholder="https://siteniz.com/webhook"
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-bold text-gray-900 placeholder:text-gray-300"
/>
</div>
<p className="text-[10px] text-gray-400 font-medium px-1">Ödeme başarılı olduğunda bu adrese bildirim gönderilecektir.</p>
</div>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="flex-1 py-4 text-gray-400 font-black text-sm uppercase tracking-widest hover:text-gray-600 transition"
>
İptal
</button>
<button
type="submit"
disabled={isUpdating}
className="flex-[2] py-4 bg-blue-600 text-white rounded-2xl font-black text-sm uppercase tracking-widest hover:bg-blue-700 transition shadow-xl shadow-blue-100 disabled:opacity-50"
>
{isUpdating ? 'Güncelleniyor...' : 'Değişiklikleri Kaydet'}
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -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>

View File

@@ -39,11 +39,11 @@ export default function SettingsPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Mağaza Adı</label>
<input type="text" defaultValue="froyd Store" className="w-full px-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-500 outline-none" />
<input type="text" defaultValue="P2CGateway" className="w-full px-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-500 outline-none" />
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Destek E-postası</label>
<input type="email" defaultValue="support@froyd.io" className="w-full px-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-500 outline-none" />
<input type="email" defaultValue="support@ayris.dev" className="w-full px-6 py-4 bg-gray-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-500 outline-none" />
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Para Birimi</label>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase';
import { supabaseAdmin } from '@/lib/supabase-admin';
import {
Search,
Filter,
@@ -9,37 +9,72 @@ import {
} from 'lucide-react';
import { format } from 'date-fns';
import { tr } from 'date-fns/locale';
import TransactionSearch from '@/components/admin/TransactionSearch';
import TransactionStatusFilter from '@/components/admin/TransactionStatusFilter';
async function getTransactions() {
const { data, error } = await supabaseAdmin
async function getTransactions(filters: { merchant_id?: string; q?: string; status?: string }) {
let query = supabaseAdmin
.from('transactions')
.select('*')
.select('*, merchants(name)')
.order('created_at', { ascending: false });
if (error) return [];
if (filters.merchant_id) {
query = query.eq('merchant_id', filters.merchant_id);
}
if (filters.status) {
query = query.eq('status', filters.status);
}
if (filters.q) {
// First, search for merchants matching the name to get their IDs
const { data: matchedMerchants } = await supabaseAdmin
.from('merchants')
.select('id')
.ilike('name', `%${filters.q}%`);
const merchantIds = matchedMerchants?.map(m => m.id) || [];
// Construct OR query parts
let orParts = [
`stripe_pi_id.ilike.%${filters.q}%`,
`source_ref_id.ilike.%${filters.q}%`,
`customer_name.ilike.%${filters.q}%`
];
if (merchantIds.length > 0) {
orParts.push(`merchant_id.in.(${merchantIds.join(',')})`);
}
query = query.or(orParts.join(','));
}
const { data, error } = await query;
if (error) {
console.error('Fetch error:', error);
return [];
}
return data;
}
export default async function TransactionsPage() {
const transactions = await getTransactions();
export default async function TransactionsPage(props: {
searchParams: Promise<{ merchant_id?: string; q?: string; status?: string }>;
}) {
const searchParams = await props.searchParams;
const transactions = await getTransactions({
merchant_id: searchParams.merchant_id,
q: searchParams.q,
status: searchParams.status
});
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Search and Filters 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 flex-1">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" size={20} />
<input
type="text"
placeholder="İşlem ID veya referans ile ara..."
className="w-full pl-12 pr-6 py-3 bg-gray-50 border-none rounded-2xl text-sm font-medium focus:ring-2 focus:ring-blue-500 outline-none placeholder:text-gray-300"
/>
</div>
<button className="flex items-center gap-2 px-6 py-3 bg-white border border-gray-100 rounded-2xl text-sm font-bold text-gray-600 hover:bg-gray-50 transition">
<Filter size={18} />
Filtreler
</button>
<TransactionSearch />
<TransactionStatusFilter />
</div>
<button className="flex items-center justify-center gap-2 px-6 py-3 bg-gray-900 text-white rounded-2xl text-sm font-bold hover:bg-gray-800 transition shadow-lg shadow-gray-200">
@@ -54,6 +89,7 @@ export default async function TransactionsPage() {
<table className="w-full text-left">
<thead>
<tr className="bg-gray-50/30 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
<th className="px-10 py-6">Firma</th>
<th className="px-10 py-6">İşlem ID</th>
<th className="px-10 py-6">Referans / Kaynak</th>
<th className="px-10 py-6">Tarih & Saat</th>
@@ -63,8 +99,15 @@ export default async function TransactionsPage() {
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{transactions.map((t) => (
{transactions.map((t: any) => (
<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-blue-600 uppercase tracking-wider">
{t.merchants?.name || 'Doğrudan'}
</span>
</div>
</td>
<td className="px-10 py-8">
<span className="text-sm font-black text-gray-900 uppercase">#{t.stripe_pi_id?.slice(-12).toUpperCase() || 'MOCK'}</span>
</td>