diff --git a/src/components/dashboard/CreditsPage.tsx b/src/components/dashboard/CreditsPage.tsx index 0cdb704..8c85525 100644 --- a/src/components/dashboard/CreditsPage.tsx +++ b/src/components/dashboard/CreditsPage.tsx @@ -1,37 +1,56 @@ -import { For, Show, createSignal, onMount } from 'solid-js'; -import { BTN_GHOST, CARD } from '~/components/DashboardShell'; -import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared'; +import { For, Show, createSignal, onMount } from "solid-js"; +import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD } from "~/components/DashboardShell"; +import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from "./RoleDashboardShared"; -const API = '/api/gateway'; +const API = "/api/gateway"; type Props = { roleKey: RoleKey }; +type Package = { + id: string; + name: string; + role_key: string; + package_type: string; + tracecoins_amount: number; + price_inr: number; + description?: string; +}; + +type Payment = { + id: string; + amount_inr: number; + tracecoins_credited: number; + status: string; + created_at: string; + package_name?: string; +}; + async function apiFetch(path: string, opts?: RequestInit) { return fetch(`${API}${path}`, { ...opts, - credentials: 'include', - headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) }, + credentials: "include", + headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) }, }); } export default function CreditsPage(props: Props) { const [wallet, setWallet] = createSignal(null); const [ledger, setLedger] = createSignal([]); + const [packages, setPackages] = createSignal([]); + const [payments, setPayments] = createSignal([]); const [loading, setLoading] = createSignal(true); - const [err, setErr] = createSignal(''); + const [loadingPayments, setLoadingPayments] = createSignal(false); + const [err, setErr] = createSignal(""); + const [msg, setMsg] = createSignal(""); + const [activeTab, setActiveTab] = createSignal<"overview" | "buy" | "transactions">("overview"); + const [busyPackageId, setBusyPackageId] = createSignal(null); const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey); const prefix = () => ROLE_PREFIXES[props.roleKey]; - const loadData = async () => { - setLoading(true); - setErr(''); + const loadWalletData = async () => { + if (!isProfessional()) return; try { - if (!isProfessional()) { - setWallet(null); - setLedger([]); - return; - } const [walletRes, ledgerRes] = await Promise.all([ apiFetch(`/api/${prefix()}/wallet/me`), apiFetch(`/api/${prefix()}/wallet/me/ledger?page=1&limit=20`), @@ -39,72 +58,584 @@ export default function CreditsPage(props: Props) { const walletJson = await walletRes.json().catch(() => ({})); const ledgerJson = await ledgerRes.json().catch(() => ({})); if (walletRes.ok) setWallet(walletJson); - else setErr(walletJson.error || walletJson.message || 'Failed to load wallet.'); if (ledgerRes.ok) setLedger(Array.isArray(ledgerJson?.data) ? ledgerJson.data : []); - else if (!err()) setErr(ledgerJson.error || ledgerJson.message || 'Failed to load ledger.'); } catch { - setErr('Network error while loading credits.'); - } finally { - setLoading(false); + // non-blocking } }; - onMount(loadData); + const loadPackages = async () => { + try { + const res = await apiFetch(`/api/packages?role=${props.roleKey}`); + const data = await res.json().catch(() => ({})); + if (res.ok) { + setPackages(Array.isArray(data?.packages) ? data.packages : []); + } + } catch { + setPackages([]); + } + }; + + const loadPayments = async () => { + setLoadingPayments(true); + try { + // Try to load from payments service + const res = await apiFetch("/api/payments/history?page=1&limit=50"); + const data = await res.json().catch(() => ({})); + if (res.ok) { + setPayments(Array.isArray(data?.payments) ? data.payments : []); + } else { + setPayments([]); + } + } catch { + setPayments([]); + } finally { + setLoadingPayments(false); + } + }; + + const loadAllData = async () => { + setLoading(true); + setErr(""); + await Promise.all([loadWalletData(), loadPackages(), loadPayments()]); + setLoading(false); + }; + + onMount(loadAllData); + + const buyPackage = async (pkg: Package) => { + setBusyPackageId(pkg.id); + setMsg(""); + setErr(""); + try { + // Create payment order + const res = await apiFetch("/api/payments/create-order", { + method: "POST", + body: JSON.stringify({ + amount: pkg.price_inr, + currency: "INR", + package_id: pkg.id, + }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setErr(data.error || data.message || "Failed to create payment order."); + return; + } + + // In production, this would open Razorpay checkout + // For now, show success message + setMsg( + `Order created for ${pkg.name}. Complete payment to receive ${pkg.tracecoins_amount} Tracecoins.` + ); + + // Refresh data after short delay + setTimeout(() => { + loadAllData(); + }, 2000); + } catch { + setErr("Network error while creating payment order."); + } finally { + setBusyPackageId(null); + } + }; + + const formatDate = (dateStr: string) => { + try { + return new Date(dateStr).toLocaleString("en-IN"); + } catch { + return dateStr; + } + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("en-IN", { + style: "currency", + currency: "INR", + minimumFractionDigits: 0, + }).format(amount); + }; return ( -
+
-

