// TradeLog.jsx const TradeLogPage = ({ trades, onAddTrade, onEditTrade, onDeleteTrade, onImport, onExportCSV, onExportJSON, onViewDetail }) => { const { useState, useMemo, useRef, useEffect } = React; const [showExport, setShowExport] = useState(false); useEffect(() => { if (!showExport) return; const handler = () => setShowExport(false); document.addEventListener('click', handler); return () => document.removeEventListener('click', handler); }, [showExport]); const [filters, setFilters] = useState({ symbol: '', type: 'all', direction: 'all', status: 'all', period: 'all', signal: 'all', label: 'all' }); const [sort, setSort] = useState({ key: 'date', dir: -1 }); const [expandedId, setExpandedId] = useState(null); const [snapshotModal, setSnapshotModal] = useState(null); const setFilter = (k, v) => setFilters(f => ({ ...f, [k]: v })); const filtered = useMemo(() => { let list = filterByPeriod([...trades], filters.period); if (filters.symbol) list = list.filter(t => t.symbol.toLowerCase().includes(filters.symbol.toLowerCase())); if (filters.type !== 'all') list = list.filter(t => t.type === filters.type); if (filters.direction !== 'all') list = list.filter(t => t.direction === filters.direction); if (filters.status !== 'all') list = list.filter(t => t.status === filters.status); if (filters.signal !== 'all') list = list.filter(t => (t.signals || []).includes(filters.signal) || t.customSignal === filters.signal); if (filters.label !== 'all') list = list.filter(t => (t.labels || []).includes(filters.label) || t.customLabel === filters.label); list.sort((a, b) => { let av = a[sort.key], bv = b[sort.key]; if (sort.key === 'pnl') { av = av ?? -Infinity; bv = bv ?? -Infinity; } if (av < bv) return -sort.dir; if (av > bv) return sort.dir; return 0; }); return list; }, [trades, filters, sort]); const allSignals = useMemo(() => { const set = new Set(); trades.forEach(t => { (t.signals || []).forEach(s => set.add(s)); if (t.customSignal) set.add(t.customSignal); }); return [...set].sort(); }, [trades]); const allLabels = useMemo(() => { const set = new Set(); trades.forEach(t => { (t.labels || []).forEach(l => set.add(l)); if (t.customLabel) set.add(t.customLabel); }); return [...set].sort(); }, [trades]); const SortIcon = ({ col }) => { if (sort.key !== col) return React.createElement('span', { style: { color: 'var(--dim)', marginLeft: '4px' } }, '⇅'); return React.createElement('span', { style: { color: '#5588ff', marginLeft: '4px' } }, sort.dir === 1 ? '↑' : '↓'); }; const thStyle = (col) => ({ padding: '10px 14px', fontSize: '11px', color: 'var(--muted)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.07em', cursor: 'pointer', whiteSpace: 'nowrap', userSelect: 'none', textAlign: 'left', borderBottom: '1px solid var(--border)' }); const tdStyle = { padding: '0 14px', fontSize: '13px', color: 'var(--text)', verticalAlign: 'middle' }; const winCount = filtered.filter(t => t.pnl > 0).length; const lossCount = filtered.filter(t => t.pnl < 0).length; const netPnL = filtered.filter(t => t.status === 'closed').reduce((s, t) => s + (t.pnl || 0), 0); const inputStyle = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '6px', color: 'var(--text)', padding: '7px 12px', fontSize: '13px', outline: 'none', fontFamily: 'Space Grotesk, sans-serif' }; const selStyle = { ...inputStyle, cursor: 'pointer', appearance: 'none', paddingRight: '28px' }; return React.createElement('div', { style: { padding: '28px 32px', maxWidth: '1300px' } }, [ // Header React.createElement('div', { key: 'hdr', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' } }, [ React.createElement('div', { key: 'left' }, [ React.createElement('h1', { key: 'h', style: { fontSize: '22px', fontWeight: 700, color: 'var(--text)', margin: 0 } }, 'Trade Log'), React.createElement('p', { key: 'sub', style: { fontSize: '13px', color: 'var(--muted)', marginTop: '4px', margin: '4px 0 0' } }, `${filtered.length} trades · ${winCount}W ${lossCount}L · Net ` + (netPnL >= 0 ? `+$${netPnL.toFixed(2)}` : `-$${Math.abs(netPnL).toFixed(2)}`) ) ]), React.createElement('div', { key: 'right', style: { display: 'flex', gap: '8px', alignItems: 'center' } }, [ // Import React.createElement('button', { key: 'imp', onClick: onImport, style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '8px', color: 'var(--text2)', fontSize: '13px', fontWeight: 500, cursor: 'pointer', padding: '8px 14px', fontFamily: 'Space Grotesk, sans-serif', display: 'flex', alignItems: 'center', gap: '6px' } }, ['↑ ', 'Import']), // Export dropdown React.createElement('div', { key: 'exp', style: { position: 'relative' } }, [ React.createElement('button', { key: 'expbtn', onClick: () => setShowExport(s => !s), style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '8px', color: 'var(--text2)', fontSize: '13px', fontWeight: 500, cursor: 'pointer', padding: '8px 14px', fontFamily: 'Space Grotesk, sans-serif', display: 'flex', alignItems: 'center', gap: '6px' } }, ['↓ ', 'Export ▾']), showExport && React.createElement('div', { key: 'expmenu', style: { position: 'absolute', top: 'calc(100% + 4px)', right: 0, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '8px', minWidth: '150px', boxShadow: '0 8px 24px rgba(0,0,0,0.5)', zIndex: 100, overflow: 'hidden' } }, [ React.createElement('button', { key: 'csv', onClick: () => { onExportCSV?.(); setShowExport(false); }, style: { width: '100%', padding: '10px 14px', background: 'none', border: 'none', color: 'var(--text)', fontSize: '13px', cursor: 'pointer', textAlign: 'left', fontFamily: 'Space Grotesk, sans-serif' }, onMouseEnter: e => e.currentTarget.style.background = 'var(--surface2)', onMouseLeave: e => e.currentTarget.style.background = 'none' }, '📄 Export CSV'), React.createElement('button', { key: 'json', onClick: () => { onExportJSON?.(); setShowExport(false); }, style: { width: '100%', padding: '10px 14px', background: 'none', border: 'none', color: 'var(--text)', fontSize: '13px', cursor: 'pointer', textAlign: 'left', fontFamily: 'Space Grotesk, sans-serif' }, onMouseEnter: e => e.currentTarget.style.background = 'var(--surface2)', onMouseLeave: e => e.currentTarget.style.background = 'none' }, '{ } Export JSON'), ]) ]), // Add React.createElement('button', { key: 'add', onClick: onAddTrade, style: { background: '#5588ff', color: '#fff', border: 'none', borderRadius: '8px', padding: '9px 20px', fontSize: '13px', fontWeight: 600, cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', letterSpacing: '0.02em' } }, '+ Add Trade') ]) ]), // Filters React.createElement('div', { key: 'filters', style: { display: 'flex', gap: '10px', marginBottom: '18px', flexWrap: 'wrap' } }, [ React.createElement('select', { key: 'period', value: filters.period, onChange: e => setFilter('period', e.target.value), style: { ...selStyle, width: '130px' } }, [ React.createElement('option', { key: 'all', value: 'all' }, 'All Time'), React.createElement('option', { key: 'year', value: 'year' }, 'This Year'), React.createElement('option', { key: 'month', value: 'month' }, 'This Month'), React.createElement('option', { key: 'week', value: 'week' }, 'This Week'), ]), React.createElement('input', { key: 'sym', placeholder: 'Search symbol…', value: filters.symbol, onChange: e => setFilter('symbol', e.target.value), style: { ...inputStyle, width: '160px' } }), React.createElement('select', { key: 'type', value: filters.type, onChange: e => setFilter('type', e.target.value), style: { ...selStyle, width: '130px' } }, [ React.createElement('option', { key: 'all', value: 'all' }, 'All Types'), React.createElement('option', { key: 'stock', value: 'stock' }, 'Stock'), React.createElement('option', { key: 'option', value: 'option' }, 'Option'), ]), React.createElement('select', { key: 'dir', value: filters.direction, onChange: e => setFilter('direction', e.target.value), style: { ...selStyle, width: '130px' } }, [ React.createElement('option', { key: 'all', value: 'all' }, 'All Directions'), React.createElement('option', { key: 'long', value: 'long' }, 'Long'), React.createElement('option', { key: 'short', value: 'short' }, 'Short'), ]), React.createElement('select', { key: 'st', value: filters.status, onChange: e => setFilter('status', e.target.value), style: { ...selStyle, width: '120px' } }, [ React.createElement('option', { key: 'all', value: 'all' }, 'All Status'), React.createElement('option', { key: 'closed', value: 'closed' }, 'Closed'), React.createElement('option', { key: 'open', value: 'open' }, 'Open'), ]), React.createElement('select', { key: 'sig', value: filters.signal, onChange: e => setFilter('signal', e.target.value), style: { ...selStyle, width: '140px' } }, [ React.createElement('option', { key: 'all', value: 'all' }, 'All Signals'), ...allSignals.map(s => React.createElement('option', { key: s, value: s }, s)) ]), React.createElement('select', { key: 'lbl', value: filters.label, onChange: e => setFilter('label', e.target.value), style: { ...selStyle, width: '140px' } }, [ React.createElement('option', { key: 'all', value: 'all' }, 'All Labels'), ...allLabels.map(l => React.createElement('option', { key: l, value: l }, l)) ]), ]), // Table React.createElement('div', { key: 'table', style: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '10px', overflow: 'hidden' } }, React.createElement('table', { style: { width: '100%', borderCollapse: 'collapse' } }, [ React.createElement('thead', { key: 'thead' }, React.createElement('tr', {}, ['date', 'symbol', 'type', 'direction', 'entry', 'exits', 'chart', 'label', 'signal', 'P&L', 'R', 'status', ''].map((col, i) => { const sortable = ['date', 'symbol', 'P&L', 'R'].includes(col); const sortKey = col === 'P&L' ? 'pnl' : col === 'R' ? 'rMultiple' : col; return React.createElement('th', { key: col + i, style: thStyle(col), onClick: sortable ? () => toggleSort(sortKey) : undefined }, [col.toUpperCase(), sortable && React.createElement(SortIcon, { key: 'si', col: sortKey })]); }) ) ), React.createElement('tbody', { key: 'tbody' }, filtered.length === 0 ? React.createElement('tr', { key: 'empty' }, React.createElement('td', { colSpan: 13, style: { textAlign: 'center', padding: '48px', color: 'var(--muted)' } }, 'No trades found')) : filtered.map(t => [ React.createElement('tr', { key: t.id, style: { borderTop: '1px solid var(--surface2)', cursor: 'pointer', transition: 'background 0.12s' }, onMouseEnter: e => e.currentTarget.style.background = '#0f1420', onMouseLeave: e => e.currentTarget.style.background = 'transparent', onClick: () => onViewDetail?.(t) }, [ React.createElement('td', { key: 'date', style: { ...tdStyle, height: '52px' } }, React.createElement('span', { style: { fontSize: '12px', color: 'var(--text2)', fontFamily: 'JetBrains Mono, monospace' } }, fmtDate(t.date)) ), React.createElement('td', { key: 'sym', style: tdStyle }, React.createElement(SymbolCell, { symbol: t.symbol })), React.createElement('td', { key: 'type', style: tdStyle }, React.createElement(TypeBadge, { type: t.type, optionType: t.optionType, strategy: t.strategy })), React.createElement('td', { key: 'dir', style: tdStyle }, React.createElement(DirectionBadge, { direction: t.direction })), React.createElement('td', { key: 'entry', style: tdStyle }, React.createElement('span', { style: { fontFamily: 'JetBrains Mono, monospace', fontSize: '13px', color: 'var(--text2)' } }, t.type === 'stock' ? fmtPrice(t.entryPrice) : `${fmtPrice(t.premium)} × ${t.contracts}c` ) ), React.createElement('td', { key: 'exits', style: tdStyle }, t.exits.length === 0 ? React.createElement('span', { style: { color: 'var(--muted)', fontSize: '12px' } }, 'Open') : React.createElement('button', { onClick: () => setExpandedId(expandedId === t.id ? null : t.id), style: { background: 'none', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--text2)', fontSize: '12px', cursor: 'pointer', padding: '2px 8px' } }, `${t.exits.length} exit${t.exits.length > 1 ? 's' : ''} ${expandedId === t.id ? '▲' : '▼'}`) ), React.createElement('td', { key: 'chart', style: { ...tdStyle, width: '58px' } }, t.snapshot ? React.createElement('div', { onClick: () => setSnapshotModal(t.snapshot), style: { width: '52px', height: '34px', borderRadius: '4px', overflow: 'hidden', cursor: 'zoom-in', border: '1px solid var(--border)', flexShrink: 0, position: 'relative' } }, [ React.createElement('img', { key: 'img', src: t.snapshot, alt: 'Chart', style: { width: '100%', height: '100%', objectFit: 'cover', display: 'block' } }), React.createElement('div', { key: 'overlay', style: { position: 'absolute', inset: 0, background: 'rgba(85,136,255,0)', transition: 'background 0.15s', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', opacity: 0 }, onMouseEnter: e => { e.currentTarget.style.background = 'rgba(0,0,0,0.45)'; e.currentTarget.style.opacity = 1; }, onMouseLeave: e => { e.currentTarget.style.background = 'rgba(85,136,255,0)'; e.currentTarget.style.opacity = 0; } }, '🔍') ]) : React.createElement('div', { style: { width: '52px', height: '34px', borderRadius: '4px', border: '1px dashed #1e2440', opacity: 0.3 } }) ), React.createElement('td', { key: 'lbl', style: { ...tdStyle, maxWidth: '140px' } }, React.createElement('div', { style: { display: 'flex', gap: '4px', flexWrap: 'wrap' } }, [...(t.labels || []), ...(t.customLabel ? [t.customLabel] : [])].slice(0, 2).map((l, i) => React.createElement(LabelBadge, { key: i, label: l }) ) ) ), React.createElement('td', { key: 'sig', style: { ...tdStyle, maxWidth: '180px' } }, React.createElement('div', { style: { display: 'flex', gap: '4px', flexWrap: 'wrap' } }, [...(t.signals || []), ...(t.customSignal ? [t.customSignal] : [])].slice(0, 2).map((s, i) => React.createElement(Badge, { key: i }, s) ) ) ), React.createElement('td', { key: 'pnl', style: tdStyle }, React.createElement(PnLValue, { value: t.pnl })), React.createElement('td', { key: 'r', style: tdStyle }, React.createElement(RValue, { value: t.rMultiple })), React.createElement('td', { key: 'st', style: tdStyle }, React.createElement(StatusDot, { status: t.status })), React.createElement('td', { key: 'actions', style: { ...tdStyle, whiteSpace: 'nowrap' }, onClick: e => e.stopPropagation() }, React.createElement('div', { style: { display: 'flex', gap: '6px' } }, [ t.snapshot && React.createElement('button', { key: 'snap', onClick: () => setSnapshotModal(t.snapshot), style: { background: 'rgba(85,136,255,0.1)', border: '1px solid rgba(85,136,255,0.2)', borderRadius: '4px', color: '#5588ff', fontSize: '11px', cursor: 'pointer', padding: '3px 8px' } }, '📈 Chart'), React.createElement('button', { key: 'edit', onClick: () => onEditTrade(t), style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '4px', color: 'var(--text2)', fontSize: '12px', cursor: 'pointer', padding: '3px 10px' } }, 'Edit'), React.createElement('button', { key: 'del', onClick: () => { if (confirm('Delete this trade?')) onDeleteTrade(t.id); }, style: { background: 'rgba(240,64,96,0.08)', border: '1px solid rgba(240,64,96,0.15)', borderRadius: '4px', color: '#f04060', fontSize: '12px', cursor: 'pointer', padding: '3px 10px' } }, '✕') ]) ), ]), // Expanded exits row expandedId === t.id && React.createElement('tr', { key: t.id + '-exp' }, React.createElement('td', { colSpan: 13, style: { padding: '0 14px 14px 14px', background: 'var(--surface3)', borderTop: 'none' } }, React.createElement('div', { style: { display: 'flex', gap: '10px', flexWrap: 'wrap', paddingTop: '10px' } }, t.exits.map((ex, i) => React.createElement('div', { key: ex.id || i, style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '6px', padding: '8px 14px', fontSize: '12px' } }, [ React.createElement('div', { key: 'l', style: { color: 'var(--muted)', marginBottom: '4px' } }, `Exit ${i + 1} · ${fmtDate(ex.date)}`), React.createElement('div', { key: 'v', style: { fontFamily: 'JetBrains Mono, monospace', color: 'var(--text)' } }, t.type === 'stock' ? `${fmtPrice(ex.price)} × ${ex.quantity} shares` : `${fmtPrice(ex.price)} × ${ex.contracts} contracts` ) ]) ) ) ) ) ]) ) ]) ), // Snapshot modal snapshotModal && React.createElement('div', { key: 'modal', onClick: () => setSnapshotModal(null), style: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 } }, React.createElement('img', { src: snapshotModal, alt: 'Chart', style: { maxWidth: '90vw', maxHeight: '85vh', borderRadius: '8px', border: '1px solid var(--border)' }, onClick: e => e.stopPropagation() }) ) ]); }; Object.assign(window, { TradeLogPage });