(() => { const { useMemo, useState } = React; function asArray(value) { return Array.isArray(value) ? value : []; } function toNumber(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; } function asUtcDate(value) { const text = String(value || "").trim(); if (!text) { return null; } const normalized = text.includes("T") ? text : text.replace(" ", "T"); const date = new Date(`${normalized}Z`); return Number.isNaN(date.getTime()) ? null : date; } function formatDateTime(value, fallback = "Unavailable") { const date = asUtcDate(value); if (!date) { return fallback; } return date.toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", timeZone: "UTC" }) + " UTC"; } function formatDate(value, fallback = "Unavailable") { const date = asUtcDate(value); if (!date) { return fallback; } return date.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric", timeZone: "UTC" }); } function formatMoney(value, currency = "USD", signed = false) { const amount = toNumber(value); const prefix = signed && amount > 0 ? "+" : signed && amount < 0 ? "-" : ""; return `${String(currency || "USD").toUpperCase()} ${prefix}${Math.abs(amount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } function formatNumber(value, options = {}) { return toNumber(value).toLocaleString(undefined, options); } function formatPips(value, signed = false) { const amount = Number(value); if (!Number.isFinite(amount)) { return "Not matched"; } const prefix = signed && amount > 0 ? "+" : signed && amount < 0 ? "-" : ""; return `${prefix}${Math.abs(amount).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} pips`; } function badgeClassForNumber(value) { const amount = toNumber(value); if (amount < 0) { return "badge-danger"; } if (amount === 0) { return "badge-gold"; } return ""; } function accountKeyFromItem(item) { const login = String(item && item.account_login ? item.account_login : "").trim() || "unknown"; const server = String(item && item.server_name ? item.server_name : "").trim() || "unknown"; return `${login}|${server}`; } function computeSummary(items) { const summary = { total_trades: asArray(items).length, closed_trades: 0, winning_trades: 0, losing_trades: 0, profit: 0, loss: 0, pips_won: 0, pips_lost: 0, net_total: 0 }; asArray(items).forEach((item) => { const net = toNumber(item.net_result); const pips = Number(item.pips); const entry = String(item.deal_entry || "").toLowerCase(); summary.net_total += net; if (net > 0) { summary.profit += net; summary.winning_trades += 1; } else if (net < 0) { summary.loss += Math.abs(net); summary.losing_trades += 1; } if (["out", "out_by", "inout"].includes(entry)) { summary.closed_trades += 1; } if (Number.isFinite(pips)) { if (pips > 0) { summary.pips_won += pips; } else if (pips < 0) { summary.pips_lost += Math.abs(pips); } } }); return summary; } function computeRunningChartPoints(items) { let running = 0; return asArray(items) .slice() .sort((left, right) => { const leftTime = asUtcDate(left.deal_time); const rightTime = asUtcDate(right.deal_time); const leftValue = leftTime ? leftTime.getTime() : 0; const rightValue = rightTime ? rightTime.getTime() : 0; if (leftValue === rightValue) { return toNumber(left.id) - toNumber(right.id); } return leftValue - rightValue; }) .map((item) => { running += toNumber(item.net_result); return { label: formatDateTime(item.deal_time, "Recent trade"), symbol: String(item.symbol || "").trim(), value: Number(running.toFixed(2)) }; }); } function buildChartGeometry(points, width = 860, height = 250) { const list = asArray(points); if (!list.length) { return null; } const padX = 28; const padTop = 18; const padBottom = 30; const values = list.map((point) => toNumber(point.value)); let minValue = Math.min(...values); let maxValue = Math.max(...values); if (Math.abs(maxValue - minValue) < 0.01) { minValue -= 1; maxValue += 1; } const plotWidth = width - padX * 2; const plotHeight = height - padTop - padBottom; const range = maxValue - minValue || 1; const coords = list.map((point, index) => { const x = padX + plotWidth * (index / Math.max(list.length - 1, 1)); const y = padTop + ((maxValue - toNumber(point.value)) / range) * plotHeight; return `${x.toFixed(2)},${y.toFixed(2)}`; }); const zeroLineY = minValue <= 0 && maxValue >= 0 ? padTop + ((maxValue - 0) / range) * plotHeight : null; return { width, height, padX, polyline: coords.join(" "), area: `${padX},${height - padBottom} ${coords.join(" ")} ${width - padX},${height - padBottom}`, maxLabel: formatMoney(maxValue), minLabel: formatMoney(minValue), startLabel: String(list[0].label || "Recent history"), endLabel: String(list[list.length - 1].label || "Latest result"), zeroLineY: zeroLineY == null ? null : zeroLineY.toFixed(2) }; } function NavLink({ item, active }) { return ( {item.label} ); } function PortalShell({ data }) { const [menuOpen, setMenuOpen] = useState(false); const navItems = asArray(data.navItems); return (
Connected workspace
{data.pageTitle || "Portal"}

Switch across the full portal from the top menu and keep mobile navigation open when you need it.

{data.roleLabel || "Portal"} {data.userName || "User"}
{navItems.map((item) => ( ))}
); } function PublicShell({ data }) { const [menuOpen, setMenuOpen] = useState(false); const links = asArray(data.links); return (
Top menu
Explore the full demo flow

Jump between pricing, partners, training, and the 7-day demo registration area from one responsive menu.

{links.map((item) => ( {item.label} ))}
); } function StatCard({ label, value, copy }) { return (
{label}
{value}
{copy ?
{copy}
: null}
); } function JournalAccountPill({ group, active, onClick }) { return ( ); } function JournalChart({ points }) { const chart = buildChartGeometry(points); if (!chart) { return (
No chart movement is available for this account yet.
); } return (
{chart.maxLabel} {chart.startLabel}
{chart.zeroLineY ? ( ) : null}
{chart.minLabel} {chart.endLabel}
); } function JournalTradeCard({ item, role }) { const net = toNumber(item.net_result); const charges = toNumber(item.swap) + toNumber(item.commission) + toNumber(item.fee); const typeClass = String(item.deal_type || "").toLowerCase() === "sell" ? "badge-danger" : String(item.deal_type || "").toLowerCase() === "buy" ? "" : "badge-gold"; const entry = String(item.deal_entry || "").toLowerCase(); const entryLabel = { in: "Open", out: "Close", inout: "Reverse", out_by: "Close By", other: "Other" }[entry] || "Other"; return (
{item.symbol || "Trade row"}
{formatDateTime(item.deal_time, "No deal time")} - Ticket #{item.deal_ticket || "0"}
{formatMoney(net, "USD", true)}
{String(item.deal_type || "other").toUpperCase()} {entryLabel} {formatPips(item.pips, true)}
{role !== "client" ? (
Client
{item.client_name || "Unassigned Client"}
) : null}
Account
{item.account_login || "No login"} / {item.server_name || "No server"}
License
{item.license_key || "Unlinked"}
Volume / Price
{formatNumber(item.volume, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} / {formatNumber(item.price, { minimumFractionDigits: 2, maximumFractionDigits: 5 })}
Raw Profit
{formatMoney(item.profit, "USD", true)}
Charges
{formatMoney(charges, "USD", true)}
{item.comment ? item.comment : "No comment saved for this transaction."}
); } function JournalApp({ data }) { const items = asArray(data.items); const accountGroups = asArray(data.account_groups); const defaultAccountKey = data.selected_account_key || ""; const [selectedAccountKey, setSelectedAccountKey] = useState(defaultAccountKey); const filteredItems = useMemo(() => { if (!selectedAccountKey) { return items; } return items.filter((item) => accountKeyFromItem(item) === selectedAccountKey); }, [items, selectedAccountKey]); const activeGroup = useMemo(() => { return accountGroups.find((group) => group.account_key === selectedAccountKey) || null; }, [accountGroups, selectedAccountKey]); const summary = useMemo(() => { return selectedAccountKey ? computeSummary(filteredItems) : (data.summary || computeSummary(filteredItems)); }, [data.summary, filteredItems, selectedAccountKey]); const chartPoints = useMemo(() => { return selectedAccountKey ? computeRunningChartPoints(filteredItems) : asArray(data.chart_points); }, [data.chart_points, filteredItems, selectedAccountKey]); if (!items.length && !accountGroups.length) { return
No transaction journal activity has been recorded for this account yet.
; } return (
Account-Based Journal Switcher
Select a trading account to focus the journal on that account's movement, or leave it on all accounts for the global view.
{accountGroups.length} tracked account{accountGroups.length === 1 ? "" : "s"}
{accountGroups.map((group) => ( setSelectedAccountKey(group.account_key)} /> ))}
{activeGroup ? (
Account Name
{activeGroup.account_name || "Not saved"}
Broker / Server
{activeGroup.broker || "No broker"} / {activeGroup.server_name || "No server"}
Latest Trade
{formatDateTime(activeGroup.latest_trade_at, "No trade yet")}
Tracked Symbols
{formatNumber(activeGroup.symbols_count || 0)}
) : null}
Cumulative Account Movement
This curve tracks the running net result using profit, swap, commission, and fee values from the selected journal data.
{selectedAccountKey ? "Focused account" : "All visible accounts"}
Recent Transaction Movement
The most recent journal transactions stay in sync with the selected account so you can inspect movement quickly on both desktop and mobile.
{filteredItems.length} visible rows
{filteredItems.length ? (
{filteredItems.map((item) => ( ))}
) : (
No recent journal rows match the selected account yet.
)}
); } function ReferralOverview({ data }) { const role = String(data.role || "client"); const managerLinks = asArray(data.active_links); const partnerClients = asArray(data.linked_clients); const topPartners = asArray(data.partner_report); const sourceRows = role === "manager" ? managerLinks : partnerClients; const totals = sourceRows.reduce((accumulator, row) => { accumulator.tradeRows += toNumber(row.movement_trade_count); accumulator.netMovement += toNumber(row.movement_net_total); accumulator.liveAccounts += toNumber(row.movement_accounts_count); const currentTrade = asUtcDate(row.latest_trade_at); if (currentTrade && (!accumulator.latestTrade || currentTrade > accumulator.latestTrade)) { accumulator.latestTrade = currentTrade; } return accumulator; }, { tradeRows: 0, netMovement: 0, liveAccounts: 0, latestTrade: null }); const spotlightRows = role === "manager" ? topPartners : sourceRows; if (role === "client") { return (
Referral Progress Snapshot
Your referral area now stays connected to the wider portal so you can move from request status into accounts, payments, and journal activity more easily.
Client view
); } return (
Live Referral Movement
Referral reporting now includes journal movement totals, latest trade timing, and live account activity from the linked trading flow.
{role === "manager" ? "Manager overview" : "Partner overview"}
{spotlightRows.length ? (
{spotlightRows.slice(0, 4).map((row, index) => (
{row.partner_name || row.full_name || row.client_name || "Linked account"}
{row.partner_email || row.email || row.client_email || "Connected portal record"}
{formatMoney(row.movement_net_total, "USD", true)}
Movement Rows
{formatNumber(row.movement_trade_count || 0)}
Live Accounts
{formatNumber(row.movement_accounts_count || 0)}
Latest Trade
{formatDateTime(row.latest_trade_at, "No journal yet")}
{role === "manager" ? "Paid Revenue" : "Pending Commission"}
{role === "manager" ? formatMoney(row.paid_revenue || 0) : formatMoney(row.pending_commission || 0)}
))}
) : null}
); } function mount(node, element) { if (!node) { return; } ReactDOM.createRoot(node).render(element); } mount(document.getElementById("ms-react-portal-shell"), ); mount(document.getElementById("ms-react-public-shell"), ); mount(document.getElementById("ms-react-journal-app"), ); mount(document.getElementById("ms-react-referral-app"), ); })();