feat: wire Credits page to pricing management and payment APIs
Updated CreditsPage with full pricing and transaction support:
- Load pricing packages from /api/packages (role-specific)
- Create payment orders via /api/payments/create-order
- Display transaction history from wallet ledger + payments
- New tabbed interface: Overview, Buy Credits, Transactions
- Shows package details: name, price, tracecoins amount
- Format currency in INR with proper locale
Data sources:
- Packages: GET /api/packages?role={role_key}
- Wallet: GET /api/{prefix}/wallet/me
- Ledger: GET /api/{prefix}/wallet/me/ledger
- Payments: POST /api/payments/create-order
This commit is contained in:
parent
c1c9e79945
commit
4795ef2910
1 changed files with 586 additions and 56 deletions
|
|
@ -1,37 +1,56 @@
|
||||||
import { For, Show, createSignal, onMount } from 'solid-js';
|
import { For, Show, createSignal, onMount } from "solid-js";
|
||||||
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
|
import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD } from "~/components/DashboardShell";
|
||||||
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
|
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from "./RoleDashboardShared";
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = "/api/gateway";
|
||||||
|
|
||||||
type Props = { roleKey: RoleKey };
|
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) {
|
async function apiFetch(path: string, opts?: RequestInit) {
|
||||||
return fetch(`${API}${path}`, {
|
return fetch(`${API}${path}`, {
|
||||||
...opts,
|
...opts,
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
|
headers: { "Content-Type": "application/json", ...(opts?.headers ?? {}) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreditsPage(props: Props) {
|
export default function CreditsPage(props: Props) {
|
||||||
const [wallet, setWallet] = createSignal<any>(null);
|
const [wallet, setWallet] = createSignal<any>(null);
|
||||||
const [ledger, setLedger] = createSignal<any[]>([]);
|
const [ledger, setLedger] = createSignal<any[]>([]);
|
||||||
|
const [packages, setPackages] = createSignal<Package[]>([]);
|
||||||
|
const [payments, setPayments] = createSignal<Payment[]>([]);
|
||||||
const [loading, setLoading] = createSignal(true);
|
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<string | null>(null);
|
||||||
|
|
||||||
const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey);
|
const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey);
|
||||||
const prefix = () => ROLE_PREFIXES[props.roleKey];
|
const prefix = () => ROLE_PREFIXES[props.roleKey];
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadWalletData = async () => {
|
||||||
setLoading(true);
|
if (!isProfessional()) return;
|
||||||
setErr('');
|
|
||||||
try {
|
try {
|
||||||
if (!isProfessional()) {
|
|
||||||
setWallet(null);
|
|
||||||
setLedger([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const [walletRes, ledgerRes] = await Promise.all([
|
const [walletRes, ledgerRes] = await Promise.all([
|
||||||
apiFetch(`/api/${prefix()}/wallet/me`),
|
apiFetch(`/api/${prefix()}/wallet/me`),
|
||||||
apiFetch(`/api/${prefix()}/wallet/me/ledger?page=1&limit=20`),
|
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 walletJson = await walletRes.json().catch(() => ({}));
|
||||||
const ledgerJson = await ledgerRes.json().catch(() => ({}));
|
const ledgerJson = await ledgerRes.json().catch(() => ({}));
|
||||||
if (walletRes.ok) setWallet(walletJson);
|
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 : []);
|
if (ledgerRes.ok) setLedger(Array.isArray(ledgerJson?.data) ? ledgerJson.data : []);
|
||||||
else if (!err()) setErr(ledgerJson.error || ledgerJson.message || 'Failed to load ledger.');
|
|
||||||
} catch {
|
} catch {
|
||||||
setErr('Network error while loading credits.');
|
// non-blocking
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>
|
<div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
|
||||||
<div style={CARD}>
|
<div style={CARD}>
|
||||||
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>Credits</p>
|
<p style={{ margin: "0", "font-size": "22px", "font-weight": "800", color: "#0D0D2A" }}>
|
||||||
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
|
Credits & Billing
|
||||||
{isProfessional() ? 'Track Tracecoin balance and usage history.' : 'Credits and billing summary for your account.'}
|
</p>
|
||||||
|
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
Manage your Tracecoin balance, purchase packages, and view transaction history.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={err()}>
|
<Show when={err()}>
|
||||||
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2', padding: '12px 14px', color: '#B91C1C', 'font-size': '13px', 'font-weight': '600' }}>{err()}</div>
|
<div
|
||||||
|
style={{
|
||||||
|
...CARD,
|
||||||
|
border: "1px solid #FECACA",
|
||||||
|
background: "#FEF2F2",
|
||||||
|
padding: "12px 14px",
|
||||||
|
color: "#B91C1C",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": "600",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{err()}
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={msg()}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...CARD,
|
||||||
|
border: "1px solid #BBF7D0",
|
||||||
|
background: "#ECFDF5",
|
||||||
|
padding: "12px 14px",
|
||||||
|
color: "#065F46",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": "600",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{msg()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
borderBottom: "1px solid #E5E7EB",
|
||||||
|
paddingBottom: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab("overview")}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: activeTab() === "overview" ? "#FF5E13" : "transparent",
|
||||||
|
color: activeTab() === "overview" ? "#fff" : "#6B7280",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: "600",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab("buy")}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: activeTab() === "buy" ? "#FF5E13" : "transparent",
|
||||||
|
color: activeTab() === "buy" ? "#fff" : "#6B7280",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: "600",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Buy Credits
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab("transactions")}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "none",
|
||||||
|
background: activeTab() === "transactions" ? "#FF5E13" : "transparent",
|
||||||
|
color: activeTab() === "transactions" ? "#fff" : "#6B7280",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: "600",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Transactions
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={loadAllData} style={{ ...BTN_GHOST, marginLeft: "auto" }}>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
<div style={{ ...CARD, 'text-align': 'center', color: '#9CA3AF' }}>Loading credits...</div>
|
<div style={{ ...CARD, "text-align": "center", color: "#9CA3AF" }}>Loading credits...</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!loading() && !isProfessional()}>
|
{/* Overview Tab */}
|
||||||
|
<Show when={!loading() && activeTab() === "overview"}>
|
||||||
|
<Show when={!isProfessional()}>
|
||||||
<div style={CARD}>
|
<div style={CARD}>
|
||||||
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '700', color: '#111827' }}>Billing Overview</p>
|
<p style={{ margin: "0", "font-size": "15px", "font-weight": "700", color: "#111827" }}>
|
||||||
<p style={{ margin: '8px 0 0', 'font-size': '13px', color: '#6B7280' }}>
|
Billing Overview
|
||||||
Your role does not use professional wallet endpoints. Billing and payments are managed through role-specific transactions.
|
</p>
|
||||||
|
<p style={{ margin: "8px 0 0", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
Your role does not use professional wallet endpoints. Billing and payments are managed
|
||||||
|
through role-specific transactions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!loading() && isProfessional()}>
|
<Show when={isProfessional()}>
|
||||||
<div style={{ ...CARD, display: 'grid', 'grid-template-columns': '1fr auto', gap: '10px', 'align-items': 'center' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
...CARD,
|
||||||
|
display: "grid",
|
||||||
|
"grid-template-columns": "1fr auto",
|
||||||
|
gap: "10px",
|
||||||
|
"align-items": "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: '0', 'font-size': '12px', 'letter-spacing': '0.06em', 'text-transform': 'uppercase', color: '#6B7280' }}>Current Balance</p>
|
<p
|
||||||
<p style={{ margin: '6px 0 0', 'font-size': '28px', 'font-weight': '800', color: '#111827' }}>
|
style={{
|
||||||
{wallet()?.balance ?? 0} <span style={{ 'font-size': '14px', color: '#6B7280', 'font-weight': '600' }}>Tracecoins</span>
|
margin: "0",
|
||||||
|
"font-size": "12px",
|
||||||
|
"letter-spacing": "0.06em",
|
||||||
|
"text-transform": "uppercase",
|
||||||
|
color: "#6B7280",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Current Balance
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "6px 0 0",
|
||||||
|
"font-size": "36px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{wallet()?.balance ?? 0}{" "}
|
||||||
|
<span style={{ "font-size": "16px", color: "#6B7280", "font-weight": "600" }}>
|
||||||
|
Tracecoins
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={loadData} style={BTN_GHOST}>Refresh</button>
|
<button type="button" onClick={() => setActiveTab("buy")} style={BTN_PRIMARY}>
|
||||||
|
Buy Credits
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={CARD}>
|
<div style={CARD}>
|
||||||
<p style={{ margin: '0 0 10px', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Recent Ledger</p>
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0 0 10px",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Recent Ledger
|
||||||
|
</p>
|
||||||
<Show when={ledger().length === 0}>
|
<Show when={ledger().length === 0}>
|
||||||
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>No ledger entries yet.</p>
|
<p style={{ margin: "0", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
No ledger entries yet.
|
||||||
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={ledger().length > 0}>
|
<Show when={ledger().length > 0}>
|
||||||
<div style={{ display: 'grid', gap: '8px' }}>
|
<div style={{ display: "grid", gap: "8px" }}>
|
||||||
|
<For each={ledger().slice(0, 5)}>
|
||||||
|
{(item: any) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "10px",
|
||||||
|
padding: "10px",
|
||||||
|
background: "#FCFCFD",
|
||||||
|
display: "flex",
|
||||||
|
"justify-content": "space-between",
|
||||||
|
gap: "10px",
|
||||||
|
"flex-wrap": "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.reason || item.type || "Transaction"}
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
|
||||||
|
{item.created_at ? formatDate(item.created_at) : "—"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: Number(item.amount || 0) >= 0 ? "#15803D" : "#B91C1C",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Number(item.amount || 0) >= 0 ? "+" : ""}
|
||||||
|
{item.amount || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<Show when={ledger().length > 5}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab("transactions")}
|
||||||
|
style={{ ...BTN_GHOST, "margin-top": "10px", width: "100%" }}
|
||||||
|
>
|
||||||
|
View All Transactions
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Buy Credits Tab */}
|
||||||
|
<Show when={!loading() && activeTab() === "buy"}>
|
||||||
|
<div style={CARD}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0 0 10px",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Purchase Tracecoins
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "0 0 16px", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
Select a package to purchase Tracecoins. These can be used to unlock leads and premium
|
||||||
|
features.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Show when={packages().length === 0}>
|
||||||
|
<p style={{ margin: "0", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
No packages available for your role.
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={packages().length > 0}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
"grid-template-columns": "repeat(auto-fill, minmax(240px, 1fr))",
|
||||||
|
gap: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={packages()}>
|
||||||
|
{(pkg) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "2px solid #E5E7EB",
|
||||||
|
borderRadius: "12px",
|
||||||
|
padding: "16px",
|
||||||
|
background: "#fff",
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "column",
|
||||||
|
gap: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pkg.name}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "4px 0 0",
|
||||||
|
"font-size": "24px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: "#FF5E13",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrency(pkg.price_inr)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
gap: "6px",
|
||||||
|
margin: "8px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: "20px" }}>🪙</span>
|
||||||
|
<span style={{ fontSize: "15px", fontWeight: "700", color: "#111827" }}>
|
||||||
|
{pkg.tracecoins_amount} Tracecoins
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Show when={pkg.description}>
|
||||||
|
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280", flex: "1" }}>
|
||||||
|
{pkg.description}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => buyPackage(pkg)}
|
||||||
|
disabled={busyPackageId() === pkg.id}
|
||||||
|
style={{
|
||||||
|
...BTN_PRIMARY,
|
||||||
|
width: "100%",
|
||||||
|
marginTop: "8px",
|
||||||
|
opacity: busyPackageId() === pkg.id ? "0.7" : "1",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{busyPackageId() === pkg.id ? "Processing..." : "Buy Now"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Transactions Tab */}
|
||||||
|
<Show when={!loading() && activeTab() === "transactions"}>
|
||||||
|
<div style={CARD}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0 0 10px",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Transaction History
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Show when={loadingPayments()}>
|
||||||
|
<p style={{ margin: "0", "font-size": "13px", color: "#9CA3AF" }}>
|
||||||
|
Loading transactions...
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!loadingPayments() && payments().length === 0 && ledger().length === 0}>
|
||||||
|
<p style={{ margin: "0", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
No transactions found.
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!loadingPayments() && (payments().length > 0 || ledger().length > 0)}>
|
||||||
|
<div style={{ display: "grid", gap: "8px" }}>
|
||||||
|
{/* Wallet Ledger */}
|
||||||
<For each={ledger()}>
|
<For each={ledger()}>
|
||||||
{(item: any) => (
|
{(item: any) => (
|
||||||
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '10px', padding: '10px', background: '#FCFCFD', display: 'flex', 'justify-content': 'space-between', gap: '10px', 'flex-wrap': 'wrap' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "10px",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#FCFCFD",
|
||||||
|
display: "flex",
|
||||||
|
"justify-content": "space-between",
|
||||||
|
"align-items": "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '700', color: '#111827' }}>{item.reason || item.type || 'Transaction'}</p>
|
<p
|
||||||
<p style={{ margin: '4px 0 0', 'font-size': '12px', color: '#6B7280' }}>{item.created_at ? new Date(item.created_at).toLocaleString('en-IN') : '—'}</p>
|
style={{
|
||||||
</div>
|
margin: "0",
|
||||||
<p style={{ margin: '0', 'font-size': '13px', 'font-weight': '800', color: Number(item.amount || 0) >= 0 ? '#15803D' : '#B91C1C' }}>
|
"font-size": "13px",
|
||||||
{Number(item.amount || 0) >= 0 ? '+' : ''}{item.amount || 0}
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.reason || "Transaction"}
|
||||||
</p>
|
</p>
|
||||||
|
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
|
||||||
|
{formatDate(item.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: "right" }}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "14px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: Number(item.amount || 0) >= 0 ? "#15803D" : "#B91C1C",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Number(item.amount || 0) >= 0 ? "+" : ""}
|
||||||
|
{item.amount || 0} TC
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "2px 0 0", "font-size": "11px", color: "#9CA3AF" }}>
|
||||||
|
Ledger
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
{/* Payments */}
|
||||||
|
<For each={payments()}>
|
||||||
|
{(payment) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "10px",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#FCFCFD",
|
||||||
|
display: "flex",
|
||||||
|
"justify-content": "space-between",
|
||||||
|
"align-items": "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{payment.package_name || "Credit Purchase"}
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
|
||||||
|
{formatDate(payment.created_at)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: "right" }}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "14px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrency(payment.amount_inr)}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "2px 0 0",
|
||||||
|
"font-size": "12px",
|
||||||
|
color: payment.status === "SUCCESS" ? "#15803D" : "#D97706",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{payment.status === "SUCCESS" ? "✓ Completed" : payment.status}
|
||||||
|
</p>
|
||||||
|
<Show when={payment.tracecoins_credited > 0}>
|
||||||
|
<p style={{ margin: "2px 0 0", "font-size": "11px", color: "#6B7280" }}>
|
||||||
|
+{payment.tracecoins_credited} TC
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -115,4 +646,3 @@ export default function CreditsPage(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue