// CalendarView.jsx const CalendarPage = ({ trades }) => { const { useState } = React; const today = new Date(); const [viewYear, setViewYear] = useState(today.getFullYear()); const [viewMonth, setViewMonth] = useState(today.getMonth()); const [selectedDay, setSelectedDay] = useState(null); const pnlByDate = getPnLByDate(trades); const prevMonth = () => { if (viewMonth === 0) { setViewYear(y => y - 1); setViewMonth(11); } else setViewMonth(m => m - 1); }; const nextMonth = () => { if (viewMonth === 11) { setViewYear(y => y + 1); setViewMonth(0); } else setViewMonth(m => m + 1); }; const firstDay = new Date(viewYear, viewMonth, 1).getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const monthName = new Date(viewYear, viewMonth, 1).toLocaleString('default', { month: 'long', year: 'numeric' }); // get max abs pnl for color scaling const monthKeys = []; for (let d = 1; d <= daysInMonth; d++) { const key = `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; if (pnlByDate[key] != null) monthKeys.push(Math.abs(pnlByDate[key])); } const maxPnL = Math.max(...monthKeys, 1); const getDayKey = (d) => `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const getDayTrades = (d) => trades.filter(t => t.date === getDayKey(d)); const getDayColor = (pnl) => { if (pnl == null) return 'transparent'; const intensity = Math.min(Math.abs(pnl) / maxPnL, 1); if (pnl > 0) return `rgba(0, 210, 122, ${0.08 + intensity * 0.35})`; return `rgba(240, 64, 96, ${0.08 + intensity * 0.35})`; }; const cells = []; for (let i = 0; i < firstDay; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); // Weekly totals const weeks = []; for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7)); // Month stats const monthTrades = trades.filter(t => { const key = `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}`; return t.date.startsWith(key) && t.status === 'closed'; }); const monthPnL = monthTrades.reduce((s, t) => s + (t.pnl || 0), 0); const monthWins = monthTrades.filter(t => t.pnl > 0).length; const monthLosses = monthTrades.filter(t => t.pnl < 0).length; const tradingDays = new Set(monthTrades.map(t => t.date)).size; const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const selectedTrades = selectedDay ? getDayTrades(selectedDay) : []; return React.createElement('div', { style: { padding: '28px 32px', maxWidth: '1100px' } }, [ // Header React.createElement('div', { key: 'hdr', style: { marginBottom: '24px' } }, [ React.createElement('h1', { key: 'h', style: { fontSize: '22px', fontWeight: 700, color: 'var(--text)', margin: 0 } }, 'Calendar'), React.createElement('p', { key: 'sub', style: { fontSize: '13px', color: 'var(--muted)', marginTop: '4px', margin: '4px 0 0' } }, 'Daily P&L heatmap') ]), React.createElement('div', { key: 'main', style: { display: 'grid', gridTemplateColumns: '1fr 300px', gap: '20px' } }, [ // Calendar React.createElement('div', { key: 'cal', style: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '10px', padding: '24px' } }, [ // Month nav React.createElement('div', { key: 'nav', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' } }, [ React.createElement('button', { key: 'prev', onClick: prevMonth, style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '6px', color: 'var(--text2)', padding: '6px 14px', cursor: 'pointer', fontSize: '16px' } }, '‹'), React.createElement('span', { key: 'label', style: { fontSize: '15px', fontWeight: 600, color: 'var(--text)' } }, monthName), React.createElement('button', { key: 'next', onClick: nextMonth, style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '6px', color: 'var(--text2)', padding: '6px 14px', cursor: 'pointer', fontSize: '16px' } }, '›'), ]), // Day headers React.createElement('div', { key: 'daynames', style: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '4px', marginBottom: '4px' } }, dayNames.map(d => React.createElement('div', { key: d, style: { textAlign: 'center', fontSize: '11px', color: 'var(--muted)', fontWeight: 600, padding: '4px', letterSpacing: '0.06em' } }, d)) ), // Calendar grid React.createElement('div', { key: 'grid' }, weeks.map((week, wi) => React.createElement('div', { key: wi, style: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '4px', marginBottom: '4px' } }, week.map((day, di) => { if (day == null) return React.createElement('div', { key: `e-${di}` }); const key = getDayKey(day); const pnl = pnlByDate[key]; const dayTrades = getDayTrades(day); const isToday = day === today.getDate() && viewMonth === today.getMonth() && viewYear === today.getFullYear(); const isSelected = selectedDay === day; const isWeekend = (di + firstDay) % 7 === 0 || (di + firstDay) % 7 === 6; return React.createElement('div', { key: day, onClick: () => setSelectedDay(isSelected ? null : day), style: { minHeight: '72px', background: getDayColor(pnl), borderRadius: '6px', border: isSelected ? '2px solid #5588ff' : isToday ? '1px solid #5588ff' : '1px solid transparent', padding: '6px', cursor: dayTrades.length > 0 ? 'pointer' : 'default', transition: 'all 0.12s', opacity: isWeekend && !dayTrades.length ? 0.5 : 1, display: 'flex', flexDirection: 'column', gap: '2px' }, onMouseEnter: e => { if (dayTrades.length > 0) e.currentTarget.style.borderColor = '#5588ff'; }, onMouseLeave: e => { if (!isSelected) e.currentTarget.style.borderColor = isToday ? '#5588ff' : 'transparent'; } }, [ React.createElement('div', { key: 'n', style: { fontSize: '12px', fontWeight: 500, color: isToday ? '#5588ff' : 'var(--text2)' } }, day), pnl != null && React.createElement('div', { key: 'pnl', style: { fontSize: '11px', fontFamily: 'JetBrains Mono, monospace', color: pnl >= 0 ? '#00d27a' : '#f04060', fontWeight: 500 } }, fmtPnL(pnl)), dayTrades.length > 0 && React.createElement('div', { key: 'cnt', style: { fontSize: '10px', color: 'var(--muted)', marginTop: 'auto' } }, `${dayTrades.length} trade${dayTrades.length > 1 ? 's' : ''}`) ]); }) ) ) ) ]), // Sidebar: month stats + selected day React.createElement('div', { key: 'sidebar', style: { display: 'flex', flexDirection: 'column', gap: '14px' } }, [ // Month stats React.createElement('div', { key: 'mstats', style: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '10px', padding: '20px' } }, [ React.createElement('div', { key: 't', style: { fontSize: '11px', color: 'var(--muted)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '14px' } }, 'Month Summary'), React.createElement('div', { key: 'pnl', style: { marginBottom: '14px' } }, [ React.createElement('div', { key: 'l', style: { fontSize: '11px', color: 'var(--muted)', marginBottom: '4px' } }, 'Net P&L'), React.createElement(PnLValue, { key: 'v', value: monthPnL, size: 'lg' }) ]), React.createElement('div', { key: 'grid', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } }, [ React.createElement('div', { key: 'w', style: { background: 'var(--surface2)', borderRadius: '6px', padding: '10px' } }, [ React.createElement('div', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', marginBottom: '2px' } }, 'Wins'), React.createElement('div', { key: 'v', style: { fontSize: '20px', fontWeight: 700, color: '#00d27a', fontFamily: 'JetBrains Mono, monospace' } }, monthWins) ]), React.createElement('div', { key: 'l', style: { background: 'var(--surface2)', borderRadius: '6px', padding: '10px' } }, [ React.createElement('div', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', marginBottom: '2px' } }, 'Losses'), React.createElement('div', { key: 'v', style: { fontSize: '20px', fontWeight: 700, color: '#f04060', fontFamily: 'JetBrains Mono, monospace' } }, monthLosses) ]), React.createElement('div', { key: 'td', style: { background: 'var(--surface2)', borderRadius: '6px', padding: '10px' } }, [ React.createElement('div', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', marginBottom: '2px' } }, 'Trading Days'), React.createElement('div', { key: 'v', style: { fontSize: '20px', fontWeight: 700, color: 'var(--text)', fontFamily: 'JetBrains Mono, monospace' } }, tradingDays) ]), React.createElement('div', { key: 'wr', style: { background: 'var(--surface2)', borderRadius: '6px', padding: '10px' } }, [ React.createElement('div', { key: 'l', style: { fontSize: '10px', color: 'var(--muted)', marginBottom: '2px' } }, 'Win Rate'), React.createElement('div', { key: 'v', style: { fontSize: '20px', fontWeight: 700, color: '#5588ff', fontFamily: 'JetBrains Mono, monospace' } }, monthTrades.length > 0 ? `${((monthWins / monthTrades.length) * 100).toFixed(0)}%` : '—' ) ]), ]) ]), // Selected day trades selectedDay && React.createElement('div', { key: 'daytrades', style: { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: '10px', padding: '20px' } }, [ React.createElement('div', { key: 't', style: { fontSize: '11px', color: 'var(--muted)', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '12px' } }, fmtDate(getDayKey(selectedDay)) ), selectedTrades.length === 0 ? React.createElement('div', { key: 'empty', style: { color: 'var(--muted)', fontSize: '13px' } }, 'No trades this day') : React.createElement('div', { key: 'list', style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, selectedTrades.map(t => React.createElement('div', { key: t.id, style: { background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: '6px', padding: '10px 12px' } }, [ React.createElement('div', { key: 'top', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' } }, [ React.createElement(SymbolCell, { key: 'sym', symbol: t.symbol }), React.createElement(PnLValue, { key: 'pnl', value: t.pnl }) ]), React.createElement('div', { key: 'sub', style: { display: 'flex', gap: '6px' } }, [ React.createElement(TypeBadge, { key: 'type', type: t.type, optionType: t.optionType }), React.createElement(DirectionBadge, { key: 'dir', direction: t.direction }) ]) ])) ) ]) ]) ]) ]); }; Object.assign(window, { CalendarPage });