/**
* 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.initial_balance.toLocaleString()}
اکوئیتی (Equity)
شناور: ${openProfitSign}$${sum.open_floating_profit.toFixed(2)}
درصد رشد کل (Gain)
بستهشده: ${closedProfitSign}${sum.closed_gain_percent.toFixed(1)}%
سود خالص (Net Profit)
امروز: $${sum.today_profit.toFixed(2)}
فاکتور سود (Profit Factor)
ناخالص: +$${sum.gross_profit.toFixed(0)}
نرخ برد (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());