first commit

This commit is contained in:
mstfyldz
2026-01-18 16:48:15 +03:00
parent 68ba54fa4f
commit af09543374
29 changed files with 2666 additions and 82 deletions

View File

@@ -0,0 +1,177 @@
import React from 'react';
import {
BarChart3,
TrendingUp,
ArrowUpRight,
ArrowDownRight,
Globe,
Monitor,
Smartphone,
Calendar
} from 'lucide-react';
import { supabaseAdmin } from '@/lib/supabase';
import { format, subDays } from 'date-fns';
import { tr } from 'date-fns/locale';
async function getAnalyticsData() {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*')
.order('created_at', { ascending: true });
if (error || !transactions) return null;
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
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);
return {
date: d.toISOString().split('T')[0],
label: format(d, 'd MMM', { locale: tr }),
amount: 0
};
});
successfulTransactions.forEach(t => {
const dateStr = new Date(t.created_at).toISOString().split('T')[0];
const periodMatch = last12Periods.find(p => p.date === dateStr);
if (periodMatch) {
periodMatch.amount += Number(t.amount);
}
});
return {
totalRevenue,
avgOrderValue,
chartData: last12Periods,
totalCount: transactions.length,
successCount: successfulTransactions.length,
};
}
export default async function AnalyticsPage() {
const data = await getAnalyticsData();
if (!data) return <div className="p-10 font-black text-gray-400 uppercase tracking-[0.2em] animate-pulse">Veriler hazırlanıyor...</div>;
const metrics = [
{ label: 'Dönüşüm Oranı', value: '3.24%', trend: '+0.8%', positive: true }, // Mocked for now
{ label: 'Ort. Sipariş Tutarı', value: `${data.avgOrderValue.toLocaleString('tr-TR', { maximumFractionDigits: 2 })} ₺`, trend: '+12%', positive: true },
{ label: 'Başarılı İşlem', value: data.successCount.toString(), trend: '+24%', positive: true },
{ label: 'İşlem Başarısı', value: `${((data.successCount / (data.totalCount || 1)) * 100).toFixed(1)}%`, trend: '-0.2%', positive: false },
];
const maxChartAmount = Math.max(...data.chartData.map(d => d.amount), 100);
return (
<div className="space-y-10 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">
<div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Analizler</h1>
<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>
</div>
</div>
{/* Main Metrics */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{metrics.map((item, i) => (
<div key={i} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm space-y-4">
<p className="text-[10px] text-gray-400 font-black uppercase tracking-[0.2em]">{item.label}</p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-black text-gray-900 leading-none">{item.value}</h3>
<div className={`flex items-center gap-1 text-[10px] font-black uppercase tracking-tighter ${item.positive ? 'text-emerald-500' : 'text-red-500'}`}>
{item.positive ? <ArrowUpRight size={14} /> : <ArrowDownRight size={14} />}
{item.trend}
</div>
</div>
</div>
))}
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Performance Chart */}
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-10 text-sans tracking-tight">
<div className="flex justify-between items-center">
<h3 className="text-xl font-black text-gray-900 uppercase tracking-tight">Ciro Trendi (12 Gün)</h3>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">Gerçekleşen</span>
</div>
</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}
</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>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,179 @@
import React from 'react';
import {
Users,
Search,
Plus,
Mail,
Phone,
MoreHorizontal,
ArrowUpRight
} from 'lucide-react';
import { supabaseAdmin } from '@/lib/supabase';
async function getCustomers() {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*');
if (error || !transactions) return null;
// Group transactions by name or phone
const customerMap = new Map();
transactions.forEach(t => {
const key = t.customer_name || t.customer_phone || 'Unknown';
if (!customerMap.has(key)) {
customerMap.set(key, {
id: t.id,
name: t.customer_name || 'İsimsiz Müşteri',
phone: t.customer_phone || 'Telefon Yok',
spent: 0,
orders: 0,
status: 'New'
});
}
const c = customerMap.get(key);
c.orders += 1;
if (t.status === 'succeeded') {
c.spent += Number(t.amount);
}
});
const customers = Array.from(customerMap.values()).map(c => {
if (c.orders > 5) c.status = 'High Value';
else if (c.orders > 1) c.status = 'Active';
return c;
});
return customers;
}
export default async function CustomersPage() {
const customers = await getCustomers();
if (!customers) return <div className="p-10 font-black text-gray-400 uppercase tracking-widest animate-pulse">Müşteriler yükleniyor...</div>;
return (
<div className="space-y-10 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">
<div>
<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>
{/* 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">
<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>
</div>
</div>
<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-emerald-50 rounded-[20px] flex items-center justify-center text-emerald-600">
<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>
</div>
</div>
<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-orange-50 rounded-[20px] flex items-center justify-center text-orange-600">
<Phone size={32} />
</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>
</div>
</div>
</div>
{/* 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"
/>
</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">
<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-8">Müşteri Bilgileri</th>
<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>
</tr>
</thead>
<tbody className="divide-y divide-gray-50">
{customers.map((customer, i) => (
<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">
{customer.name.slice(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-sm font-black text-gray-900 uppercase tracking-tight">{customer.name}</span>
<span className="text-[10px] text-gray-400 font-bold mt-1 tracking-widest">{customer.phone}</span>
</div>
</div>
</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 === 'New' ? 'bg-indigo-50 text-indigo-600' :
'bg-gray-50 text-gray-400'
}`}>
{customer.status === 'Active' ? 'Aktif' :
customer.status === 'High Value' ? 'VIP' :
customer.status === 'New' ? 'Yeni Üye' : 'İnaktif'}
</span>
</td>
<td className="px-10 py-10">
<span className="text-sm font-black text-gray-900">{customer.orders}</span>
</td>
<td className="px-10 py-10">
<span className="text-sm font-black text-gray-900">
{customer.spent.toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
</span>
</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>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

137
app/admin/layout.tsx Normal file
View File

@@ -0,0 +1,137 @@
'use client';
import React from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import {
LayoutDashboard,
CreditCard,
Users,
BarChart3,
Settings,
LogOut,
Search,
Bell,
MessageSquare,
ChevronDown,
Wallet
} from 'lucide-react';
import Image from 'next/image';
import { createClient } from '@/utils/supabase/client'; // Assuming a client-side Supabase client utility
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();
const router = useRouter();
const supabase = createClient();
const handleSignOut = async () => {
await supabase.auth.signOut();
router.push('/login');
router.refresh();
};
const navItems = [
{ label: 'Genel Bakış', icon: LayoutDashboard, href: '/admin' },
{ label: 'İşlemler', icon: CreditCard, href: '/admin/transactions' },
{ label: 'Müşteriler', icon: Users, href: '/admin/customers' },
{ label: 'Analizler', icon: BarChart3, href: '/admin/analytics' },
{ label: 'Ayarlar', icon: Settings, href: '/admin/settings' },
];
return (
<div className="flex h-screen bg-[#F8FAFC]">
{/* Sidebar */}
<aside className="w-72 bg-white border-r border-gray-100 flex flex-col shrink-0">
<div className="p-8 flex items-center gap-4">
<div className="w-12 h-12 bg-[#2563EB] rounded-xl flex items-center justify-center text-white shadow-lg shadow-blue-100">
<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>
</div>
</div>
<nav className="flex-1 px-4 mt-4 space-y-2">
{navItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.label}
href={item.label === 'Müşteriler' || item.label === 'Analizler' || item.label === 'Ayarlar' ? (item.href === pathname ? item.href : item.href) : item.href}
className={`flex items-center gap-4 px-6 py-4 rounded-2xl text-sm font-bold transition-all duration-200 group ${isActive
? 'bg-blue-50 text-blue-600'
: 'text-gray-400 hover:text-gray-900 hover:bg-gray-50'
}`}
>
<item.icon size={22} className={isActive ? 'text-blue-600' : 'text-gray-300 group-hover:text-gray-500'} />
{item.label}
</Link>
);
})}
</nav>
<div className="p-6 border-t border-gray-50">
<button
onClick={handleSignOut}
className="flex items-center gap-4 px-6 py-4 text-sm font-bold text-gray-400 hover:text-red-500 hover:bg-red-50 w-full rounded-2xl transition-all duration-200 group"
>
<LogOut size={22} className="text-gray-300 group-hover:text-red-500" />
Çıkış Yap
</button>
</div>
</aside>
{/* 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 gap-8">
<div className="flex-1 flex items-center">
<h2 className="text-xl font-black text-gray-900 mr-8 shrink-0">Yönetim Paneli</h2>
<div className="relative max-w-md w-full">
<Search className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
<input
type="text"
placeholder="İşlemlerde ara..."
className="w-full pl-14 pr-6 py-4 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>
</div>
<div className="flex items-center gap-8">
<div className="flex items-center gap-4">
<button className="relative p-2 text-gray-400 hover:text-blue-600 transition">
<Bell size={24} />
<span className="absolute top-2 right-2 w-2 h-2 bg-blue-600 rounded-full border-2 border-white"></span>
</button>
<button className="p-2 text-gray-400 hover:text-blue-600 transition">
<MessageSquare size={24} />
</button>
</div>
<div className="flex items-center gap-4 pl-8 border-l border-gray-100">
<div className="text-right">
<p className="text-sm font-black text-gray-900 leading-none">Admin</p>
<p className="text-[10px] text-blue-600 font-bold uppercase tracking-wider mt-1">Süper Admin</p>
</div>
<div className="relative w-12 h-12 rounded-2xl bg-orange-100 overflow-hidden ring-4 ring-orange-50">
<div className="absolute inset-0 flex items-center justify-center text-orange-500 font-bold font-mono">AD</div>
{/* Fallback avatar if needed */}
</div>
<ChevronDown size={18} className="text-gray-300" />
</div>
</div>
</header>
{/* Content Area */}
<main className="flex-1 overflow-y-auto p-10">
{children}
</main>
</div>
</div>
);
}

