Files
Pay2Gateway/app/admin/transactions/page.tsx
mstfyldz 3562e10713 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
2026-01-20 21:58:41 +03:00

172 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase-admin';
import {
Search,
Filter,
Download,
ExternalLink,
MoreVertical
} 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(filters: { merchant_id?: string; q?: string; status?: string }) {
let query = supabaseAdmin
.from('transactions')
.select('*, merchants(name)')
.order('created_at', { ascending: false });
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(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">
<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">
<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">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>
<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: 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>
<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>
);
}