import { useCallback, useEffect, useState, type FormEvent } from 'react'; import { getSikshyaApi, getWpApi, getErrorSummary, 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, ButtonSecondary } from '../components/shared/buttons'; import { Modal } from '../components/shared/Modal'; import { RowActionsMenu, type RowActionItem } from '../components/shared/list/RowActionsMenu'; import { BulkActionsBar } from '../components/shared/list/BulkActionsBar'; import { MultiCoursePicker } from '../components/shared/MultiCoursePicker'; 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, WpRestUser } from '../types'; import { __ } from '../lib/i18n'; type OrderLine = { course_id: number; course_title: string; line_total: number }; type OrderSubscriptionSummary = { is_subscription_checkout?: boolean; plan_id?: number; interval_unit?: string; plan_name?: string; }; type OrderRow = { id: number; user_id: number; status: string; currency: string; subtotal: number; discount_total: number; total: number; gateway: string; gateway_intent_id: string; created_at: string; payer_name: string; payer_email: string; lines: OrderLine[]; subscription?: OrderSubscriptionSummary; dynamic_fields?: Record; dynamic_fields_display?: Array<{ id: string; label: string; value: string }>; }; type ListResponse = { success?: boolean; orders?: OrderRow[]; total?: number; pages?: number; page?: number; per_page?: number; table_missing?: boolean; }; type CreateOrderResponse = { ok?: boolean; order_id?: number; mark_paid?: boolean; message?: string; }; function canMarkOrderPaid(r: OrderRow): boolean { if (r.status === 'paid') { return false; } if (r.status !== 'pending' && r.status !== 'on-hold') { return false; } const gw = (r.gateway || '').toLowerCase(); return gw === 'offline' || gw === ''; } export function OrdersPage(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 [markBusyId, setMarkBusyId] = useState(null); const [selectedIds, setSelectedIds] = useState([]); const [bulkAction, setBulkAction] = useState(''); const [bulkBusy, setBulkBusy] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [pickedUser, setPickedUser] = useState(null); const [userQuery, setUserQuery] = useState(''); const [userResults, setUserResults] = useState([]); const [userLoading, setUserLoading] = useState(false); const [userOpen, setUserOpen] = useState(false); const [courseIds, setCourseIds] = useState([]); const [couponCode, setCouponCode] = useState(''); const [markPaidNow, setMarkPaidNow] = useState(false); const [creating, setCreating] = useState(false); const [editOpen, setEditOpen] = useState(false); const [editId, setEditId] = useState(null); const [editStatus, setEditStatus] = useState<'pending' | 'on-hold' | 'paid'>('pending'); const loader = useCallback(async () => { const q = new URLSearchParams({ page: String(page), per_page: '30' }); return getSikshyaApi().get(`${SIKSHYA_ENDPOINTS.admin.orders}?${q.toString()}`); }, [page]); const { loading, data, error, refetch } = useAsyncData(loader, [page]); const rows = data?.orders ?? []; const total = data?.total ?? 0; const pages = data?.pages ?? 0; const tableMissing = Boolean(data?.table_missing); useEffect(() => { setSelectedIds([]); setBulkAction(''); }, [page]); 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} order(s)?`, message: __('This permanently removes the orders and their line items. 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.ordersBulk, { action: 'status', status: st, ids: selectedIds }); } else if (bulkAction === 'delete') { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.ordersBulk, { action: 'delete', ids: selectedIds }); } setSelectedIds([]); setBulkAction(''); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } finally { setBulkBusy(false); } }; useEffect(() => { if (!createOpen) { return; } const q = userQuery.trim(); if (q.length < 2) { setUserResults([]); return; } let alive = true; const t = window.setTimeout(async () => { setUserLoading(true); try { const users = await getWpApi().get( `/users?context=edit&per_page=20&search=${encodeURIComponent(q)}` ); if (alive) { setUserResults(Array.isArray(users) ? users : []); } } catch { if (alive) { setUserResults([]); } } finally { if (alive) { setUserLoading(false); } } }, 250); return () => { alive = false; window.clearTimeout(t); }; }, [createOpen, userQuery]); const resetCreateForm = () => { setPickedUser(null); setUserQuery(''); setUserResults([]); setUserOpen(false); setCourseIds([]); setCouponCode(''); setMarkPaidNow(false); }; const openEdit = (r: OrderRow) => { setEditId(r.id); setEditStatus((r.status === 'paid' ? 'paid' : r.status === 'on-hold' ? __('on-hold', 'sikshya') : __('pending', 'sikshya')) as any); setEditOpen(true); }; const openCreate = () => { resetCreateForm(); setCreateOpen(true); }; return ( {!tableMissing ? ( openCreate()}> New manual order ) : null} refetch()}> Refresh } > {error ? (
refetch()} />
) : null} {tableMissing ? (
Orders table is not installed yet. Run plugin updates / migrations, then complete a test checkout.
) : null} {!tableMissing ? (
{__('Manual / offline orders:', 'sikshya')}{' '} {config.offlineCheckoutEnabled === false ? ( <> Enable offline checkout under{' '} Sikshya → Settings → Payment {' '} so learners can choose offline on the storefront.{' '} ) : null} Use {__('New manual order', 'sikshya')} to bill a learner without going through the cart, or open a pending offline row and use {__('Mark paid', 'sikshya')} after you confirm payment.
) : null} { if (!creating) { setCreateOpen(false); } }} footer={
{pickedUser ? ( <> Learner: {pickedUser.name || pickedUser.slug} · #{pickedUser.id} ) : ( 'Select a learner to continue.' )}
{creating ? __('Creating…', 'sikshya') : __('Create order', 'sikshya')}
} >
{ e.preventDefault(); if (!pickedUser || courseIds.length === 0) { return; } setCreating(true); try { const res = await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.orders, { user_id: pickedUser.id, course_ids: courseIds, coupon_code: couponCode.trim() || undefined, mark_paid: markPaidNow, }); if (res && res.ok === false) { throw new Error(res.message || 'Could not create order.'); } setCreateOpen(false); resetCreateForm(); await refetch(); await dialog.alert({ title: __('Order created', 'sikshya'), message: res?.message || 'The order was created successfully.' }); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } finally { setCreating(false); } }} >
{__('Learner', 'sikshya')}
{ setPickedUser(null); setUserQuery(e.target.value); setUserOpen(true); }} onFocus={() => setUserOpen(true)} placeholder={__('Search users by name or email…', 'sikshya')} disabled={creating} className="w-full rounded-lg border border-slate-200 px-3 py-2 dark:border-slate-700 dark:bg-slate-950" /> {userOpen ? (
{userLoading ? (
{__('Searching…', 'sikshya')}
) : userQuery.trim().length < 2 ? (
{__('Type at least 2 characters.', 'sikshya')}
) : userResults.length === 0 ? (
{__('No users found.', 'sikshya')}
) : ( userResults.map((u) => ( )) )}
) : null}
{__('Courses', 'sikshya')}
{ if (!markBusyId) { setEditOpen(false); } }} footer={
{ if (!editId) return; setMarkBusyId(editId); try { await getSikshyaApi().patch<{ ok?: boolean; message?: string }>( SIKSHYA_ENDPOINTS.admin.orderUpdate(editId), { status: editStatus } ); setEditOpen(false); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } finally { setMarkBusyId(null); } }} > {markBusyId === editId ? __('Saving…', 'sikshya') : __('Save', 'sikshya')}
} >
Setting status to {__('paid', 'sikshya')} here updates the row, but does not run fulfillment hooks. For offline payments, prefer {__('Mark paid', 'sikshya')}.
{loading ? (
{__('Loading orders…', 'sikshya')}
) : rows.length === 0 ? ( ) : ( <>
void applyBulk()} applyBusy={bulkBusy} trashMode={false} customOptions={[ { value: 'delete', label: 'Delete permanently' }, { value: 'status:pending', label: 'Mark pending' }, { value: 'status:on-hold', label: 'Mark on-hold' }, { value: 'status:paid', label: 'Mark paid (no fulfillment)' }, ]} selectId="orders-bulk" />
{selectedIds.length > 0 ? `${selectedIds.length} selected` : ''}
{rows.map((r) => ( ))}
0 && selectedIds.length === rows.length} onChange={(e) => toggleAll(e.target.checked)} /> {__('Created', 'sikshya')} {__('Customer', 'sikshya')} {__('Courses', 'sikshya')} {__('Total', 'sikshya')} {__('Gateway', 'sikshya')} {__('Status', 'sikshya')} {__('Actions', 'sikshya')}
toggleOne(r.id, e.target.checked)} /> {formatPostDate(r.created_at)}
{r.payer_name || `User #${r.user_id}`}
{r.payer_email || '—'}
{r.total.toFixed(2)} {r.currency} {r.discount_total > 0 ? ( (−{r.discount_total.toFixed(2)}) ) : null} {r.gateway || '—'} {r.gateway_intent_id ? (
{r.gateway_intent_id}
) : null} {r.subscription?.is_subscription_checkout ? (
{r.subscription.plan_name || (r.subscription.plan_id ? `Plan #${r.subscription.plan_id}` : 'Plan')} {r.subscription.interval_unit ? ` · ${r.subscription.interval_unit}` : ''}
) : null}
e.stopPropagation()} className="flex justify-end"> {(() => { const items: RowActionItem[] = [ { key: 'view', label: 'View details', onClick: () => navigateView('order', { id: String(r.id) }), }, { key: 'edit', label: 'Change status', onClick: () => openEdit(r), }, ]; if (canMarkOrderPaid(r)) { items.push({ key: 'mark_paid', label: markBusyId === r.id ? __('Marking…', 'sikshya') : __('Mark paid', 'sikshya'), disabled: markBusyId === r.id || loading, onClick: async () => { setMarkBusyId(r.id); try { await getSikshyaApi().post<{ ok?: boolean; message?: string }>( SIKSHYA_ENDPOINTS.admin.ordersMarkPaid(r.id), {} ); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } finally { setMarkBusyId(null); } }, }); } items.push({ key: 'delete', label: 'Delete', danger: true, onClick: async () => { const ok = await dialog.confirm({ title: `Delete order #${r.id}?`, message: __('This permanently removes the order and its line items. This cannot be undone.', 'sikshya'), confirmLabel: __('Delete', 'sikshya'), variant: 'danger', }); if (!ok) return; try { await getSikshyaApi().delete<{ ok?: boolean; message?: string }>( SIKSHYA_ENDPOINTS.admin.order(r.id) ); await refetch(); } catch (err) { void dialog.alert({ title: __('Something went wrong', 'sikshya'), message: getErrorSummary(err) }); } }, }); return ; })()}
{pages > 1 ? (

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

) : null} )}
); }