/** * FX Journal Pro • Frontend JavaScript Engine * Complete interactive state, Chart.js integrations, trade management & analytics. */ const app = { currentAccountId: 1, accounts: [], analytics: null, trades: [], pagination: { page: 1, limit: 50, total: 0, total_pages: 1 }, calendarYear: 2026, calendarMonth: 8, // 1 to 12 charts: {}, activeTab: 'overview', // Initialization async init() { const today = new Date(); this.calendarYear = today.getFullYear(); this.calendarMonth = today.getMonth() + 1; await this.loadAccounts(); await this.loadAnalytics(); await this.loadTrades(); this.renderCalendar(); }, // ------------------------------------------------------------- // ACCOUNTS MANAGEMENT // ------------------------------------------------------------- async loadAccounts() { try { const res = await fetch('/api/accounts'); const data = await res.json(); if (data.success && data.accounts) { this.accounts = data.accounts; const select = document.getElementById('accountSelect'); select.innerHTML = ''; let defaultAcc = this.accounts.find(a => a.is_default == 1) || this.accounts[0]; if (defaultAcc) { this.currentAccountId = defaultAcc.id; } this.accounts.forEach(acc => { const opt = document.createElement('option'); opt.value = acc.id; opt.textContent = `${acc.name} (${acc.broker} • $${parseFloat(acc.initial_balance).toLocaleString()})`; if (acc.id === this.currentAccountId) opt.selected = true; select.appendChild(opt); }); this.renderSettingsAccounts(); } } catch (e) { console.error('Error loading accounts:', e); } }, async switchAccount(accId) { this.currentAccountId = parseInt(accId); this.pagination.page = 1; await this.loadAnalytics(); await this.loadTrades(); this.renderCalendar(); }, renderSettingsAccounts() { const container = document.getElementById('accountsListSettings'); if (!container) return; container.innerHTML = ''; this.accounts.forEach(acc => { const isCurrent = acc.id === this.currentAccountId; const netProfit = (parseFloat(acc.net_closed_profit) || 0) + (parseFloat(acc.net_open_profit) || 0); const profitClass = netProfit >= 0 ? 'text-emerald-400' : 'text-rose-400'; const profitSign = netProfit >= 0 ? '+' : ''; const el = document.createElement('div'); el.className = `p-4 rounded-xl border ${isCurrent ? 'bg-blue-900/20 border-blue-500/50' : 'bg-gray-800/40 border-gray-700/40'} flex items-center justify-between flex-wrap gap-2`; el.innerHTML = `
${acc.name} ${acc.account_type} ${isCurrent ? 'فعال' : ''}
بروکر: ${acc.broker} بالانس اولیه: $${parseFloat(acc.initial_balance).toLocaleString()} معاملات: ${acc.total_trades || 0}
سود/زیان کل: ${profitSign}$${netProfit.toFixed(2)}
${this.accounts.length > 1 ? ` ` : ''}
`; container.appendChild(el); }); }, async createAccount(e) { e.preventDefault(); const name = document.getElementById('newAccName').value.trim(); const broker = document.getElementById('newAccBroker').value.trim(); const balance = parseFloat(document.getElementById('newAccBalance').value) || 10000; const accType = document.getElementById('newAccType').value; const accNum = document.getElementById('newAccNumber').value.trim(); try { const res = await fetch('/api/accounts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, broker, initial_balance: balance, account_type: accType, account_number: accNum, is_default: 1 }) }); const data = await res.json(); if (data.success) { this.closeModal('modalAccount'); await this.loadAccounts(); if (data.id) await this.switchAccount(data.id); } } catch (e) { alert('خطا در ایجاد حساب: ' + e.message); } }, async deleteAccount(id) { if (!confirm('آیا از حذف این حساب و تمام معاملات آن مطمئن هستید؟')) return; try { const res = await fetch(`/api/accounts/${id}`, { method: 'DELETE' }); const data = await res.json(); if (data.success) { await this.loadAccounts(); await this.switchAccount(this.accounts[0]?.id || 1); } } catch (e) { alert('خطا در حذف حساب: ' + e.message); } }, // ------------------------------------------------------------- // ANALYTICS & CHARTS // ------------------------------------------------------------- async loadAnalytics() { try { const res = await fetch(`/api/analytics?account_id=${this.currentAccountId}`); const json = await res.json(); if (json.success && json.data) { this.analytics = json.data; this.renderSummaryTicker(); this.renderKeyMetrics(); this.renderGrowthChart(); this.renderDrawdownChart(); this.renderWinRateChart(); this.renderMonthlyReturns(); this.renderDeepAnalytics(); } } catch (e) { console.error('Error loading analytics:', e); } }, renderSummaryTicker() { const sum = this.analytics.summary; const container = document.getElementById('accountSummaryTicker'); if (!container) return; const gainSign = sum.total_gain_percent >= 0 ? '+' : ''; const gainClass = sum.total_gain_percent >= 0 ? 'text-emerald-400' : 'text-rose-400'; const gainBg = sum.total_gain_percent >= 0 ? 'bg-emerald-500/10 border-emerald-500/20' : 'bg-rose-500/10 border-rose-500/20'; const closedProfitSign = sum.total_closed_profit >= 0 ? '+' : ''; const closedProfitClass = sum.total_closed_profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; const openProfitSign = sum.open_floating_profit >= 0 ? '+' : ''; const openProfitClass = sum.open_floating_profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; container.innerHTML = `
بالانس (Balance)
$${sum.current_balance.toLocaleString('en-US', {minimumFractionDigits: 2})}
پایه: $${sum.initial_balance.toLocaleString()}
اکوئیتی (Equity)
$${sum.current_equity.toLocaleString('en-US', {minimumFractionDigits: 2})}
شناور: ${openProfitSign}$${sum.open_floating_profit.toFixed(2)}
درصد رشد کل (Gain)
${gainSign}${sum.total_gain_percent.toFixed(2)}%
بسته‌شده: ${closedProfitSign}${sum.closed_gain_percent.toFixed(1)}%
سود خالص (Net Profit)
${closedProfitSign}$${sum.total_closed_profit.toLocaleString('en-US', {minimumFractionDigits: 2})}
امروز: $${sum.today_profit.toFixed(2)}
فاکتور سود (Profit Factor)
${sum.profit_factor}
ناخالص: +$${sum.gross_profit.toFixed(0)}
نرخ برد (Win Rate)
${sum.win_rate}%
${sum.won_trades} برد / ${sum.lost_trades} باخت
`; document.getElementById('chartGainPct').textContent = `${gainSign}${sum.total_gain_percent.toFixed(2)}%`; }, renderKeyMetrics() { const sum = this.analytics.summary; document.getElementById('metricProfitFactor').textContent = sum.profit_factor; document.getElementById('metricMaxDD').textContent = `${sum.max_drawdown_percent.toFixed(1)}% ($${sum.max_drawdown_amount.toFixed(0)})`; document.getElementById('metricExpectancy').textContent = `$${sum.expectancy.toFixed(2)}`; document.getElementById('metricWinLossRatio').textContent = sum.win_loss_ratio; document.getElementById('metricSharpe').textContent = sum.sharpe_ratio; document.getElementById('metricTotalPips').textContent = sum.total_pips.toLocaleString(); document.getElementById('metricAvgWin').textContent = `$${sum.avg_win.toFixed(2)} (${sum.avg_pips_win.toFixed(1)} pips)`; document.getElementById('metricAvgLoss').textContent = `$${sum.avg_loss.toFixed(2)} (${sum.avg_pips_loss.toFixed(1)} pips)`; document.getElementById('metricMaxConsecWins').textContent = `${sum.max_consecutive_wins} معامله`; const durHours = (sum.avg_trade_duration_seconds / 3600).toFixed(1); document.getElementById('metricAvgDuration').textContent = `${durHours} ساعت`; document.getElementById('longWinRate').textContent = `${sum.long_win_rate}% (${sum.long_won}/${sum.long_trades})`; document.getElementById('longProfit').textContent = `$${sum.long_profit.toFixed(0)}`; document.getElementById('shortWinRate').textContent = `${sum.short_win_rate}% (${sum.short_won}/${sum.short_trades})`; document.getElementById('shortProfit').textContent = `$${sum.short_profit.toFixed(0)}`; }, renderGrowthChart() { const ctx = document.getElementById('chartGrowth'); if (!ctx) return; if (this.charts.growth) this.charts.growth.destroy(); const curve = this.analytics.growth_curve || []; const labels = curve.map((pt, i) => i === 0 ? 'شروع' : (pt.ticket || `#${i}`)); const balances = curve.map(pt => pt.balance); const gainPcts = curve.map(pt => pt.gain_percent); const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 300); gradient.addColorStop(0, 'rgba(59, 130, 246, 0.35)'); gradient.addColorStop(1, 'rgba(59, 130, 246, 0.0)'); this.charts.growth = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: 'بالانس ($)', data: balances, borderColor: '#3B82F6', backgroundColor: gradient, borderWidth: 2.5, fill: true, tension: 0.25, pointRadius: curve.length > 30 ? 2 : 4, pointHoverRadius: 6, yAxisID: 'y' }, { label: 'درصد رشد (%)', data: gainPcts, borderColor: '#10B981', borderWidth: 1.8, borderDash: [4, 4], fill: false, tension: 0.25, pointRadius: 0, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: '#9CA3AF', font: { family: 'Vazirmatn', size: 11 } } }, tooltip: { callbacks: { label: function(context) { if (context.datasetIndex === 0) return ` بالانس: $${context.parsed.y.toLocaleString()}`; return ` رشد: ${context.parsed.y.toFixed(2)}%`; }, afterBody: function(items) { const idx = items[0]?.dataIndex; if (idx > 0 && curve[idx]) { const pt = curve[idx]; return [`نماد: ${pt.symbol}`, `سود ترید: $${pt.profit}`, `تاریخ: ${pt.date}`]; } return []; } } } }, scales: { x: { grid: { color: 'rgba(31, 41, 61, 0.5)' }, ticks: { color: '#6B7280', font: { size: 10 } } }, y: { type: 'linear', display: true, position: 'left', grid: { color: 'rgba(31, 41, 61, 0.5)' }, ticks: { color: '#9CA3AF', callback: v => '$' + v.toLocaleString() } }, y1: { type: 'linear', display: true, position: 'right', grid: { drawOnChartArea: false }, ticks: { color: '#10B981', callback: v => v + '%' } } } } }); }, renderDrawdownChart() { const ctx = document.getElementById('chartDrawdown'); if (!ctx) return; if (this.charts.drawdown) this.charts.drawdown.destroy(); const curve = this.analytics.drawdown_curve || []; const labels = curve.map((_, i) => `#${i + 1}`); const ddData = curve.map(pt => pt.drawdown_percent); const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200); gradient.addColorStop(0, 'rgba(239, 68, 68, 0.0)'); gradient.addColorStop(1, 'rgba(239, 68, 68, 0.35)'); this.charts.drawdown = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: 'درصد افت (Drawdown %)', data: ddData, borderColor: '#EF4444', backgroundColor: gradient, borderWidth: 2, fill: true, tension: 0.2, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#9CA3AF', font: { family: 'Vazirmatn', size: 11 } } }, tooltip: { callbacks: { label: ctx => ` افت سرمایه: ${Math.abs(ctx.parsed.y).toFixed(2)}%` } } }, scales: { x: { grid: { color: 'rgba(31, 41, 61, 0.4)' }, ticks: { color: '#6B7280' } }, y: { grid: { color: 'rgba(31, 41, 61, 0.4)' }, ticks: { color: '#EF4444', callback: v => v + '%' } } } } }); }, renderWinRateChart() { const ctx = document.getElementById('chartWinRate'); if (!ctx) return; if (this.charts.winRate) this.charts.winRate.destroy(); const sum = this.analytics.summary; this.charts.winRate = new Chart(ctx, { type: 'doughnut', data: { labels: ['برد (Won)', 'باخت (Lost)', 'سر‌به‌سر (BE)'], datasets: [{ data: [sum.won_trades, sum.lost_trades, sum.breakeven_trades], backgroundColor: ['#10B981', '#EF4444', '#6B7280'], borderWidth: 0, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: '#9CA3AF', font: { family: 'Vazirmatn', size: 11 } } } } } }); }, renderMonthlyReturns() { const table = document.getElementById('monthlyReturnsTable'); if (!table) return; table.innerHTML = ''; const returns = this.analytics.monthly_returns || []; if (returns.length === 0) { table.innerHTML = `معامله‌ای در این بازه ثبت نشده است`; return; } returns.forEach(y => { const tr = document.createElement('tr'); let cols = `${y.year}`; for (let m = 1; m <= 12; m++) { const mData = y.months[m]; if (mData && mData.trades > 0) { const pct = mData.gain_percent; const bg = pct > 0 ? 'bg-emerald-500/20 text-emerald-400 font-bold' : (pct < 0 ? 'bg-rose-500/20 text-rose-400 font-bold' : 'text-gray-400'); const sign = pct > 0 ? '+' : ''; cols += `${sign}${pct.toFixed(1)}%`; } else { cols += `-`; } } const totalPct = y.total_gain_percent; const totalBg = totalPct >= 0 ? 'bg-emerald-600/30 text-emerald-300 font-extrabold' : 'bg-rose-600/30 text-rose-300 font-extrabold'; const totalSign = totalPct >= 0 ? '+' : ''; cols += `${totalSign}${totalPct.toFixed(1)}%`; tr.innerHTML = cols; table.appendChild(tr); }); }, renderDeepAnalytics() { // Performance by Symbol Chart & Table const symCtx = document.getElementById('chartSymbols'); if (symCtx) { if (this.charts.symbols) this.charts.symbols.destroy(); const syms = this.analytics.by_symbol || []; const labels = syms.map(s => s.symbol); const profits = syms.map(s => s.profit); const colors = profits.map(p => p >= 0 ? '#10B981' : '#EF4444'); this.charts.symbols = new Chart(symCtx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'سود دلاری ($)', data: profits, backgroundColor: colors, borderRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#9CA3AF' }, grid: { display: false } }, y: { ticks: { color: '#9CA3AF', callback: v => '$' + v }, grid: { color: 'rgba(31, 41, 61, 0.4)' } } } } }); const symTable = document.getElementById('symbolsTable'); if (symTable) { symTable.innerHTML = ''; syms.forEach(s => { const tr = document.createElement('tr'); const profitClass = s.profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; const sign = s.profit >= 0 ? '+' : ''; tr.innerHTML = ` ${s.symbol} ${s.count} ${s.win_rate}% ${s.pips.toFixed(1)} ${sign}$${s.profit.toFixed(2)} `; symTable.appendChild(tr); }); } } // Performance by Strategy Table const stratTable = document.getElementById('strategiesTable'); if (stratTable) { stratTable.innerHTML = ''; (this.analytics.by_strategy || []).forEach(st => { const tr = document.createElement('tr'); const profitClass = st.profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; const sign = st.profit >= 0 ? '+' : ''; tr.innerHTML = ` ${st.strategy} ${st.count} ${st.win_rate}% ${st.profit_factor} ${sign}$${st.profit.toFixed(2)} `; stratTable.appendChild(tr); }); } // Hourly Chart const hrCtx = document.getElementById('chartHourly'); if (hrCtx) { if (this.charts.hourly) this.charts.hourly.destroy(); const hourly = this.analytics.by_hour || []; const labels = hourly.map(h => `${h.hour}:00`); const counts = hourly.map(h => h.count); this.charts.hourly = new Chart(hrCtx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'تعداد معاملات', data: counts, backgroundColor: '#3B82F6', borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#6B7280', font: { size: 9 } }, grid: { display: false } }, y: { ticks: { color: '#9CA3AF', stepSize: 1 }, grid: { color: 'rgba(31, 41, 61, 0.4)' } } } } }); } // Day of Week List const dowContainer = document.getElementById('dayOfWeekList'); if (dowContainer) { dowContainer.innerHTML = ''; (this.analytics.by_day_of_week || []).forEach(d => { const profitClass = d.profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; const sign = d.profit >= 0 ? '+' : ''; const el = document.createElement('div'); el.className = 'flex items-center justify-between p-2 rounded-lg bg-gray-800/40 border border-gray-700/30'; el.innerHTML = ` ${d.name_fa} (${d.name})
${d.count} ترید ${sign}$${d.profit.toFixed(0)}
`; dowContainer.appendChild(el); }); } // Emotion List const emoContainer = document.getElementById('emotionList'); if (emoContainer) { emoContainer.innerHTML = ''; (this.analytics.by_emotion || []).forEach(em => { const profitClass = em.profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; const sign = em.profit >= 0 ? '+' : ''; const el = document.createElement('div'); el.className = 'flex items-center justify-between p-2 rounded-lg bg-gray-800/40 border border-gray-700/30'; el.innerHTML = ` ${em.emotion}
${em.win_rate}% برد ${sign}$${em.profit.toFixed(0)}
`; emoContainer.appendChild(el); }); } }, // ------------------------------------------------------------- // TRADES MANAGEMENT & LOG // ------------------------------------------------------------- async loadTrades() { const status = document.getElementById('tradeFilterStatus')?.value || 'all'; const symbol = document.getElementById('tradeFilterSymbol')?.value || 'all'; const search = document.getElementById('tradeSearch')?.value || ''; try { const res = await fetch(`/api/trades?account_id=${this.currentAccountId}&page=${this.pagination.page}&limit=${this.pagination.limit}&status=${status}&symbol=${symbol}&search=${encodeURIComponent(search)}`); const json = await res.json(); if (json.success) { this.trades = json.trades || []; this.pagination = json.pagination; this.renderTradesTable(); } } catch (e) { console.error('Error loading trades:', e); } }, renderTradesTable() { const tbody = document.getElementById('tradesTableBody'); if (!tbody) return; tbody.innerHTML = ''; document.getElementById('tradesCountLabel').textContent = this.pagination.total; document.getElementById('paginationInfo').textContent = `صفحه ${this.pagination.page} از ${this.pagination.total_pages || 1}`; document.getElementById('btnPrevPage').disabled = this.pagination.page <= 1; document.getElementById('btnNextPage').disabled = this.pagination.page >= this.pagination.total_pages; if (this.trades.length === 0) { tbody.innerHTML = `معامله‌ای یافت نشد`; return; } this.trades.forEach((t, i) => { const tr = document.createElement('tr'); const isBuy = t.trade_type.toLowerCase() === 'buy'; const badgeType = isBuy ? 'BUY' : 'SELL'; const profitVal = parseFloat(t.profit) + parseFloat(t.commission) + parseFloat(t.swap); const profitClass = profitVal >= 0 ? 'profit-pos' : 'profit-neg'; const profitSign = profitVal >= 0 ? '+' : ''; const openTimeFormatted = t.open_time ? t.open_time.substring(5, 16) : '-'; const hasChart = t.screenshot_entry || t.screenshot_exit; tr.innerHTML = ` ${t.id}
${t.symbol}
${t.ticket || '-'}
${badgeType} ${parseFloat(t.lot_size).toFixed(2)} ${parseFloat(t.open_price).toFixed(t.open_price > 50 ? 2 : 5)} ${t.close_price ? parseFloat(t.close_price).toFixed(t.close_price > 50 ? 2 : 5) : 'باز'}
SL: ${t.stop_loss ? parseFloat(t.stop_loss) : '-'}
TP: ${t.take_profit ? parseFloat(t.take_profit) : '-'}
${openTimeFormatted} ${t.pips ? (t.pips > 0 ? '+' : '') + parseFloat(t.pips).toFixed(1) : '-'} ${profitSign}$${profitVal.toFixed(2)}
${t.strategy || '-'}
${t.emotion || ''}
${hasChart ? ` ` : '-'}
`; tbody.appendChild(tr); }); }, handleSearch(e) { if (e.key === 'Enter' || !e.target.value) { this.pagination.page = 1; this.loadTrades(); } }, prevPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadTrades(); } }, nextPage() { if (this.pagination.page < this.pagination.total_pages) { this.pagination.page++; this.loadTrades(); } }, // ------------------------------------------------------------- // TRADE FORM & CALCULATION // ------------------------------------------------------------- openNewTradeModal() { document.getElementById('modalTradeTitle').innerHTML = 'ثبت معامله جدید'; document.getElementById('tradeId').value = ''; document.getElementById('tradeSymbol').value = 'EURUSD'; document.querySelector('input[name="tradeType"][value="buy"]').checked = true; document.getElementById('tradeLotSize').value = '0.10'; document.getElementById('tradeOpenPrice').value = ''; document.getElementById('tradeClosePrice').value = ''; document.getElementById('tradeStopLoss').value = ''; document.getElementById('tradeTakeProfit').value = ''; document.getElementById('tradeProfit').value = ''; document.getElementById('tradePips').value = ''; document.getElementById('tradeEntryNotes').value = ''; document.getElementById('tradeLessons').value = ''; document.getElementById('tradeScreenshotEntryUrl').value = ''; document.getElementById('tradeScreenshotExitUrl').value = ''; this.openModal('modalTrade'); }, async editTrade(id) { try { const res = await fetch(`/api/trades/${id}`); const json = await res.json(); if (json.success && json.trade) { const t = json.trade; document.getElementById('modalTradeTitle').innerHTML = 'ویرایش معامله'; document.getElementById('tradeId').value = t.id; document.getElementById('tradeSymbol').value = t.symbol; const typeRadio = document.querySelector(`input[name="tradeType"][value="${t.trade_type.toLowerCase()}"]`); if (typeRadio) typeRadio.checked = true; document.getElementById('tradeLotSize').value = t.lot_size; document.getElementById('tradeOpenPrice').value = t.open_price; document.getElementById('tradeClosePrice').value = t.close_price || ''; document.getElementById('tradeStopLoss').value = t.stop_loss || ''; document.getElementById('tradeTakeProfit').value = t.take_profit || ''; document.getElementById('tradeProfit').value = t.profit || ''; document.getElementById('tradePips').value = t.pips || ''; document.getElementById('tradeCommission').value = t.commission || '0.00'; document.getElementById('tradeSwap').value = t.swap || '0.00'; document.getElementById('tradeStrategy').value = t.strategy || 'Price Action'; document.getElementById('tradeSession').value = t.session || 'London'; document.getElementById('tradeTimeframe').value = t.timeframe || 'M15'; document.getElementById('tradeEmotion').value = t.emotion || 'Disciplined'; document.getElementById('tradeEntryNotes').value = t.entry_notes || ''; document.getElementById('tradeLessons').value = t.lessons || ''; document.getElementById('tradeScreenshotEntryUrl').value = t.screenshot_entry || ''; document.getElementById('tradeScreenshotExitUrl').value = t.screenshot_exit || ''; this.openModal('modalTrade'); } } catch (e) { alert('خطا در دریافت اطلاعات معامله: ' + e.message); } }, calcTradeFields() { const symbol = document.getElementById('tradeSymbol').value.toUpperCase().trim(); const type = document.querySelector('input[name="tradeType"]:checked')?.value || 'buy'; const lot = parseFloat(document.getElementById('tradeLotSize').value) || 0.1; const openPrice = parseFloat(document.getElementById('tradeOpenPrice').value); const closePrice = parseFloat(document.getElementById('tradeClosePrice').value); if (!isNaN(openPrice) && !isNaN(closePrice) && closePrice > 0) { const diff = (type === 'buy') ? (closePrice - openPrice) : (openPrice - closePrice); let pips = 0; let pipVal = 10 * lot; // $10 per standard lot if (symbol.includes('JPY')) { pips = diff * 100; } else if (symbol.includes('XAU') || symbol.includes('GOLD')) { pips = diff * 10; pipVal = 10 * lot; } else if (symbol.includes('BTC') || symbol.includes('US30') || symbol.includes('NAS')) { pips = diff; pipVal = 1 * lot; } else { pips = diff * 10000; } const estimatedProfit = pips * pipVal; document.getElementById('tradePips').value = pips.toFixed(1); document.getElementById('tradeProfit').value = estimatedProfit.toFixed(2); } }, async saveTrade(e) { e.preventDefault(); const tradeId = document.getElementById('tradeId').value; const symbol = document.getElementById('tradeSymbol').value.toUpperCase().trim(); const tradeType = document.querySelector('input[name="tradeType"]:checked')?.value || 'buy'; const lotSize = parseFloat(document.getElementById('tradeLotSize').value) || 0.1; const openPrice = parseFloat(document.getElementById('tradeOpenPrice').value); const closePriceVal = document.getElementById('tradeClosePrice').value; const closePrice = closePriceVal !== '' ? parseFloat(closePriceVal) : null; const stopLoss = document.getElementById('tradeStopLoss').value ? parseFloat(document.getElementById('tradeStopLoss').value) : null; const takeProfit = document.getElementById('tradeTakeProfit').value ? parseFloat(document.getElementById('tradeTakeProfit').value) : null; const profit = document.getElementById('tradeProfit').value !== '' ? parseFloat(document.getElementById('tradeProfit').value) : 0.0; const pips = document.getElementById('tradePips').value !== '' ? parseFloat(document.getElementById('tradePips').value) : 0.0; const commission = parseFloat(document.getElementById('tradeCommission').value) || 0.0; const swap = parseFloat(document.getElementById('tradeSwap').value) || 0.0; const strategy = document.getElementById('tradeStrategy').value; const session = document.getElementById('tradeSession').value; const timeframe = document.getElementById('tradeTimeframe').value; const emotion = document.getElementById('tradeEmotion').value; const entryNotes = document.getElementById('tradeEntryNotes').value; const lessons = document.getElementById('tradeLessons').value; const screenshotEntry = document.getElementById('tradeScreenshotEntryUrl').value; const screenshotExit = document.getElementById('tradeScreenshotExitUrl').value; const payload = { account_id: this.currentAccountId, symbol, trade_type: tradeType, lot_size: lotSize, open_price: openPrice, close_price: closePrice, stop_loss: stopLoss, take_profit: takeProfit, profit, pips, commission, swap, strategy, session, timeframe, emotion, entry_notes: entryNotes, lessons, screenshot_entry: screenshotEntry, screenshot_exit: screenshotExit, status: closePrice !== null ? 'closed' : 'open' }; try { const url = tradeId ? `/api/trades/${tradeId}` : '/api/trades'; const method = tradeId ? 'PUT' : 'POST'; const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { this.closeModal('modalTrade'); await this.loadAnalytics(); await this.loadTrades(); this.renderCalendar(); } else { alert('خطا: ' + json.error); } } catch (err) { alert('خطا در ذخیره معامله: ' + err.message); } }, async deleteTrade(id) { if (!confirm('آیا از حذف این معامله مطمئن هستید؟')) return; try { const res = await fetch(`/api/trades/${id}`, { method: 'DELETE' }); const json = await res.json(); if (json.success) { await this.loadAnalytics(); await this.loadTrades(); this.renderCalendar(); } } catch (e) { alert('خطا در حذف معامله: ' + e.message); } }, async uploadScreenshot(input, targetHiddenId) { if (!input.files || !input.files[0]) return; const formData = new FormData(); formData.append('image', input.files[0]); try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); const json = await res.json(); if (json.success && json.url) { document.getElementById(targetHiddenId).value = json.url; } else { alert('خطا در بارگذاری تصویر: ' + json.error); } } catch (e) { alert('خطا در ارسال تصویر: ' + e.message); } }, // ------------------------------------------------------------- // MT4 / MT5 IMPORT & EXPORT // ------------------------------------------------------------- async importStatement(e) { e.preventDefault(); const fileInput = document.getElementById('statementFile'); if (!fileInput.files || !fileInput.files[0]) { alert('لطفاً ابتدا فایل گزارش را انتخاب کنید'); return; } const formData = new FormData(); formData.append('account_id', this.currentAccountId); formData.append('statement', fileInput.files[0]); const btn = document.getElementById('btnSubmitImport'); btn.disabled = true; btn.textContent = 'در حال پردازش...'; try { const res = await fetch('/api/trades/import', { method: 'POST', body: formData }); const json = await res.json(); if (json.success) { alert(json.message); this.closeModal('modalImport'); await this.loadAnalytics(); await this.loadTrades(); this.renderCalendar(); } else { alert('خطا در ایمپورت: ' + json.error); } } catch (err) { alert('خطا در ارسال استیتمنت: ' + err.message); } finally { btn.disabled = false; btn.textContent = 'شروع پردازش و ایمپورت'; } }, exportTradesCSV() { window.location.href = `/api/trades/export?account_id=${this.currentAccountId}`; }, async resetSampleData() { if (!confirm('آیا مایلید تمام داده‌های این حساب با نمونه‌های واقعی مای‌اف‌ایکس‌بوک جایگزین شوند؟')) return; try { const res = await fetch(`/api/reset_sample?account_id=${this.currentAccountId}`, { method: 'POST' }); const json = await res.json(); if (json.success) { await this.loadAnalytics(); await this.loadTrades(); this.renderCalendar(); } } catch (e) { alert('خطا در بازیابی داده‌ها: ' + e.message); } }, // ------------------------------------------------------------- // CALENDAR VIEW // ------------------------------------------------------------- prevMonth() { this.calendarMonth--; if (this.calendarMonth < 1) { this.calendarMonth = 12; this.calendarYear--; } this.renderCalendar(); }, nextMonth() { this.calendarMonth++; if (this.calendarMonth > 12) { this.calendarMonth = 1; this.calendarYear++; } this.renderCalendar(); }, renderCalendar() { const grid = document.getElementById('calendarGrid'); if (!grid) return; grid.innerHTML = ''; const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; document.getElementById('calendarMonthLabel').textContent = `${monthNames[this.calendarMonth - 1]} ${this.calendarYear}`; const firstDay = new Date(this.calendarYear, this.calendarMonth - 1, 1); const lastDay = new Date(this.calendarYear, this.calendarMonth, 0); const totalDays = lastDay.getDate(); // Sunday is 0, Monday is 1 in JS getDay(). We want Monday = 0 .. Sunday = 6 let startDayIndex = firstDay.getDay() - 1; if (startDayIndex === -1) startDayIndex = 6; // Empty cells for preceding month for (let i = 0; i < startDayIndex; i++) { const emptyCell = document.createElement('div'); emptyCell.className = 'bg-gray-900/30 rounded-xl p-2 min-h-[70px] border border-gray-800/40 opacity-30'; grid.appendChild(emptyCell); } const dailyPnL = this.analytics?.daily_pnl || {}; for (let day = 1; day <= totalDays; day++) { const dateStr = `${this.calendarYear}-${String(this.calendarMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const dayData = dailyPnL[dateStr]; const cell = document.createElement('div'); cell.className = 'rounded-xl p-2.5 min-h-[70px] border transition flex flex-col justify-between '; if (dayData && dayData.trades > 0) { const isPos = dayData.profit >= 0; cell.className += isPos ? 'bg-emerald-500/10 border-emerald-500/30 hover:border-emerald-400' : 'bg-rose-500/10 border-rose-500/30 hover:border-rose-400'; const sign = dayData.profit >= 0 ? '+' : ''; const profitClass = dayData.profit >= 0 ? 'text-emerald-400' : 'text-rose-400'; cell.innerHTML = `
${day} ${dayData.trades} معامله
${sign}$${dayData.profit.toFixed(2)}
${dayData.pips ? (dayData.pips > 0 ? '+' : '') + dayData.pips.toFixed(1) + ' p' : ''}
`; } else { cell.className += 'bg-gray-800/20 border-gray-800/60 text-gray-500'; cell.innerHTML = `
${day}
-
`; } grid.appendChild(cell); } }, // ------------------------------------------------------------- // UI UTILITIES & MODALS // ------------------------------------------------------------- switchTab(tabId) { this.activeTab = tabId; ['overview', 'deep_analytics', 'trades', 'calendar', 'settings'].forEach(t => { const btn = document.getElementById(`tab-${t}`); const view = document.getElementById(`view-${t}`); if (btn && view) { if (t === tabId) { btn.classList.add('active'); view.classList.remove('hidden'); } else { btn.classList.remove('active'); view.classList.add('hidden'); } } }); }, openModal(id) { const m = document.getElementById(id); if (m) m.classList.remove('hidden'); }, closeModal(id) { const m = document.getElementById(id); if (m) m.classList.add('hidden'); }, openLightbox(imgUrl) { if (!imgUrl) return; const m = document.getElementById('modalLightbox'); const img = document.getElementById('lightboxImg'); img.src = imgUrl; m.classList.remove('hidden'); }, toggleTheme() { const body = document.body; const icon = document.getElementById('themeIcon'); if (body.classList.contains('light-mode')) { body.classList.remove('light-mode'); icon.className = 'fa-solid fa-moon text-amber-400'; } else { body.classList.add('light-mode'); icon.className = 'fa-solid fa-sun text-amber-500'; } } }; // Bootstrap application on DOM ready document.addEventListener('DOMContentLoaded', () => app.init());