327
app/admin/page.tsx Normal file
View File

@@ -0,0 +1,327 @@
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase';
import {
TrendingUp,
TrendingDown,
Users,
Wallet,
ClipboardList,
CheckCircle2,
} from 'lucide-react';
import { format } from 'date-fns';
import { tr } from 'date-fns/locale';
import Link from 'next/link';
async function getStats() {
const { data: transactions, error } = await supabaseAdmin
.from('transactions')
.select('*')
.order('created_at', { ascending: false });
if (error || !transactions) 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 pendingCount = transactions.filter(t => t.status === 'pending').length;
const totalCount = transactions.length;
const successRate = totalCount > 0 ? (successfulCount / totalCount) * 100 : 0;
// Calculate unique customers
const uniqueCustomers = new Set(
transactions
.filter(t => t.customer_name || t.customer_phone)
.map(t => t.customer_name || t.customer_phone)
).size;
// Last 30 days chart data
const last30Days = 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 = last30Days.find(d => d.date === dateStr);
if (dayMatch) {
dayMatch.amount += Number(t.amount);
}
});
return {
transactions,
totalRevenue,
successfulCount,
pendingCount,
successRate,
totalCount,
uniqueCustomers,
chartData: last30Days
};
}
export default async function AdminDashboard() {
const stats = await getStats();
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>;
}
const recentTransactions = stats.transactions.slice(0, 5);
return (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Top Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{/* Total Revenue */}
<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 Ciro</p>
<div className="p-3 bg-blue-50 rounded-xl text-blue-600">
<Wallet size={20} />
</div>
</div>
<div>
<h3 className="text-3xl font-black text-gray-900">{stats.totalRevenue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })} </h3>
<div className="flex items-center gap-1 mt-2 text-emerald-500 font-bold text-xs uppercase tracking-tighter">
<TrendingUp size={14} />
<span>Sistem Aktif <span className="text-gray-400 font-medium lowercase">gerçek zamanlı veri</span></span>
</div>
</div>
</div>
{/* Total 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">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>
<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>
</div>
</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>
<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>
</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>
</div>
{/* Bottom Section: Recent Transactions Table */}
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden text-sans tracking-tight">
<div className="p-8 border-b border-gray-50 flex justify-between items-center text-sans tracking-tight">
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tight">Son İşlemler</h2>
<Link href="/admin/transactions" className="text-blue-600 text-xs font-black uppercase tracking-widest hover:underline decoration-2 underline-offset-4">
Tümünü Gör
</Link>
</div>
<div className="overflow-x-auto text-sans tracking-tight">
<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">İşlem ID</th>
<th className="px-10 py-6">Müşteri / Ref</th>
<th className="px-10 py-6">Tarih</th>
<th className="px-10 py-6 text-right pr-20">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">
<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-[10px] text-gray-400 font-bold uppercase tracking-wider mt-1">{t.id.slice(0, 8)}</span>
</div>
</td>
<td className="px-10 py-8">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center text-blue-600 font-black text-xs uppercase tracking-tighter">
{t.customer_name ? t.customer_name.slice(0, 2).toUpperCase() : (t.source_ref_id ? t.source_ref_id.slice(0, 2).toUpperCase() : 'PI')}
</div>
<div className="flex flex-col">
<span className="text-sm font-black text-gray-900">{t.customer_name || t.source_ref_id || 'Sistem Ödemesi'}</span>
<span className="text-[10px] text-gray-400 font-bold truncate max-w-[150px] mt-1">{t.customer_phone || t.callback_url || 'doğrudan-ödeme'}</span>
</div>
</div>
</td>
<td className="px-10 py-8">
<span className="text-xs font-bold text-gray-500 uppercase">
{format(new Date(t.created_at), 'dd MMM yyyy', { locale: tr })}
</span>
</td>
<td className="px-10 py-8 text-right pr-20">
<span className="text-sm font-black text-gray-900">
{Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
</span>
</td>
<td className="px-10 py-8">
<div className="flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ${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>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

138
app/admin/settings/page.tsx Normal file
View File

@@ -0,0 +1,138 @@
'use client';
import React from 'react';
import {
Globe,
ShieldCheck,
Bell,
Trash2,
Smartphone,
Monitor,
} from 'lucide-react';
export default function SettingsPage() {
return (
<div className="max-w-4xl space-y-10 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">
<div>
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Ayarlar</h1>
<p className="text-sm text-gray-400 font-bold uppercase tracking-widest mt-2">Platform tercihlerinizi yönetin</p>
</div>
<button className="px-8 py-4 bg-gray-900 text-white rounded-2xl font-black text-sm shadow-xl shadow-gray-200 hover:bg-gray-800 transition active:scale-95">
Değişiklikleri Kaydet
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
{/* Left Column: Sections */}
<div className="lg:col-span-2 space-y-8">
{/* General Section */}
<section className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
<div className="flex items-center gap-4 border-b border-gray-50 pb-6">
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
<Globe size={24} />
</div>
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Genel Ayarlar</h2>
</div>
<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" />
</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" />
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Para Birimi</label>
<select 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 appearance-none">
<option>Türk Lirası ()</option>
<option>US Dollar ($)</option>
<option>Euro ()</option>
</select>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-2">Zaman Dilimi</label>
<select 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 appearance-none">
<option>Istanbul (GMT+3)</option>
<option>London (GMT+0)</option>
<option>New York (EST)</option>
</select>
</div>
</div>
</section>
{/* Security Section */}
<section className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
<div className="flex items-center gap-4 border-b border-gray-50 pb-6">
<div className="w-12 h-12 bg-emerald-50 rounded-2xl flex items-center justify-center text-emerald-600">
<ShieldCheck size={24} />
</div>
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Güvenlik</h2>
</div>
<div className="space-y-6">
<div className="flex items-center justify-between p-6 bg-gray-50 rounded-[32px]">
<div className="space-y-1">
<p className="text-sm font-black text-gray-900">İki Faktörlü Doğrulama</p>
<p className="text-[10px] text-gray-400 font-bold uppercase">Hesabınıza ekstra bir güvenlik katmanı ekleyin</p>
</div>
<div className="w-12 h-6 bg-blue-500 rounded-full relative cursor-pointer">
<div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
<div className="flex items-center justify-between p-6 bg-gray-50 rounded-[32px]">
<div className="space-y-1">
<p className="text-sm font-black text-gray-900">API Erişimi</p>
<p className="text-[10px] text-gray-400 font-bold uppercase">Harici uygulamalar için anahtar yönetimi</p>
</div>
<button className="text-blue-600 text-[10px] font-black uppercase tracking-widest hover:underline">Anahtarları Düzenle</button>
</div>
</div>
</section>
</div>
{/* Right Column: Notifications & Danger Zone */}
<div className="space-y-8">
<section className="bg-white p-8 rounded-[40px] border border-gray-100 shadow-sm space-y-6">
<div className="flex items-center gap-4 mb-2">
<div className="w-10 h-10 bg-orange-50 rounded-xl flex items-center justify-center text-orange-600">
<Bell size={20} />
</div>
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tight">Bildirimler</h2>
</div>
<div className="space-y-4">
{['Yeni Satışlar', 'Müşteri Mesajları', 'Sistem Güncellemeleri'].map((item) => (
<label key={item} className="flex items-center gap-4 cursor-pointer group">
<input type="checkbox" defaultChecked className="w-5 h-5 rounded-lg border-2 border-gray-100 text-blue-600 focus:ring-blue-500" />
<span className="text-sm font-bold text-gray-500 group-hover:text-gray-900 transition">{item}</span>
</label>
))}
</div>
</section>
<section className="bg-red-50 p-8 rounded-[40px] border border-red-100 space-y-6">
<div className="flex items-center gap-4 text-red-600">
<Trash2 size={20} />
<h2 className="text-lg font-black uppercase tracking-tight">Tehlikeli Bölge</h2>
</div>
<p className="text-xs font-bold text-red-400 leading-relaxed uppercase tracking-wider">
Mağaza verilerini kalıcı olarak silmek veya hesabı kapatmak için bu bölümü kullanın. Bu işlem geri alınamaz.
</p>
<button className="w-full py-4 bg-red-600 text-white rounded-2xl font-black text-sm hover:bg-red-700 transition shadow-lg shadow-red-100">
Mağazayı Devre Dışı Bırak
</button>
</section>
</div>
</div>
{/* Footer Meta */}
<div className="pt-12 text-center">
<p className="text-[10px] text-gray-300 font-black uppercase tracking-[0.4em]">v1.0.4 Platinum Enterprise Edition</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase';
import {
Search,
Filter,
Download,
ExternalLink,
MoreVertical
} from 'lucide-react';
import { format } from 'date-fns';
import { tr } from 'date-fns/locale';
async function getTransactions() {
const { data, error } = await supabaseAdmin
.from('transactions')
.select('*')
.order('created_at', { ascending: false });
if (error) return [];
return data;
}
export default async function TransactionsPage() {
const transactions = await getTransactions();
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>
</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">
<Download size={18} />
CSV Olarak İndir
</button>
</div>
{/* Full Transactions Table */}
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden text-sans tracking-tight">
<div className="overflow-x-auto">
<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">İşlem ID</th>
<th className="px-10 py-6">Referans / Kaynak</th>
<th className="px-10 py-6">Tarih & Saat</th>
<th className="px-10 py-6">Tutar</th>
<th className="px-10 py-6 text-center">Durum</th>
<th className="px-10 py-6 text-right">İşlemler</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">
<span className="text-sm font-black text-gray-900 uppercase">#{t.stripe_pi_id?.slice(-12).toUpperCase() || 'MOCK'}</span>
</td>
<td className="px-10 py-8">
<div className="flex flex-col">
<span className="text-sm font-bold text-gray-900">{t.customer_name || t.source_ref_id || 'Doğrudan Ödeme'}</span>
{t.customer_phone ? (
<span className="text-[10px] text-blue-600 font-black uppercase tracking-wider mt-1">{t.customer_phone}</span>
) : (
<span className="text-[10px] text-gray-400 font-bold truncate max-w-[200px] mt-1">{t.callback_url || 'Geri dönüş yok'}</span>
)}
</div>
</td>
<td className="px-10 py-8">
<span className="text-xs font-bold text-gray-500">
{format(new Date(t.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })}
</span>
</td>
<td className="px-10 py-8 font-black text-gray-900">
{Number(t.amount).toLocaleString('tr-TR', { minimumFractionDigits: 2 })} {t.currency.toUpperCase() === 'TRY' ? '₺' : t.currency.toUpperCase()}
</td>
<td className="px-10 py-8">
<div className="flex justify-center">
<span className={`inline-flex items-center px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ${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>
</div>
</td>
<td className="px-10 py-8 text-right">
<div className="flex items-center justify-end gap-2">
{t.callback_url && (
<a href={t.callback_url} target="_blank" className="p-2 text-gray-300 hover:text-blue-600 transition">
<ExternalLink size={18} />
</a>
)}
<button className="p-2 text-gray-300 hover:text-gray-900 transition">
<MoreVertical size={18} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{transactions.length === 0 && (
<div className="p-20 text-center space-y-4">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto text-gray-300">
<Search size={32} />
</div>
<p className="text-gray-400 font-bold uppercase tracking-widest text-xs">İşlem bulunamadı</p>
</div>
)}
</div>
</div>
);
}