'use client'; import React, { useEffect, useState } from 'react'; import { useRouter, useParams } from 'next/navigation'; const SIZE_PRESETS = [5_000, 10_000, 25_000, 50_000, 75_000, 100_000, 150_000]; interface AccountConfig { id: number; prefix: string; profitTarget: number; consistency: number; minDayPnL: number; minTradingDays: number; accountSize: number; maxPositionSize: number; targetSameEquity: boolean; withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[]; } interface FirmConfig { id: number; firm: string; accounts: AccountConfig[]; } interface RowState extends AccountConfig { dirty: boolean; saving: boolean; error: string; useCustomSize: boolean; confirmDelete: boolean; } function initRow(acc: AccountConfig): RowState { return { ...acc, dirty: false, saving: false, error: '', useCustomSize: !SIZE_PRESETS.includes(acc.accountSize), confirmDelete: false, }; } function newRow(): RowState { return { id: -(Date.now()), prefix: '', profitTarget: 3000, consistency: 0.5, minDayPnL: -999, minTradingDays: 5, accountSize: 50_000, maxPositionSize: 0, targetSameEquity: true, withdrawalStages: [], dirty: true, saving: false, error: '', useCustomSize: false, confirmDelete: false, }; } function fmtSize(n: number) { return n >= 1000 ? `${n / 1000}k` : String(n); } function TrashIcon() { return ( ); } const inputCls = 'border border-slate-200 rounded-md px-2 py-1.5 text-sm text-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-400 bg-white w-full'; export default function FirmSettingsPage() { const router = useRouter(); const params = useParams(); const id = params.id as string; const [firmName, setFirmName] = useState(''); const [rows, setRows] = useState([]); const [loadError, setLoadError] = useState(''); // Instruments (globally enabled only) and per-firm bans const [instruments, setInstruments] = useState([]); const [bannedSymbols, setBannedSymbols] = useState>(new Set()); useEffect(() => { fetch(`/api/firms/${id}`) .then((r) => { if (!r.ok) throw new Error('Not found'); return r.json() as Promise; }) .then((firm) => { setFirmName(firm.firm); setRows(firm.accounts.map(initRow)); }) .catch(() => setLoadError('Failed to load firm')); // Load globally-enabled instruments fetch('/api/instruments') .then((r) => r.json()) .then((data: { symbol: string; enabled: boolean }[]) => setInstruments(data.filter((i) => i.enabled).map((i) => i.symbol)) ); // Load this firm's banned symbols fetch(`/api/firms/${id}/banned-symbols`) .then((r) => r.json()) .then((syms: string[]) => setBannedSymbols(new Set(syms))); }, [id]); async function toggleBan(symbol: string) { const nowBanned = !bannedSymbols.has(symbol); setBannedSymbols((prev) => { const next = new Set(prev); if (nowBanned) next.add(symbol); else next.delete(symbol); return next; }); await fetch(`/api/firms/${id}/banned-symbols`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol, banned: nowBanned }), }); } const updateRow = (rowId: number, patch: Partial) => { setRows((prev) => prev.map((r) => (r.id === rowId ? { ...r, ...patch, dirty: true } : r)) ); }; const saveRow = async (row: RowState) => { setRows((prev) => prev.map((r) => (r.id === row.id ? { ...r, saving: true, error: '' } : r)) ); try { const body = { prefix: row.prefix, profitTarget: row.profitTarget, consistency: row.consistency, minDayPnL: row.minDayPnL, minTradingDays: row.minTradingDays, accountSize: row.accountSize, maxPositionSize: row.maxPositionSize, targetSameEquity: row.targetSameEquity, withdrawalStages: row.withdrawalStages, }; if (row.id < 0) { const res = await fetch(`/api/firms/${id}/accounts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error('Failed'); const created = await res.json() as AccountConfig; setRows((prev) => prev.map((r) => r.id === row.id ? { ...initRow(created), dirty: false } : r ) ); } else { const res = await fetch(`/api/account-configs/${row.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error('Failed'); setRows((prev) => prev.map((r) => r.id === row.id ? { ...r, saving: false, dirty: false, error: '' } : r ) ); } } catch { setRows((prev) => prev.map((r) => r.id === row.id ? { ...r, saving: false, error: 'Save failed' } : r ) ); } }; const confirmDeleteRow = (rowId: number) => { setRows((prev) => prev.map((r) => (r.id === rowId ? { ...r, confirmDelete: true } : r)) ); }; const cancelDeleteRow = (rowId: number) => { setRows((prev) => prev.map((r) => (r.id === rowId ? { ...r, confirmDelete: false } : r)) ); }; const deleteRow = async (row: RowState) => { if (row.id < 0) { setRows((prev) => prev.filter((r) => r.id !== row.id)); return; } try { const res = await fetch(`/api/account-configs/${row.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed'); setRows((prev) => prev.filter((r) => r.id !== row.id)); } catch { setRows((prev) => prev.map((r) => r.id === row.id ? { ...r, confirmDelete: false, error: 'Delete failed' } : r ) ); } }; if (loadError) { return (

{loadError}

); } return (
{/* Header */}

{firmName || '…'}

/ Settings
{/* Account Types */}

Account Types

{rows.length === 0 && firmName && ( )} {rows.map((row) => ( {/* Prefix */} {/* Account Size */} {/* Profit Target */} {/* Consistency */} {/* Min Day P&L */} {/* Min Trading Days */} {/* Max Contracts */} {/* After Withdrawal */} {/* Actions */} {/* Stage sub-rows — shown when targetSameEquity is OFF */} {!row.targetSameEquity && ( <> {row.withdrawalStages.map((stage, idx) => { const isLast = idx === row.withdrawalStages.length - 1; const showPlus = isLast; return ( ); })} )} ))} {/* Add row */}
Prefix Account Size Profit Target Consistency Min Day P&L Min Trading Days Max Contracts After First W/D
No account types yet
updateRow(row.id, { prefix: e.target.value })} />
{row.useCustomSize && ( updateRow(row.id, { accountSize: Number(e.target.value) }) } /> )}
$ updateRow(row.id, { profitTarget: Number(e.target.value) }) } />
updateRow(row.id, { consistency: Number(e.target.value) / 100 }) } /> %
$ updateRow(row.id, { minDayPnL: e.target.value === '' ? -999 : Number(e.target.value), }) } />
updateRow(row.id, { minTradingDays: Number(e.target.value) }) } /> updateRow(row.id, { maxPositionSize: e.target.value === '' ? 0 : Number(e.target.value), }) } />
{row.error && ( {row.error} )} {row.confirmDelete ? ( <> Delete? ) : ( <> )}
Stage {idx + 2}{showPlus ? '+' : ''}
$ { const next = row.withdrawalStages.map((s, i) => i === idx ? { ...s, profit: Number(e.target.value) } : s ); updateRow(row.id, { withdrawalStages: next }); }} />
{row.consistency > 0 && (
{ const next = row.withdrawalStages.map((s, i) => i === idx ? { ...s, consistency: Number(e.target.value) / 100 } : s ); updateRow(row.id, { withdrawalStages: next }); }} /> %
)}
{ const next = row.withdrawalStages.map((s, i) => i === idx ? { ...s, minTradingDays: Number(e.target.value) } : s ); updateRow(row.id, { withdrawalStages: next }); }} /> days
{/* ── Instruments ── */}

Instruments

{instruments.length === 0 ? (
Loading…
) : ( {instruments.map((sym) => { const banned = bannedSymbols.has(sym); return ( ); })}
Symbol Banned
{sym} {banned && ( {sym} BANNED )}
)}
); }