Files
igny8/frontend/src/pages/admin/AdminAllPaymentsPage.tsx
2025-12-07 04:28:46 +00:00

754 lines
33 KiB
TypeScript

/**
* Admin Payments Page
* Tabs: All Payments, Pending Approvals (approve/reject), Payment Methods (country-level configs + per-account methods)
*/
import { useEffect, useState } from 'react';
import { Filter, Loader2, AlertCircle, Check, X, RefreshCw, Plus, Trash, Star } from 'lucide-react';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import {
getAdminPayments,
getPendingPayments,
approvePayment,
rejectPayment,
getAdminPaymentMethodConfigs,
createAdminPaymentMethodConfig,
updateAdminPaymentMethodConfig,
deleteAdminPaymentMethodConfig,
getAdminAccountPaymentMethods,
createAdminAccountPaymentMethod,
updateAdminAccountPaymentMethod,
deleteAdminAccountPaymentMethod,
setAdminDefaultAccountPaymentMethod,
getAdminUsers,
type Payment,
type PaymentMethod,
type PaymentMethodConfig,
type AdminAccountPaymentMethod,
type AdminUser,
} from '../../services/billing.api';
type AdminPayment = Payment & { account_name?: string };
type TabType = 'all' | 'pending' | 'methods';
export default function AdminAllPaymentsPage() {
const [payments, setPayments] = useState<AdminPayment[]>([]);
const [pendingPayments, setPendingPayments] = useState<AdminPayment[]>([]);
const [paymentConfigs, setPaymentConfigs] = useState<PaymentMethodConfig[]>([]);
const [accounts, setAccounts] = useState<AdminUser[]>([]);
const [accountPaymentMethods, setAccountPaymentMethods] = useState<AdminAccountPaymentMethod[]>([]);
const [accountIdFilter, setAccountIdFilter] = useState<string>('');
const [selectedConfigIdForAccount, setSelectedConfigIdForAccount] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
const [statusFilter, setStatusFilter] = useState('all');
const [activeTab, setActiveTab] = useState<TabType>('all');
const [actionLoadingId, setActionLoadingId] = useState<number | null>(null);
const [rejectNotes, setRejectNotes] = useState<Record<number, string>>({});
const [newConfig, setNewConfig] = useState<{
country_code: string;
payment_method: PaymentMethod['type'];
display_name: string;
instructions?: string;
sort_order?: number;
is_enabled?: boolean;
}>({
country_code: '*',
payment_method: 'bank_transfer',
display_name: '',
instructions: '',
sort_order: 0,
is_enabled: true,
});
const [editingConfigId, setEditingConfigId] = useState<number | null>(null);
useEffect(() => {
loadAll();
}, []);
const loadAll = async () => {
try {
setLoading(true);
const [allData, pendingData, configsData, usersData] = await Promise.all([
getAdminPayments(),
getPendingPayments(),
getAdminPaymentMethodConfigs(),
getAdminUsers(),
]);
setPayments(allData.results || []);
setPendingPayments(pendingData.results || []);
setPaymentConfigs(configsData.results || []);
setAccounts(usersData.results || []);
} catch (err: any) {
setError(err.message || 'Failed to load payments');
} finally {
setLoading(false);
}
};
const filteredPayments = payments.filter((payment) => statusFilter === 'all' || payment.status === statusFilter);
const getStatusColor = (status: string) => {
switch (status) {
case 'succeeded':
case 'completed':
return 'success';
case 'processing':
case 'pending':
case 'pending_approval':
return 'warning';
case 'refunded':
return 'info';
default:
return 'error';
}
};
const handleApprove = async (id: number) => {
try {
setActionLoadingId(id);
await approvePayment(id);
await loadAll();
} catch (err: any) {
setError(err.message || 'Failed to approve payment');
} finally {
setActionLoadingId(null);
}
};
const handleReject = async (id: number) => {
try {
setActionLoadingId(id);
await rejectPayment(id, { notes: rejectNotes[id] || '' });
await loadAll();
} catch (err: any) {
setError(err.message || 'Failed to reject payment');
} finally {
setActionLoadingId(null);
}
};
// Payment method configs (country-level)
const handleSaveConfig = async () => {
if (!newConfig.display_name.trim()) {
setError('Payment method display name is required');
return;
}
if (!newConfig.payment_method) {
setError('Payment method type is required');
return;
}
try {
setActionLoadingId(-1);
if (editingConfigId) {
await updateAdminPaymentMethodConfig(editingConfigId, {
country_code: newConfig.country_code || '*',
payment_method: newConfig.payment_method,
display_name: newConfig.display_name,
instructions: newConfig.instructions,
sort_order: newConfig.sort_order,
is_enabled: newConfig.is_enabled ?? true,
});
} else {
await createAdminPaymentMethodConfig({
country_code: newConfig.country_code || '*',
payment_method: newConfig.payment_method,
display_name: newConfig.display_name,
instructions: newConfig.instructions,
sort_order: newConfig.sort_order,
is_enabled: newConfig.is_enabled ?? true,
});
}
setNewConfig({
country_code: '*',
payment_method: 'bank_transfer',
display_name: '',
instructions: '',
sort_order: 0,
is_enabled: true,
});
setEditingConfigId(null);
const cfgs = await getAdminPaymentMethodConfigs();
setPaymentConfigs(cfgs.results || []);
} catch (err: any) {
setError(err.message || 'Failed to add payment method config');
} finally {
setActionLoadingId(null);
}
};
const handleToggleConfigEnabled = async (cfg: PaymentMethodConfig) => {
try {
setActionLoadingId(cfg.id);
await updateAdminPaymentMethodConfig(cfg.id, { is_enabled: !cfg.is_enabled });
const cfgs = await getAdminPaymentMethodConfigs();
setPaymentConfigs(cfgs.results || []);
} catch (err: any) {
setError(err.message || 'Failed to update payment method config');
} finally {
setActionLoadingId(null);
}
};
const handleDeleteConfig = async (id: number) => {
try {
setActionLoadingId(id);
await deleteAdminPaymentMethodConfig(id);
const cfgs = await getAdminPaymentMethodConfigs();
setPaymentConfigs(cfgs.results || []);
} catch (err: any) {
setError(err.message || 'Failed to delete payment method config');
} finally {
setActionLoadingId(null);
}
};
const handleEditConfig = (cfg: PaymentMethodConfig) => {
setEditingConfigId(cfg.id);
setNewConfig({
country_code: cfg.country_code,
payment_method: cfg.payment_method,
display_name: cfg.display_name,
instructions: cfg.instructions,
sort_order: cfg.sort_order,
is_enabled: cfg.is_enabled,
});
};
const handleCancelConfigEdit = () => {
setEditingConfigId(null);
setNewConfig({
country_code: '*',
payment_method: 'bank_transfer',
display_name: '',
instructions: '',
sort_order: 0,
is_enabled: true,
});
};
// Account payment methods
const handleLoadAccountMethods = async () => {
const accountId = accountIdFilter.trim();
if (!accountId) {
setAccountPaymentMethods([]);
return;
}
try {
setActionLoadingId(-3);
const data = await getAdminAccountPaymentMethods({ account_id: Number(accountId) });
setAccountPaymentMethods(data.results || []);
} catch (err: any) {
setError(err.message || 'Failed to load account payment methods');
} finally {
setActionLoadingId(null);
}
};
// Associate an existing country-level config to the account (one per account)
const handleAssociateConfigToAccount = async () => {
const accountId = accountIdFilter.trim();
if (!accountId) {
setError('Select an account first');
return;
}
if (!selectedConfigIdForAccount) {
setError('Select a payment method config to assign');
return;
}
const cfg = paymentConfigs.find((c) => c.id === selectedConfigIdForAccount);
if (!cfg) {
setError('Selected config not found');
return;
}
try {
setActionLoadingId(-2);
// Create or replace with the chosen config; treat as association.
const created = await createAdminAccountPaymentMethod({
account: Number(accountId),
type: cfg.payment_method,
display_name: cfg.display_name,
instructions: cfg.instructions,
is_enabled: cfg.is_enabled,
is_default: true,
});
// Remove extras if more than one exists for this account to enforce single association.
const refreshed = await getAdminAccountPaymentMethods({ account_id: Number(accountId) });
const others = (refreshed.results || []).filter((m) => m.id !== created.id);
for (const other of others) {
await deleteAdminAccountPaymentMethod(other.id);
}
await handleLoadAccountMethods();
} catch (err: any) {
setError(err.message || 'Failed to assign payment method to account');
} finally {
setActionLoadingId(null);
}
};
const handleDeleteAccountMethod = async (id: number | string) => {
try {
setActionLoadingId(Number(id));
await deleteAdminAccountPaymentMethod(id);
await handleLoadAccountMethods();
} catch (err: any) {
setError(err.message || 'Failed to delete account payment method');
} finally {
setActionLoadingId(null);
}
};
const handleSetDefaultAccountMethod = async (id: number | string) => {
try {
setActionLoadingId(Number(id));
await setAdminDefaultAccountPaymentMethod(id);
await handleLoadAccountMethods();
} catch (err: any) {
setError(err.message || 'Failed to set default account payment method');
} finally {
setActionLoadingId(null);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
</div>
);
}
const renderPaymentsTable = (rows: AdminPayment[]) => (
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Account</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Invoice</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{rows.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">No payments found</td>
</tr>
) : (
rows.map((payment) => (
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 font-medium">{payment.account_name}</td>
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{payment.invoice_number || payment.invoice_id || '—'}
</td>
<td className="px-6 py-4 font-semibold">{payment.currency} {payment.amount}</td>
<td className="px-6 py-4 text-sm capitalize">{payment.payment_method.replace('_', ' ')}</td>
<td className="px-6 py-4">
<Badge variant="light" color={getStatusColor(payment.status)}>
{payment.status}
</Badge>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{new Date(payment.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 text-right">
<button className="text-blue-600 hover:text-blue-700 text-sm">View</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
);
const renderPendingTable = () => (
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Account</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Invoice</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Reference</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{pendingPayments.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">No pending payments</td>
</tr>
) : (
pendingPayments.map((payment) => (
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 font-medium">{payment.account_name}</td>
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{payment.invoice_number || payment.invoice_id || '—'}
</td>
<td className="px-6 py-4 font-semibold">{payment.currency} {payment.amount}</td>
<td className="px-6 py-4 text-sm capitalize">{payment.payment_method.replace('_', ' ')}</td>
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{payment.transaction_reference || '—'}
</td>
<td className="px-6 py-4 text-right flex items-center justify-end gap-2">
<button
className="inline-flex items-center gap-1 text-green-600 hover:text-green-700 text-sm px-2 py-1 border border-green-200 rounded"
disabled={actionLoadingId === payment.id}
onClick={() => handleApprove(payment.id as number)}
>
{actionLoadingId === payment.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Approve
</button>
<div className="flex items-center gap-2">
<input
className="text-sm px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800"
placeholder="Rejection notes"
value={rejectNotes[payment.id as number] || ''}
onChange={(e) => setRejectNotes({ ...rejectNotes, [payment.id as number]: e.target.value })}
/>
<button
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 text-sm px-2 py-1 border border-red-200 rounded"
disabled={actionLoadingId === payment.id}
onClick={() => handleReject(payment.id as number)}
>
{actionLoadingId === payment.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <X className="w-4 h-4" />}
Reject
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
);
return (
<div className="p-6">
<div className="mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Payments</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Admin-only billing management
</p>
</div>
<button
onClick={loadAll}
className="inline-flex items-center gap-2 px-3 py-2 text-sm border border-gray-300 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-800"
>
<RefreshCw className="w-4 h-4" />
Refresh
</button>
</div>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-600" />
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<div className="mb-4 flex items-center gap-4">
<div className="flex gap-2">
<button
className={`px-3 py-2 rounded-lg text-sm border ${activeTab === 'all' ? 'border-blue-500 text-blue-600' : 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300'}`}
onClick={() => setActiveTab('all')}
>
All Payments
</button>
<button
className={`px-3 py-2 rounded-lg text-sm border ${activeTab === 'pending' ? 'border-blue-500 text-blue-600' : 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300'}`}
onClick={() => setActiveTab('pending')}
>
Pending Approvals
</button>
<button
className={`px-3 py-2 rounded-lg text-sm border ${activeTab === 'methods' ? 'border-blue-500 text-blue-600' : 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300'}`}
onClick={() => setActiveTab('methods')}
>
Payment Methods
</button>
</div>
{activeTab === 'all' && (
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-gray-400" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
>
<option value="all">All Status</option>
<option value="pending_approval">Pending Approval</option>
<option value="processing">Processing</option>
<option value="succeeded">Succeeded</option>
<option value="completed">Completed</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
<option value="cancelled">Cancelled</option>
<option value="refunded">Refunded</option>
</select>
</div>
)}
</div>
{activeTab === 'all' && renderPaymentsTable(filteredPayments)}
{activeTab === 'pending' && renderPendingTable()}
{activeTab === 'methods' && (
<div className="space-y-6">
{/* Payment Method Configs (country-level) */}
<Card className="p-4">
<h3 className="text-lg font-semibold mb-3">Payment Method Configs (country-level)</h3>
<div className="flex flex-col lg:flex-row gap-3">
<input
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-32"
placeholder="Country (e.g., *, US)"
value={newConfig.country_code}
onChange={(e) => setNewConfig({ ...newConfig, country_code: e.target.value })}
/>
<select
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-40"
value={newConfig.payment_method}
onChange={(e) => setNewConfig({ ...newConfig, payment_method: e.target.value as PaymentMethod['type'] })}
>
<option value="bank_transfer">Bank Transfer</option>
<option value="local_wallet">Manual (local wallet)</option>
<option value="stripe">Stripe</option>
<option value="paypal">PayPal</option>
</select>
<input
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded"
placeholder="Display name"
value={newConfig.display_name}
onChange={(e) => setNewConfig({ ...newConfig, display_name: e.target.value })}
/>
<input
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded"
placeholder="Instructions (optional)"
value={newConfig.instructions || ''}
onChange={(e) => setNewConfig({ ...newConfig, instructions: e.target.value })}
/>
<input
type="number"
className="w-28 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded"
placeholder="Sort"
value={newConfig.sort_order ?? 0}
onChange={(e) => setNewConfig({ ...newConfig, sort_order: Number(e.target.value) })}
/>
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={!!newConfig.is_enabled}
onChange={(e) => setNewConfig({ ...newConfig, is_enabled: e.target.checked })}
/>
Enabled
</label>
<button
className="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
onClick={handleSaveConfig}
disabled={actionLoadingId === -1}
>
{actionLoadingId === -1 ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
{editingConfigId ? 'Save' : 'Add'}
</button>
{editingConfigId && (
<button
className="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50"
onClick={handleCancelConfigEdit}
disabled={actionLoadingId === -1}
>
Cancel
</button>
)}
</div>
</Card>
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Country</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Enabled</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Instructions</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{paymentConfigs.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">No payment method configs</td>
</tr>
) : (
paymentConfigs.map((cfg) => (
<tr key={cfg.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 font-medium">{cfg.country_code}</td>
<td className="px-6 py-4 font-medium">{cfg.display_name}</td>
<td className="px-6 py-4 text-sm capitalize">{cfg.payment_method.replace('_', ' ')}</td>
<td className="px-6 py-4 text-sm">
<button
className="text-blue-600 hover:text-blue-700 text-sm mr-3"
onClick={() => handleToggleConfigEnabled(cfg)}
disabled={actionLoadingId === cfg.id}
>
{actionLoadingId === cfg.id ? <Loader2 className="w-4 h-4 animate-spin" /> : cfg.is_enabled ? 'Disable' : 'Enable'}
</button>
<button
className="text-blue-600 hover:text-blue-700 text-sm"
onClick={() => handleEditConfig(cfg)}
disabled={actionLoadingId === cfg.id}
>
Edit
</button>
</td>
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{cfg.instructions || '—'}
</td>
<td className="px-6 py-4 text-right">
<button
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 text-sm px-2 py-1 border border-red-200 rounded"
onClick={() => handleDeleteConfig(cfg.id)}
disabled={actionLoadingId === cfg.id}
>
{actionLoadingId === cfg.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash className="w-4 h-4" />}
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
{/* Account Payment Methods (associate existing configs only) */}
<Card className="p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold">Account Payment Methods (association)</h3>
<div className="flex items-center gap-3">
<select
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-52"
value={accountIdFilter}
onChange={(e) => setAccountIdFilter(e.target.value)}
>
<option value="">Select account</option>
{accounts.map((acc) => (
<option key={acc.id} value={acc.id}>
{acc.account_name || acc.email || acc.id}
</option>
))}
</select>
<button
className="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50"
onClick={handleLoadAccountMethods}
disabled={!accountIdFilter}
>
{actionLoadingId === -3 ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
Load
</button>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-3 mb-4">
<select
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-64"
value={selectedConfigIdForAccount ?? ''}
onChange={(e) => setSelectedConfigIdForAccount(e.target.value ? Number(e.target.value) : null)}
disabled={!accountIdFilter}
>
<option value="">Select payment method config</option>
{paymentConfigs.map((cfg) => (
<option key={cfg.id} value={cfg.id}>
{cfg.display_name} ({cfg.payment_method.replace('_', ' ')} - {cfg.country_code})
</option>
))}
</select>
<button
className="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
onClick={handleAssociateConfigToAccount}
disabled={!accountIdFilter || !selectedConfigIdForAccount || actionLoadingId === -2}
>
{actionLoadingId === -2 ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
Assign to account
</button>
<p className="text-sm text-gray-600 dark:text-gray-400">
Only one payment method per account; assigning replaces existing.
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Account</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Enabled</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Default</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Instructions</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{accountPaymentMethods.length === 0 ? (
<tr>
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">No account payment methods</td>
</tr>
) : (
accountPaymentMethods.map((m) => (
<tr key={m.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="px-6 py-4 font-medium">{m.account}</td>
<td className="px-6 py-4 font-medium">{m.display_name}</td>
<td className="px-6 py-4 text-sm capitalize">{m.type.replace('_', ' ')}</td>
<td className="px-6 py-4 text-sm">{m.is_enabled ? 'Yes' : 'No'}</td>
<td className="px-6 py-4 text-sm">{m.is_default ? <Star className="w-4 h-4 text-yellow-500" /> : '—'}</td>
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{m.instructions || '—'}
</td>
<td className="px-6 py-4 text-right space-x-2">
{!m.is_default && (
<button
className="text-blue-600 hover:text-blue-700 text-sm"
onClick={() => handleSetDefaultAccountMethod(m.id)}
disabled={actionLoadingId === Number(m.id)}
>
{actionLoadingId === Number(m.id) ? <Loader2 className="w-4 h-4 animate-spin inline" /> : 'Set default'}
</button>
)}
<button
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 text-sm px-2 py-1 border border-red-200 rounded"
onClick={() => handleDeleteAccountMethod(m.id)}
disabled={actionLoadingId === Number(m.id)}
>
{actionLoadingId === Number(m.id) ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash className="w-4 h-4" />}
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</Card>
</div>
)}
</div>
);
}