import { useCallback, useEffect, useState } from 'react'; import { getErrorSummary, getSikshyaApi, SIKSHYA_ENDPOINTS } from '../api'; import { EmbeddableShell } from '../components/shared/EmbeddableShell'; import { ApiErrorPanel } from '../components/shared/ApiErrorPanel'; import { ListPanel } from '../components/shared/list/ListPanel'; import { ListEmptyState } from '../components/shared/list/ListEmptyState'; import { StatusBadge } from '../components/shared/list/StatusBadge'; import { ButtonPrimary } from '../components/shared/buttons'; import { RowActionsMenu, type RowActionItem } from '../components/shared/list/RowActionsMenu'; import { Modal } from '../components/shared/Modal'; import { BulkActionsBar } from '../components/shared/list/BulkActionsBar'; import { appViewHref } from '../lib/appUrl'; import { formatPostDate } from '../lib/formatPostDate'; import { useAsyncData } from '../hooks/useAsyncData'; import { useAdminRouting } from '../lib/adminRouting'; import { useSikshyaDialog } from '../components/shared/SikshyaDialogContext'; import type { SikshyaReactConfig } from '../types'; import { __ } from '../lib/i18n'; type PaymentRow = { id: number; user_id: number; course_id: number; amount: number; currency: string; payment_method: string; transaction_id: string; status: string; charge_kind?: string; payment_date: string; payer_name: string; payer_email: string; course_title: string; }; type ListResponse = { success?: boolean; payments?: PaymentRow[]; total?: number; pages?: number; page?: number; per_page?: number; table_missing?: boolean; }; export function PaymentsPage(props: { config: SikshyaReactConfig; title: string; embedded?: boolean }) { const { config, title, embedded } = props; const adminBase = config.adminUrl.replace(/\/?$/, '/'); const { navigateView } = useAdminRouting(); const dialog = useSikshyaDialog(); const [page, setPage] = useState(1); const [statusFilter, setStatusFilter] = useState(''); const [chargeKindFilter, setChargeKindFilter] = useState(''); const [editOpen, setEditOpen] = useState(false); const [editId, setEditId] = useState(null); const [editStatus, setEditStatus] = useState('pending'); const [saving, setSaving] = useState(false); const [selectedIds, setSelectedIds] = useState([]); const [bulkAction, setBulkAction] = useState(''); const [bulkBusy, setBulkBusy] = useState(false); useEffect(() => { setPage(1); }, [statusFilter, chargeKindFilter]); const loader = useCallback(async () => { const q = new URLSearchParams({ page: String(page), per_page: '30' }); if (statusFilter) { q.set('status', statusFilter); } if (chargeKindFilter) { q.set('charge_kind', chargeKindFilter); } return getSikshyaApi().get(`${SIKSHYA_ENDPOINTS.admin.payments}?${q.toString()}`); }, [page, statusFilter, chargeKindFilter]); const { loading, data, error, refetch } = useAsyncData(loader, [page, statusFilter, chargeKindFilter]); const rows = data?.payments ?? []; const total = data?.total ?? 0; const pages = data?.pages ?? 0; const tableMissing = Boolean(data?.table_missing); useEffect(() => { // Clear selection on page/filter changes so bulk actions never surprise the admin. setSelectedIds([]); setBulkAction(''); }, [page, statusFilter, chargeKindFilter]); const toggleAll = (checked: boolean) => { setSelectedIds(checked ? rows.map((r) => r.id) : []); }; const toggleOne = (id: number, checked: boolean) => { setSelectedIds((prev) => { const s = new Set(prev); if (checked) s.add(id); else s.delete(id); return Array.from(s); }); }; const applyBulk = async () => { if (bulkBusy || selectedIds.length === 0 || !bulkAction) return; if (bulkAction === 'delete') { const ok = await dialog.confirm({ title: `Delete ${selectedIds.length} payment(s)?`, message: __('This permanently removes the payment records. This cannot be undone.', 'sikshya'), confirmLabel: __('Delete', 'sikshya'), variant: 'danger', }); if (!ok) return; } setBulkBusy(true); try { if (bulkAction.startsWith('status:')) { const st = bulkAction.replace(/^status:/, ''); await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.paymentsBulk, { action: 'status', status: st, ids: selectedIds, }); } else if (bulkAction === 'delete') { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.paymentsBulk, { action: 'delete', ids: selectedIds }); } setSelectedIds([]); setBulkAction(''); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } finally { setBulkBusy(false); } }; const openEdit = (r: PaymentRow) => { setEditId(r.id); setEditStatus(r.status || 'pending'); setEditOpen(true); }; return ( refetch()}> Refresh } > {error ? (
refetch()} />
) : null} {tableMissing ? (
The payments database table is not installed yet. Payments will appear here after migrations and successful checkouts.
) : null} { if (!saving) setEditOpen(false); }} footer={
{ if (!editId) return; setSaving(true); try { await getSikshyaApi().patch<{ ok?: boolean; message?: string }>( SIKSHYA_ENDPOINTS.admin.paymentUpdate(editId), { status: editStatus } ); setEditOpen(false); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } finally { setSaving(false); } }} > {saving ? __('Saving…', 'sikshya') : __('Save', 'sikshya')}
} >
{tableMissing ? ( ) : ( <>
void applyBulk()} applyBusy={bulkBusy} trashMode={false} customOptions={[ { value: 'delete', label: 'Delete permanently' }, { value: 'status:pending', label: 'Mark pending' }, { value: 'status:completed', label: 'Mark completed' }, { value: 'status:failed', label: 'Mark failed' }, { value: 'status:refunded', label: 'Mark refunded' }, ]} selectId="payments-bulk" />
{selectedIds.length > 0 ? `${selectedIds.length} selected` : ''}
{loading ? ( ) : rows.length === 0 ? ( ) : ( rows.map((r) => ( )))}
0 && selectedIds.length === rows.length} onChange={(e) => toggleAll(e.target.checked)} /> {__('Date', 'sikshya')} {__('Payer', 'sikshya')} {__('Course', 'sikshya')} {__('Amount', 'sikshya')} {__('Status', 'sikshya')} {__('Actions', 'sikshya')}
Loading payments…
{statusFilter || chargeKindFilter ? 'No records match the current filters.' : 'Completed checkouts and renewal charges will list here for auditing and support.'}
toggleOne(r.id, e.target.checked)} /> {formatPostDate(r.payment_date)}
{r.transaction_id ? (
) : null}
e.stopPropagation()} > {r.payer_name || `User #${r.user_id}`}
{r.payer_email || '—'}
{r.course_id > 0 ? ( e.stopPropagation()} > {r.course_title || `Course #${r.course_id}`} ) : (r.charge_kind || '').toLowerCase() === 'renewal' ? ( {__('Subscription renewal', 'sikshya')} ) : ( '—' )} {r.amount.toFixed(2)} {r.currency || ''} {(r.charge_kind || '').toLowerCase() === 'renewal' ? (
) : null}
e.stopPropagation()} className="inline-flex"> {(() => { const items: RowActionItem[] = [ { key: 'view', label: 'View details', onClick: () => navigateView('payment', { id: String(r.id) }), }, { key: 'status', label: 'Change status', onClick: () => openEdit(r), }, { key: 'delete', label: 'Delete', danger: true, onClick: async () => { const ok = await dialog.confirm({ title: `Delete payment #${r.id}?`, message: __('This permanently removes the payment record. This cannot be undone.', 'sikshya'), confirmLabel: __('Delete', 'sikshya'), variant: 'danger', }); if (!ok) return; try { await getSikshyaApi().delete<{ ok?: boolean; message?: string }>( SIKSHYA_ENDPOINTS.admin.payment(r.id) ); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } }, }, ]; return ; })()}
{!loading && pages > 1 ? (

Page {page} of {pages} · {total} total

) : null} )}
); }