first commit
This commit is contained in:
177
app/admin/analytics/page.tsx
Normal file
177
app/admin/analytics/page.tsx
Normal 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ı Aç
|
||||
</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>
|
||||
);
|
||||
}
|
||||
179
app/admin/customers/page.tsx
Normal file
179
app/admin/customers/page.tsx
Normal 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
137
app/admin/layout.tsx
Normal 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
327
app/admin/page.tsx
Normal 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
138
app/admin/settings/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
128
app/admin/transactions/page.tsx
Normal file
128
app/admin/transactions/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user