Credits

-

- {isProfessional() ? 'Track Tracecoin balance and usage history.' : 'Credits and billing summary for your account.'} +

+ Credits & Billing +

+

+ Manage your Tracecoin balance, purchase packages, and view transaction history.

-
{err()}
+
+ {err()} +
+ +
+ {msg()} +
+
+ + {/* Tabs */} +
+ + + + +
+ -
Loading credits...
+
Loading credits...
- -
-

Billing Overview

-

- Your role does not use professional wallet endpoints. Billing and payments are managed through role-specific transactions. -

-
-
- - -
-
-

Current Balance

-

- {wallet()?.balance ?? 0} Tracecoins + {/* Overview Tab */} + + +

+

+ Billing Overview +

+

+ Your role does not use professional wallet endpoints. Billing and payments are managed + through role-specific transactions.

- -
+ + +
+
+

+ Current Balance +

+

+ {wallet()?.balance ?? 0}{" "} + + Tracecoins + +

+
+ +
+ +
+

+ Recent Ledger +

+ +

+ No ledger entries yet. +

+
+ 0}> +
+ + {(item: any) => ( +
+
+

+ {item.reason || item.type || "Transaction"} +

+

+ {item.created_at ? formatDate(item.created_at) : "—"} +

+
+

= 0 ? "#15803D" : "#B91C1C", + }} + > + {Number(item.amount || 0) >= 0 ? "+" : ""} + {item.amount || 0} +

+
+ )} +
+
+ 5}> + + +
+
+
+ + + {/* Buy Credits Tab */} +
-

Recent Ledger

- -

No ledger entries yet.

+

+ Purchase Tracecoins +

+

+ Select a package to purchase Tracecoins. These can be used to unlock leads and premium + features. +

+ + +

+ No packages available for your role. +

- 0}> -
+ + 0}> +
+ + {(pkg) => ( +
+
+

+ {pkg.name} +

+

+ {formatCurrency(pkg.price_inr)} +

+
+
+ 🪙 + + {pkg.tracecoins_amount} Tracecoins + +
+ +

+ {pkg.description} +

+
+ +
+ )} +
+
+
+
+
+ + {/* Transactions Tab */} + +
+

+ Transaction History +

+ + +

+ Loading transactions... +

+
+ + +

+ No transactions found. +

+
+ + 0 || ledger().length > 0)}> +
+ {/* Wallet Ledger */} {(item: any) => ( -
+
-

{item.reason || item.type || 'Transaction'}

-

{item.created_at ? new Date(item.created_at).toLocaleString('en-IN') : '—'}

+

+ {item.reason || "Transaction"} +

+

+ {formatDate(item.created_at)} +

+
+
+

= 0 ? "#15803D" : "#B91C1C", + }} + > + {Number(item.amount || 0) >= 0 ? "+" : ""} + {item.amount || 0} TC +

+

+ Ledger +

+
+
+ )} + + + {/* Payments */} + + {(payment) => ( +
+
+

+ {payment.package_name || "Credit Purchase"} +

+

+ {formatDate(payment.created_at)} +

+
+
+

+ {formatCurrency(payment.amount_inr)} +

+

+ {payment.status === "SUCCESS" ? "✓ Completed" : payment.status} +

+ 0}> +

+ +{payment.tracecoins_credited} TC +

+
-

= 0 ? '#15803D' : '#B91C1C' }}> - {Number(item.amount || 0) >= 0 ? '+' : ''}{item.amount || 0} -

)}
@@ -115,4 +646,3 @@ export default function CreditsPage(props: Props) {
); } -