/home/devscfvi/web.devsquantum.com/js/history.js
// ============================================
// TRADE HISTORY & MANAGEMENT
// ============================================
// Exit screenshot state (per trade)
const exitScreenshotState = {};
// Selection state for mass delete
const historySelection = new Set();
// Module-level trade list for select-all (avoids JSON embedding in onclick)
let _historyTrades = [];
// Render History View
function renderHistory() {
const trades = state.trades || [];
_historyTrades = trades;
// Populate registry so onclick handlers can reference trades by ID (avoids JSON-in-attribute bugs)
window._tradeRegistry = window._tradeRegistry || {};
trades.forEach(t => { window._tradeRegistry[t.id] = t; });
// Quick stats for the top bar (desktop)
const wins = trades.filter(t => t.status === 'win').length;
const losses = trades.filter(t => t.status === 'loss').length;
const pending = trades.filter(t => t.status === 'pending' || !t.status).length;
const winRate = trades.length > 0 ? Math.round((wins / trades.length) * 100) : 0;
return `
<div class="min-h-screen p-4 md:p-6">
<div class="max-w-2xl md:max-w-none mx-auto">
<!-- Header row -->
<div class="flex items-center justify-between mb-4 pt-2 md:pt-0">
<div>
<h1 class="text-xl font-bold text-white md:hidden">Trade History</h1>
<!-- Desktop quick-stats pill row -->
<div class="hidden md:flex items-center gap-4 text-sm">
<span class="text-slate-400">${trades.length} trades</span>
<span class="text-green-400 font-semibold">${wins}W</span>
<span class="text-red-400 font-semibold">${losses}L</span>
${pending > 0 ? `<span class="text-yellow-400 font-semibold">${pending} pending</span>` : ''}
${trades.length > 0 ? `<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-violet-500/20 text-violet-300">${winRate}% win rate</span>` : ''}
</div>
</div>
<div class="flex gap-2">
${trades.length > 0 ? `
<button onclick="exportTrades()"
class="bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded-xl shadow-lg active:scale-95 transition flex items-center gap-1.5 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span class="hidden md:inline">Export</span>
</button>
` : ''}
<button onclick="navigateTo('import')"
class="bg-violet-600 hover:bg-violet-700 text-white px-3 py-2 rounded-xl shadow-lg active:scale-95 transition flex items-center gap-1.5 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
</svg>
<span class="hidden md:inline">Import</span>
</button>
<button onclick="showFilterMenu()"
class="bg-slate-700 hover:bg-slate-600 text-white px-3 py-2 rounded-xl shadow-lg active:scale-95 transition flex items-center gap-1.5 text-sm font-medium">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
</svg>
<span class="hidden md:inline">Filter</span>
</button>
</div>
</div>
${trades.length === 0 ? renderEmptyTrades() : `
<!-- Bulk delete bar (shown when items are selected) -->
<div id="bulk-delete-bar" class="hidden items-center gap-3 mb-3 p-3 bg-red-500/10 border border-red-500/20 rounded-xl">
<span id="bulk-delete-count" class="text-red-300 text-sm font-medium">0 selected</span>
<button onclick="deleteSelectedTrades()"
class="bg-red-600 hover:bg-red-700 text-white text-sm px-3 py-1.5 rounded-lg font-medium active:scale-95 transition">
Delete Selected
</button>
<button onclick="clearTradeSelection()"
class="text-slate-400 hover:text-white text-sm px-2 py-1.5 transition">
Clear
</button>
<button onclick="selectAllTrades(true)"
class="text-violet-400 hover:text-violet-300 text-sm px-2 py-1.5 transition ml-auto">
Select All (${trades.length})
</button>
</div>
<!-- Mobile: Cards -->
<div class="md:hidden space-y-3">
${trades.map(trade => renderTradeCard(trade)).join('')}
</div>
<!-- Desktop: Dense Data Table -->
<div class="hidden md:block">
${renderTradesTable(trades)}
</div>
`}
</div>
</div>
`;
}
// Empty state
function renderEmptyTrades() {
return `
<div class="glass rounded-3xl p-12 text-center border border-violet-500/20">
<svg class="w-16 h-16 mx-auto text-violet-400/50 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-lg text-slate-400 mb-2">No trades yet</p>
<p class="text-sm text-slate-500 mb-4">Start by calculating and saving your first trade</p>
<button onclick="navigateTo('calculator')"
class="gradient-bg text-white px-6 py-3 rounded-xl font-semibold active:scale-95 transition">
Go to Calculator
</button>
</div>
`;
}
// Render trades list
function renderTradesList(trades) {
return `
<div class="space-y-3">
${trades.map(trade => renderTradeCard(trade)).join('')}
</div>
`;
}
// ============================================
// DESKTOP DATA TABLE VIEW
// ============================================
function renderTradesTable(trades) {
return `
<div class="glass rounded-2xl border border-violet-500/20 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="bg-violet-500/10 border-b border-violet-500/20 text-violet-300 text-left">
<th class="pl-3 pr-1 py-3 w-8">
<input type="checkbox" id="select-all-cb" title="Select all"
onchange="event.stopPropagation(); selectAllTrades(this.checked)"
class="w-3.5 h-3.5 rounded accent-violet-500 cursor-pointer">
</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap w-28">Date</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap">Symbol</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap">Type</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap">Category</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap text-right">Position</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap text-right">Entry</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap text-right">Loss</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap text-right">Profit</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap text-right">R:R</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap">Status</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap">Emotion</th>
<th class="px-3 py-3 font-semibold whitespace-nowrap text-center">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-violet-500/10">
${trades.map(trade => renderTradeRow(trade)).join('')}
</tbody>
</table>
</div>
</div>
`;
}
// Render a single row for the desktop data table
function renderTradeRow(trade) {
const statusStyles = {
'win': 'bg-green-500/15 text-green-400 border border-green-500/30',
'loss': 'bg-red-500/15 text-red-400 border border-red-500/30',
'pending': 'bg-yellow-500/15 text-yellow-400 border border-yellow-500/30'
};
const statusStyle = statusStyles[trade.status] || statusStyles['pending'];
// Prefer trade_date (actual trade date) over created_at (import timestamp)
const _displayDate = trade.trade_date || trade.created_at || '';
const dateStr = _displayDate
? (() => {
// Parse as local date to avoid timezone shift: "YYYY-MM-DD" → no UTC offset issue
const parts = _displayDate.substring(0, 10).split('-');
if (parts.length === 3) {
const d = new Date(+parts[0], +parts[1] - 1, +parts[2]);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
}
return _displayDate.substring(0, 10);
})()
: '—';
const extras = Array.isArray(trade.screenshots) ? trade.screenshots : [];
const hasScreenshots = trade.entry_screenshot || trade.exit_screenshot || extras.length > 0;
const hasNotes = trade.comment || trade.post_trade_review || trade.mistake_type;
return `
<tr id="trade-row-${trade.id}"
class="hover:bg-violet-500/5 transition-colors cursor-pointer group"
onclick="openTradeDetailModal(window._tradeRegistry[${trade.id}])">
<!-- Checkbox cell — stopPropagation so row click doesn't also toggle -->
<td class="pl-3 pr-1 py-2.5" onclick="event.stopPropagation()">
<input type="checkbox" id="cb-${trade.id}"
class="trade-checkbox w-3.5 h-3.5 rounded accent-violet-500 cursor-pointer"
onchange="toggleTradeSelection(${trade.id})"
${historySelection.has(trade.id) ? 'checked' : ''}>
</td>
<td class="px-3 py-2.5 text-slate-500 text-xs whitespace-nowrap">${dateStr}</td>
<td class="px-3 py-2.5">
<div class="flex items-center gap-1.5">
<span class="font-bold text-white">${trade.coin || '—'}</span>
${hasScreenshots ? '<span class="text-violet-400/60" title="Has screenshots">📷</span>' : ''}
${hasNotes ? '<span class="text-blue-400/60" title="Has notes">💬</span>' : ''}
</div>
</td>
<td class="px-3 py-2.5">
${trade.order_type
? `<span class="px-1.5 py-0.5 rounded text-xs font-medium bg-blue-500/15 text-blue-400 uppercase">${trade.order_type}</span>`
: '<span class="text-slate-700">—</span>'}
</td>
<td class="px-3 py-2.5">
${trade.trade_category
? `<span class="category-badge category-${trade.trade_category} text-xs">${getCategoryLabel(trade.trade_category)}</span>`
: '<span class="text-slate-700">—</span>'}
</td>
<td class="px-3 py-2.5 text-right text-slate-300 tabular-nums">$${parseFloat(trade.position_size || 0).toLocaleString()}</td>
<td class="px-3 py-2.5 text-right text-slate-300 tabular-nums">$${parseFloat(trade.entry_price || 0).toLocaleString()}</td>
<td class="px-3 py-2.5 text-right text-red-400 tabular-nums font-medium">-$${parseFloat(trade.loss_amount || 0).toFixed(2)}</td>
<td class="px-3 py-2.5 text-right text-green-400 tabular-nums font-medium">+$${parseFloat(trade.profit_amount || 0).toFixed(2)}</td>
<td class="px-3 py-2.5 text-right text-violet-300 tabular-nums">1:${trade.reward_risk_ratio || '—'}</td>
<td class="px-3 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs font-bold uppercase ${statusStyle}">
${trade.status || 'pending'}
</span>
</td>
<td class="px-3 py-2.5">
${trade.emotion
? `<span class="emotion-badge emotion-${trade.emotion} text-xs">${getEmotionLabel(trade.emotion)}</span>`
: '<span class="text-slate-700">—</span>'}
</td>
<!-- Actions — stop propagation so clicking buttons doesn't open detail view -->
<td class="px-3 py-2.5" onclick="event.stopPropagation()">
<div class="flex items-center justify-center gap-0.5 opacity-50 group-hover:opacity-100 transition-opacity">
${(trade.status === 'open' || !trade.status || trade.status === 'pending') && trade.entry_price > 0 ? `
<button onclick="openClosingTradeModal(window._tradeRegistry[${trade.id}])"
class="text-green-400 hover:text-green-300 p-1.5 rounded hover:bg-green-500/10 transition"
title="Close Trade">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</button>
` : ''}
<button onclick="openTradeModal(window._tradeRegistry[${trade.id}])"
class="text-blue-400 hover:text-blue-300 p-1.5 rounded hover:bg-blue-500/10 transition"
title="Edit Trade">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button onclick="confirmDeleteTrade(${trade.id})"
class="text-red-400 hover:text-red-300 p-1.5 rounded hover:bg-red-500/10 transition"
title="Delete Trade">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</td>
</tr>
${hasNotes ? `
<tr onclick="openTradeDetailModal(window._tradeRegistry[${trade.id}])" class="cursor-pointer hover:bg-violet-500/5 transition-colors">
<td colspan="13" class="pl-10 pr-4 pb-2 pt-0">
<div class="flex flex-wrap gap-3 text-xs">
${trade.comment ? `<span class="text-slate-500">💬 ${trade.comment.substring(0, 100)}${trade.comment.length > 100 ? '…' : ''}</span>` : ''}
${trade.mistake_type ? `<span class="text-red-400/70">⚠️ ${trade.mistake_type}</span>` : ''}
${trade.discipline_score ? `<span class="text-violet-400/70">📊 ${trade.discipline_score}/10</span>` : ''}
</div>
</td>
</tr>
` : ''}
`;
}
// Render individual trade card
function renderTradeCard(trade) {
const statusColor = {
'win': 'green',
'loss': 'red',
'pending': 'yellow'
}[trade.status] || 'slate';
const statusBadge = {
'win': '<span class="px-2 py-1 bg-green-500/20 text-green-400 text-xs font-semibold rounded-full">WIN</span>',
'loss': '<span class="px-2 py-1 bg-red-500/20 text-red-400 text-xs font-semibold rounded-full">LOSS</span>',
'pending': '<span class="px-2 py-1 bg-yellow-500/20 text-yellow-400 text-xs font-semibold rounded-full">PENDING</span>'
}[trade.status] || '<span class="px-2 py-1 bg-slate-500/20 text-slate-400 text-xs font-semibold rounded-full">UNKNOWN</span>';
return `
<div class="glass rounded-2xl p-5 border border-violet-500/20 fade-in">
<!-- Header -->
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<div class="flex items-center gap-2 flex-wrap mb-1">
<h3 class="text-xl font-bold text-white">${trade.coin}</h3>
${trade.order_type ? `<span class="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs font-semibold rounded-full uppercase">${trade.order_type}</span>` : ''}
${statusBadge}
${trade.trade_category ? `<span class="category-badge category-${trade.trade_category}">${getCategoryLabel(trade.trade_category)}</span>` : ''}
</div>
<p class="text-sm text-violet-300">${formatDateTime(trade.created_at)}</p>
<p class="text-sm text-violet-400 mt-1">R:R 1:${trade.reward_risk_ratio}</p>
${trade.emotion ? `<span class="emotion-badge emotion-${trade.emotion} mt-2 inline-block">${getEmotionLabel(trade.emotion)}</span>` : ''}
</div>
<!-- Actions -->
<div class="flex gap-2">
${trade.status === 'open' || !trade.status ? `
<button onclick="openClosingTradeModal(window._tradeRegistry[${trade.id}])"
class="text-green-400 hover:text-green-300 p-2 transition"
title="Close Trade with P&L">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</button>
` : ''}
<button onclick="openTradeModal(window._tradeRegistry[${trade.id}])"
class="text-blue-400 hover:text-blue-300 p-2 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button onclick="confirmDeleteTrade(${trade.id})"
class="text-red-400 hover:text-red-300 p-2 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</div>
<!-- Pre-Trade Plan -->
${trade.pre_trade_plan ? `
<div class="mb-3 p-3 bg-blue-500/10 rounded-lg border border-blue-500/20">
<p class="text-xs text-blue-300 mb-1 font-semibold">📝 Pre-Trade Plan:</p>
<p class="text-sm text-white">${trade.pre_trade_plan}</p>
</div>
` : ''}
<!-- Comment -->
${trade.comment ? `
<div class="mb-3 p-3 bg-slate-900/50 rounded-lg">
<p class="text-xs text-slate-400 mb-1">💬 Comment:</p>
<p class="text-sm text-white">${trade.comment}</p>
</div>
` : ''}
<!-- Post-Trade Review -->
${trade.post_trade_review ? `
<div class="mb-3 p-3 bg-violet-500/10 rounded-lg border border-violet-500/20">
<p class="text-xs text-violet-300 mb-1 font-semibold">📊 Post-Trade Review:</p>
<p class="text-sm text-white">${trade.post_trade_review}</p>
${trade.discipline_score ? `<p class="text-xs text-violet-400 mt-2">Discipline: ${trade.discipline_score}/10</p>` : ''}
</div>
` : ''}
<!-- Mistake -->
${trade.mistake_type ? `
<div class="mb-3 p-3 bg-red-500/10 rounded-lg border border-red-500/20">
<p class="text-xs text-red-300 mb-1 font-semibold">⚠️ Mistake:</p>
<p class="text-sm text-white">${trade.mistake_type}</p>
${trade.lesson_learned ? `<p class="text-xs text-red-300 mt-2">💡 Lesson: ${trade.lesson_learned}</p>` : ''}
</div>
` : ''}
<!-- Screenshots (mobile card — show all photos) -->
${(() => {
const cardExtras = Array.isArray(trade.screenshots) ? trade.screenshots : [];
const cardPhotos = [
trade.entry_screenshot ? { url: trade.entry_screenshot, label: '📸 Entry' } : null,
trade.exit_screenshot ? { url: trade.exit_screenshot, label: '📸 Exit' } : null,
...cardExtras.map((url, i) => ({ url, label: `📷 Photo ${i+1}` }))
].filter(Boolean);
return cardPhotos.length > 0 ? `
<div class="mb-3 grid grid-cols-2 gap-2">
${cardPhotos.map(p => `
<div class="bg-slate-900/30 rounded-lg overflow-hidden border border-violet-500/20">
<img src="${p.url}"
onclick='viewScreenshot(${JSON.stringify(p.url)})'
class="w-full h-32 object-cover cursor-pointer hover:opacity-80 transition"
alt="${p.label}">
<p class="text-xs text-center text-slate-400 py-1.5">${p.label}</p>
</div>
`).join('')}
</div>
` : '';
})()}
<!-- Trade Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<div class="bg-slate-900/50 rounded-lg p-3">
<p class="text-slate-400 text-xs mb-1">Position</p>
<p class="text-white font-semibold">$${trade.position_size}</p>
</div>
<div class="bg-slate-900/50 rounded-lg p-3">
<p class="text-slate-400 text-xs mb-1">Entry</p>
<p class="text-white font-semibold">$${trade.entry_price}</p>
</div>
<div class="bg-red-500/10 rounded-lg p-3 border border-red-500/20">
<p class="text-red-300 text-xs mb-1">Max Loss</p>
<p class="text-red-400 font-semibold">-$${trade.loss_amount}</p>
</div>
<div class="bg-green-500/10 rounded-lg p-3 border border-green-500/20">
<p class="text-green-300 text-xs mb-1">Max Profit</p>
<p class="text-green-400 font-semibold">+$${trade.profit_amount}</p>
</div>
</div>
</div>
`;
}
// Open trade modal for editing
function openTradeModal(trade) {
const modalContent = `
<div class="mb-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold text-white">Edit Trade: ${trade.coin}</h2>
<button onclick="closeAllModals()" class="text-slate-400 hover:text-white p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<p class="text-violet-300 text-sm mt-1">${formatDateTime(trade.created_at)}</p>
</div>
<!-- Risk/Reward Display -->
<div class="bg-violet-500/20 rounded-2xl p-4 border border-violet-500/30 mb-4">
<div class="flex items-center justify-between">
<span class="text-sm text-violet-300">Risk/Reward Ratio</span>
<span class="text-2xl font-bold text-white">1:${trade.reward_risk_ratio}</span>
</div>
</div>
<!-- Edit Prices -->
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-red-300 mb-2">Stop Loss Price</label>
<input type="number" id="edit-sl-${trade.id}" value="${trade.sl_price}" step="0.01"
class="w-full px-4 py-3 bg-slate-900/50 border border-red-500/30 rounded-xl text-white placeholder-slate-500 focus:border-red-400 focus:ring-2 focus:ring-red-400/20">
</div>
<div>
<label class="block text-sm font-medium text-green-300 mb-2">Take Profit Price</label>
<input type="number" id="edit-tp-${trade.id}" value="${trade.tp_price}" step="0.01"
class="w-full px-4 py-3 bg-slate-900/50 border border-green-500/30 rounded-xl text-white placeholder-slate-500 focus:border-green-400 focus:ring-2 focus:ring-green-400/20">
</div>
</div>
<button onclick="updateTradePrices(${trade.id})"
class="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-xl font-semibold active:scale-95 transition mb-4">
Update Prices
</button>
<!-- Update Status -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Trade Status</label>
<div class="grid grid-cols-3 gap-2">
<button onclick="updateStatus(${trade.id}, 'win')"
class="bg-green-600 hover:bg-green-700 text-white py-3 rounded-xl font-semibold active:scale-95 transition">
Win
</button>
<button onclick="updateStatus(${trade.id}, 'loss')"
class="bg-red-600 hover:bg-red-700 text-white py-3 rounded-xl font-semibold active:scale-95 transition">
Loss
</button>
<button onclick="updateStatus(${trade.id}, 'pending')"
class="bg-slate-600 hover:bg-slate-700 text-white py-3 rounded-xl font-semibold active:scale-95 transition">
Pending
</button>
</div>
</div>
<!-- Post-Trade Review -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Post-Trade Review</label>
<textarea id="review-${trade.id}"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white placeholder-slate-500 focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20"
rows="3"
placeholder="What happened? Did you follow your plan?">${trade.post_trade_review || ''}</textarea>
</div>
<!-- Discipline Score -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Discipline Score (1-10)</label>
<input type="number" id="discipline-${trade.id}" value="${trade.discipline_score || ''}" min="1" max="10"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white placeholder-slate-500 focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20"
placeholder="How well did you stick to your plan?">
</div>
<!-- Mistake Type -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Mistake (if any)</label>
<select id="mistake-${trade.id}"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20">
<option value="">No mistake</option>
<option value="Entered too early" ${trade.mistake_type === 'Entered too early' ? 'selected' : ''}>Entered too early</option>
<option value="Entered too late" ${trade.mistake_type === 'Entered too late' ? 'selected' : ''}>Entered too late</option>
<option value="Ignored stop loss" ${trade.mistake_type === 'Ignored stop loss' ? 'selected' : ''}>Ignored stop loss</option>
<option value="Moved stop loss" ${trade.mistake_type === 'Moved stop loss' ? 'selected' : ''}>Moved stop loss</option>
<option value="Took profit too early" ${trade.mistake_type === 'Took profit too early' ? 'selected' : ''}>Took profit too early</option>
<option value="Greed - held too long" ${trade.mistake_type === 'Greed - held too long' ? 'selected' : ''}>Greed - held too long</option>
<option value="FOMO entry" ${trade.mistake_type === 'FOMO entry' ? 'selected' : ''}>FOMO entry</option>
<option value="Revenge trading" ${trade.mistake_type === 'Revenge trading' ? 'selected' : ''}>Revenge trading</option>
<option value="Overleveraged" ${trade.mistake_type === 'Overleveraged' ? 'selected' : ''}>Overleveraged</option>
<option value="No clear plan" ${trade.mistake_type === 'No clear plan' ? 'selected' : ''}>No clear plan</option>
<option value="Ignored trend" ${trade.mistake_type === 'Ignored trend' ? 'selected' : ''}>Ignored trend</option>
<option value="Poor risk/reward" ${trade.mistake_type === 'Poor risk/reward' ? 'selected' : ''}>Poor risk/reward</option>
</select>
</div>
<!-- Lesson Learned -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Lesson Learned</label>
<textarea id="lesson-${trade.id}"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white placeholder-slate-500 focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20"
rows="2"
placeholder="What did you learn from this trade?">${trade.lesson_learned || ''}</textarea>
</div>
<!-- Comments -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Comments</label>
<textarea id="comment-${trade.id}"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white placeholder-slate-500 focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20"
rows="2"
placeholder="Additional notes...">${trade.comment || ''}</textarea>
</div>
<!-- Exit Screenshot Upload -->
<div class="mb-4 p-4 bg-slate-900/30 rounded-xl border border-violet-500/20">
<label class="block text-sm font-medium text-violet-200 mb-3">
📸 Exit Screenshot (Optional)
</label>
<p class="text-xs text-slate-400 mb-3">Upload a screenshot of your chart at exit</p>
${trade.exit_screenshot ? `
<div class="mb-3">
<img src="${trade.exit_screenshot}"
class="w-full rounded-lg border-2 border-violet-500/30 mb-2"
alt="Current Exit Screenshot">
<p class="text-xs text-green-400">✓ Exit screenshot already uploaded</p>
</div>
` : ''}
<input type="file"
id="exit-screenshot-input-${trade.id}"
accept="image/*"
onchange="handleExitScreenshot(${trade.id}, this)"
class="hidden">
<div id="exit-screenshot-preview-${trade.id}" class="hidden mb-3">
<img id="exit-screenshot-img-${trade.id}"
class="w-full rounded-lg border-2 border-violet-500/30 mb-2"
alt="Exit Screenshot Preview">
<button onclick="clearExitScreenshot(${trade.id})"
class="text-sm text-red-400 hover:text-red-300">
✕ Remove Screenshot
</button>
</div>
<button type="button"
onclick="document.getElementById('exit-screenshot-input-${trade.id}').click()"
class="w-full bg-violet-600 hover:bg-violet-700 text-white py-3 rounded-xl font-semibold active:scale-95 transition">
<span id="exit-screenshot-button-${trade.id}">📷 ${trade.exit_screenshot ? 'Replace' : 'Upload'} Exit Screenshot</span>
</button>
</div>
<!-- Save Review Button -->
<button onclick="saveTradeReview(${trade.id})"
class="w-full gradient-bg text-white py-4 rounded-xl font-bold text-lg shadow-lg active:scale-95 transition mb-3">
Save Changes
</button>
<button onclick="closeAllModals()"
class="w-full bg-slate-700 hover:bg-slate-600 text-white py-3 rounded-xl font-semibold active:scale-95 transition">
Close
</button>
`;
createModal(modalContent);
}
// Update trade prices
async function updateTradePrices(tradeId) {
const slPrice = document.getElementById(`edit-sl-${tradeId}`).value;
const tpPrice = document.getElementById(`edit-tp-${tradeId}`).value;
const result = await apiPost('updateTradePrices', { id: tradeId, slPrice, tpPrice });
if (result.success) {
showToast('Prices updated successfully!', 'success');
await loadTrades();
closeAllModals();
render();
} else {
showToast('Failed to update prices', 'error');
}
}
// Update trade status
async function updateStatus(tradeId, status) {
const comment = document.getElementById(`comment-${tradeId}`)?.value || '';
const result = await apiPost('updateTradeStatus', { id: tradeId, status, comment });
if (result.success) {
showToast(`Trade marked as ${status}!`, 'success');
await loadTrades();
await loadStats();
closeAllModals();
render();
} else {
showToast('Failed to update status', 'error');
}
}
// Save trade review
async function saveTradeReview(tradeId) {
const review = document.getElementById(`review-${tradeId}`).value;
const disciplineScore = document.getElementById(`discipline-${tradeId}`).value;
const mistakeType = document.getElementById(`mistake-${tradeId}`).value;
const lessonLearned = document.getElementById(`lesson-${tradeId}`).value;
const comment = document.getElementById(`comment-${tradeId}`).value;
const exitScreenshot = exitScreenshotState[tradeId] || null;
const result = await apiPost('updateTradeReview', {
id: tradeId,
review,
disciplineScore,
mistakeType,
lessonLearned,
exitScreenshot
});
if (result.success) {
// Also update comment if changed
await apiPost('updateTradeStatus', {
id: tradeId,
status: state.trades.find(t => t.id === tradeId)?.status || 'pending',
comment
});
// Clear screenshot state
delete exitScreenshotState[tradeId];
showToast('Review saved successfully!', 'success');
await loadTrades();
closeAllModals();
render();
} else {
showToast('Failed to save review', 'error');
}
}
// Confirm delete trade
function confirmDeleteTrade(tradeId) {
if (confirm('Are you sure you want to delete this trade? This action cannot be undone.')) {
deleteTrade(tradeId);
}
}
// Delete trade
async function deleteTrade(tradeId) {
const result = await apiPost('deleteTrade', { id: tradeId });
if (result.success) {
showToast('Trade deleted', 'success');
await loadTrades();
await loadStats();
render();
} else {
showToast('Failed to delete trade', 'error');
}
}
// Export trades
function exportTrades() {
if (!state.trades || state.trades.length === 0) {
showToast('No trades to export', 'info');
return;
}
const csv = [
['Date', 'Coin', 'Category', 'Order Type', 'Position', 'Entry', 'SL%', 'TP%', 'SL Price', 'TP Price',
'Max Loss', 'Max Profit', 'Fees', 'R:R', 'Status', 'Emotion', 'Discipline', 'Mistake', 'Pre-Trade Plan', 'Post-Trade Review', 'Lesson', 'Comment'],
...state.trades.map(t => [
t.trade_date || t.created_at,
t.coin,
t.trade_category || '',
t.order_type || 'market',
t.position_size,
t.entry_price,
t.stop_loss_percent,
t.take_profit_percent,
t.sl_price,
t.tp_price,
t.loss_amount,
t.profit_amount,
t.fees || 0,
t.reward_risk_ratio,
t.status || 'pending',
t.emotion || '',
t.discipline_score || '',
t.mistake_type || '',
(t.pre_trade_plan || '').replace(/,/g, ';').replace(/\n/g, ' '),
(t.post_trade_review || '').replace(/,/g, ';').replace(/\n/g, ' '),
(t.lesson_learned || '').replace(/,/g, ';').replace(/\n/g, ' '),
(t.comment || '').replace(/,/g, ';').replace(/\n/g, ' ')
])
];
exportToCSV(csv, 'trades');
}
// Show filter menu (placeholder)
function showFilterMenu() {
showToast('Filters coming soon!', 'info');
}
// Upload additional photo to a trade (multiple photos support)
async function uploadTradePhoto(tradeId, inputEl) {
const file = inputEl.files[0];
if (!file) return;
showToast('Uploading photo...', 'info');
try {
const formData = new FormData();
formData.append('screenshot', file);
const uploadRes = await fetch('upload_screenshot.php', { method: 'POST', body: formData });
const uploadData = await uploadRes.json();
if (!uploadData.success || !uploadData.url) {
showToast('Upload failed: ' + (uploadData.error || 'Unknown error'), 'error');
return;
}
// Register in the API's screenshots array
const res = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'addTradeScreenshot', id: tradeId, url: uploadData.url })
});
const data = await res.json();
if (data.success) {
showToast('Photo added!', 'success');
// Update the registry and re-open modal
if (window._tradeRegistry && window._tradeRegistry[tradeId]) {
window._tradeRegistry[tradeId].screenshots = data.screenshots;
}
// Reload trades then re-open detail
await loadTrades();
const updatedTrade = (state.trades || []).find(t => t.id == tradeId);
if (updatedTrade) {
closeAllModals();
setTimeout(() => openTradeDetailModal(updatedTrade), 50);
}
} else {
showToast('Failed to save photo', 'error');
}
} catch (err) {
showToast('Upload error: ' + err.message, 'error');
}
}
// Remove a photo from a trade's extra screenshots
async function removeTradePhoto(tradeId, url) {
if (!confirm('Remove this photo?')) return;
try {
const res = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'removeTradeScreenshot', id: tradeId, url })
});
const data = await res.json();
if (data.success) {
showToast('Photo removed', 'success');
if (window._tradeRegistry && window._tradeRegistry[tradeId]) {
window._tradeRegistry[tradeId].screenshots = data.screenshots;
}
await loadTrades();
const updatedTrade = (state.trades || []).find(t => t.id == tradeId);
if (updatedTrade) {
closeAllModals();
setTimeout(() => openTradeDetailModal(updatedTrade), 50);
}
}
} catch (err) {
showToast('Error: ' + err.message, 'error');
}
}
// View screenshot in modal
function viewScreenshot(base64Image) {
const modal = createModal(`
<div class="text-center">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-white">Screenshot</h3>
<button onclick="closeAllModals()" class="text-slate-400 hover:text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<img src="${base64Image}" class="max-w-full max-h-[70vh] mx-auto rounded-xl shadow-2xl">
<button onclick="closeAllModals()"
class="mt-4 bg-violet-600 hover:bg-violet-700 px-6 py-2 rounded-lg text-white font-semibold active:scale-95 transition">
Close
</button>
</div>
`, 'screenshot-modal');
}
// Open modal to close trade with P&L
function openClosingTradeModal(trade) {
const modalContent = `
<div class="mb-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold text-white">Close Trade: ${trade.coin}</h2>
<button onclick="closeAllModals()" class="text-slate-400 hover:text-white p-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<p class="text-violet-300 text-sm mt-1">Enter your actual results</p>
</div>
<!-- Trade Info -->
<div class="bg-violet-500/10 rounded-xl p-4 mb-4 border border-violet-500/20">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="text-slate-400">Entry Price</p>
<p class="text-white font-semibold text-lg">$${trade.entry_price}</p>
</div>
<div>
<p class="text-slate-400">Position Size</p>
<p class="text-white font-semibold text-lg">$${trade.position_size}</p>
</div>
<div>
<p class="text-slate-400">Max Loss</p>
<p class="text-red-400 font-semibold">-$${trade.loss_amount}</p>
</div>
<div>
<p class="text-slate-400">Max Profit</p>
<p class="text-green-400 font-semibold">+$${trade.profit_amount}</p>
</div>
</div>
</div>
<!-- Result Selection -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Trade Result</label>
<div class="grid grid-cols-2 gap-3">
<button onclick="document.getElementById('status-win').checked = true; updatePnLSuggestion('win', ${trade.profit_amount})"
class="p-4 rounded-xl border-2 border-green-500/30 bg-green-500/10 hover:bg-green-500/20 transition">
<p class="text-green-400 font-bold text-lg">WIN ✓</p>
<p class="text-xs text-slate-400 mt-1">Profit taken</p>
</button>
<button onclick="document.getElementById('status-loss').checked = true; updatePnLSuggestion('loss', -${trade.loss_amount})"
class="p-4 rounded-xl border-2 border-red-500/30 bg-red-500/10 hover:bg-red-500/20 transition">
<p class="text-red-400 font-bold text-lg">LOSS ✗</p>
<p class="text-xs text-slate-400 mt-1">Stop loss hit</p>
</button>
</div>
<input type="radio" id="status-win" name="trade-status" value="win" class="hidden">
<input type="radio" id="status-loss" name="trade-status" value="loss" class="hidden">
</div>
<!-- Exit Price -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Actual Exit Price</label>
<input type="number"
id="actual-exit-price-${trade.id}"
step="0.01"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white placeholder-slate-500 focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20"
placeholder="Enter exit price">
</div>
<!-- Actual P&L -->
<div class="mb-4">
<label class="block text-sm font-medium text-violet-200 mb-2">Actual P&L ($)</label>
<input type="number"
id="actual-pnl-${trade.id}"
step="0.01"
class="w-full px-4 py-3 bg-slate-900/50 border border-violet-500/30 rounded-xl text-white placeholder-slate-500 focus:border-violet-400 focus:ring-2 focus:ring-violet-400/20"
placeholder="Enter profit (+) or loss (-)">
<p class="text-xs text-slate-400 mt-1">Positive for profit, negative for loss (e.g., 15.50 or -8.25)</p>
</div>
<!-- Budget Impact Preview -->
<div id="budget-impact" class="mb-4 p-4 bg-slate-900/50 rounded-xl border border-slate-700 hidden">
<p class="text-sm text-slate-400 mb-2">Budget Impact:</p>
<div class="flex items-center justify-between">
<span class="text-white">Current Budget:</span>
<span class="text-white font-semibold">$${state.user.budget || 0}</span>
</div>
<div class="flex items-center justify-between mt-1">
<span id="impact-label" class="text-white">New Budget:</span>
<span id="impact-value" class="font-bold text-lg"></span>
</div>
</div>
<!-- Close Trade Button -->
<button onclick="closeTrade(${trade.id})"
class="w-full gradient-bg text-white py-4 rounded-xl font-bold text-lg shadow-lg active:scale-95 transition">
Close Trade & Update Budget
</button>
`;
createModal(modalContent, 'close-trade-modal');
}
// Update P&L suggestion based on win/loss selection
function updatePnLSuggestion(status, suggestedAmount) {
const pnlInput = document.querySelector('[id^="actual-pnl-"]');
if (pnlInput && !pnlInput.value) {
pnlInput.value = suggestedAmount.toFixed(2);
updateBudgetImpact();
}
}
// Update budget impact preview
function updateBudgetImpact() {
const pnlInput = document.querySelector('[id^="actual-pnl-"]');
const budgetImpact = document.getElementById('budget-impact');
const impactValue = document.getElementById('impact-value');
if (pnlInput && pnlInput.value) {
const pnl = parseFloat(pnlInput.value);
const currentBudget = parseFloat(state.user.budget) || 0;
const newBudget = currentBudget + pnl;
budgetImpact.classList.remove('hidden');
impactValue.textContent = formatCurrency(newBudget);
impactValue.className = pnl >= 0 ? 'font-bold text-lg text-green-400' : 'font-bold text-lg text-red-400';
}
}
// Close trade with P&L
async function closeTrade(tradeId) {
const statusWin = document.getElementById('status-win');
const statusLoss = document.getElementById('status-loss');
const exitPriceInput = document.getElementById(`actual-exit-price-${tradeId}`);
const pnlInput = document.getElementById(`actual-pnl-${tradeId}`);
// Validate
if (!statusWin.checked && !statusLoss.checked) {
showToast('Please select WIN or LOSS', 'error');
return;
}
if (!exitPriceInput.value) {
showToast('Please enter exit price', 'error');
return;
}
if (!pnlInput.value) {
showToast('Please enter actual P&L', 'error');
return;
}
const status = statusWin.checked ? 'win' : 'loss';
const actualExitPrice = parseFloat(exitPriceInput.value);
const actualPnl = parseFloat(pnlInput.value);
// Validate P&L matches status
if (status === 'win' && actualPnl < 0) {
const confirm = window.confirm('You selected WIN but P&L is negative. Continue anyway?');
if (!confirm) return;
}
if (status === 'loss' && actualPnl > 0) {
const confirm = window.confirm('You selected LOSS but P&L is positive. Continue anyway?');
if (!confirm) return;
}
// Close trade
const result = await apiPost('closeTradeWithPnL', {
id: tradeId,
status: status,
actualExitPrice: actualExitPrice,
actualPnl: actualPnl
});
if (result.success) {
showToast(`Trade closed! Budget updated to ${formatCurrency(result.newBudget)}`, 'success');
closeAllModals();
// Reload data
await loadTrades();
await loadBudget();
render();
} else {
showToast('Failed to close trade: ' + (result.error || 'Unknown error'), 'error');
}
}
// Exit Screenshot Handling Functions
async function handleExitScreenshot(tradeId, input) {
if (input.files && input.files[0]) {
const file = input.files[0];
// Check file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('Image too large! Max 5MB', 'error');
return;
}
// Show preview immediately using FileReader
const reader = new FileReader();
reader.onload = function(e) {
const preview = document.getElementById(`exit-screenshot-preview-${tradeId}`);
const img = document.getElementById(`exit-screenshot-img-${tradeId}`);
if (preview && img) {
img.src = e.target.result;
preview.classList.remove('hidden');
}
};
reader.readAsDataURL(file);
// Upload file to server
const formData = new FormData();
formData.append('screenshot', file);
try {
showToast('Uploading screenshot...', 'info');
const response = await fetch('upload_screenshot.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
// Store file path instead of base64
exitScreenshotState[tradeId] = result.filepath;
// Update button text
const button = document.getElementById(`exit-screenshot-button-${tradeId}`);
if (button) {
button.textContent = '✓ Screenshot Uploaded';
}
showToast('Exit screenshot uploaded!', 'success');
} else {
showToast('Upload failed: ' + result.error, 'error');
clearExitScreenshot(tradeId);
}
} catch (error) {
showToast('Upload error: ' + error.message, 'error');
console.error('Screenshot upload error:', error);
clearExitScreenshot(tradeId);
}
}
}
function clearExitScreenshot(tradeId) {
delete exitScreenshotState[tradeId];
// Hide preview
const preview = document.getElementById(`exit-screenshot-preview-${tradeId}`);
if (preview) {
preview.classList.add('hidden');
}
// Clear file input
const input = document.getElementById(`exit-screenshot-input-${tradeId}`);
if (input) {
input.value = '';
}
// Reset button text
const button = document.getElementById(`exit-screenshot-button-${tradeId}`);
if (button) {
button.textContent = '📷 Upload Exit Screenshot';
}
showToast('Screenshot removed', 'success');
}
// ============================================
// TRADE DETAIL MODAL (TraderVue-style)
// ============================================
function openTradeDetailModal(trade) {
if (!trade) return;
// Ensure trade is in registry so action buttons inside modal can use it
window._tradeRegistry = window._tradeRegistry || {};
window._tradeRegistry[trade.id] = trade;
const statusColors = {
'win': { text: 'text-green-400', bg: 'bg-green-500/10 border-green-500/20' },
'loss': { text: 'text-red-400', bg: 'bg-red-500/10 border-red-500/20' },
'pending': { text: 'text-yellow-400',bg: 'bg-yellow-500/10 border-yellow-500/20' }
};
const sc = statusColors[trade.status] || statusColors['pending'];
const pnl = trade.status === 'win' ? parseFloat(trade.profit_amount || 0)
: trade.status === 'loss' ? -parseFloat(trade.loss_amount || 0)
: null;
const pnlStr = pnl != null
? `${pnl >= 0 ? '+' : ''}$${Math.abs(pnl).toFixed(2)}`
: '—';
// Use trade_date (actual trade date) when available, else fall back to created_at
const _detailDate = trade.trade_date || trade.created_at || '';
const dateStr = _detailDate
? (() => {
const parts = _detailDate.substring(0, 10).split('-');
if (parts.length === 3) {
const d = new Date(+parts[0], +parts[1] - 1, +parts[2]);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
return _detailDate.substring(0, 10);
})()
: '—';
const modalContent = `
<div class="space-y-5">
<!-- ── Header ── -->
<div class="flex items-start justify-between gap-4">
<div>
<div class="flex items-center gap-3 flex-wrap">
<h2 class="text-3xl font-black text-white tracking-tight">${trade.coin || '—'}</h2>
<span class="text-2xl font-bold ${sc.text}">${pnlStr}</span>
<span class="px-2.5 py-1 rounded-full text-xs font-bold uppercase ${sc.bg} ${sc.text} border">
${trade.status || 'pending'}
</span>
</div>
<p class="text-slate-400 text-sm mt-1">${dateStr}
${trade.order_type ? `<span class="ml-2 px-2 py-0.5 rounded bg-blue-500/15 text-blue-400 text-xs uppercase">${trade.order_type}</span>` : ''}
${trade.trade_category ? `<span class="ml-1 category-badge category-${trade.trade_category} text-xs">${getCategoryLabel(trade.trade_category)}</span>` : ''}
${trade.emotion ? `<span class="ml-1 emotion-badge emotion-${trade.emotion} text-xs">${getEmotionLabel(trade.emotion)}</span>` : ''}
</p>
</div>
<button onclick="closeAllModals()" class="text-slate-500 hover:text-white p-2 flex-shrink-0 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- ── Key Stats Grid ── -->
${(() => {
const exitP = parseFloat(trade.actual_exit_price || 0);
const stats = [
['Position', `$${parseFloat(trade.position_size || 0).toLocaleString()}`, ''],
['Entry Price', `$${parseFloat(trade.entry_price || 0).toLocaleString()}`, ''],
['Exit Price', exitP > 0 ? `$${exitP.toLocaleString()}` : '—', exitP > 0 ? (exitP >= parseFloat(trade.entry_price || 0) ? 'text-green-400' : 'text-red-400') : 'text-slate-500'],
['Net P&L', pnlStr, pnl != null ? (pnl >= 0 ? 'text-green-400' : 'text-red-400') : ''],
['Fees', `$${parseFloat(trade.fees || 0).toFixed(2)}`, 'text-slate-400'],
['R:R', `1:${trade.reward_risk_ratio || '—'}`, 'text-violet-300'],
];
return `<div class="grid grid-cols-3 md:grid-cols-6 gap-2">
${stats.map(([label, val, cls]) => `
<div class="bg-slate-900/60 rounded-xl p-3 border border-slate-800/80 text-center">
<p class="text-xs text-slate-500 mb-1">${label}</p>
<p class="font-bold text-sm ${cls || 'text-white'}">${val}</p>
</div>
`).join('')}
</div>`;
})()}
${(trade.sl_price > 0 || trade.tp_price > 0) ? `
<div class="grid grid-cols-2 gap-2">
${trade.sl_price > 0 ? `
<div class="bg-red-500/10 rounded-xl p-3 border border-red-500/20 flex items-center justify-between">
<span class="text-xs text-red-300 font-semibold">Stop Loss</span>
<span class="text-red-300 font-bold">$${trade.sl_price}</span>
</div>
` : ''}
${trade.tp_price > 0 ? `
<div class="bg-green-500/10 rounded-xl p-3 border border-green-500/20 flex items-center justify-between">
<span class="text-xs text-green-300 font-semibold">Take Profit</span>
<span class="text-green-300 font-bold">$${trade.tp_price}</span>
</div>
` : ''}
</div>
` : ''}
<!-- ── Photos Gallery ── -->
${(() => {
const extras = Array.isArray(trade.screenshots) ? trade.screenshots : [];
const allPhotos = [
trade.entry_screenshot ? { url: trade.entry_screenshot, label: 'Entry Chart', dot: 'bg-green-400' } : null,
trade.exit_screenshot ? { url: trade.exit_screenshot, label: 'Exit Chart', dot: 'bg-red-400' } : null,
...extras.map((url, i) => ({ url, label: `Photo ${i + 1}`, dot: 'bg-violet-400', removable: true }))
].filter(Boolean);
return `
<div>
<div class="flex items-center justify-between mb-2">
<p class="text-xs text-slate-500 uppercase tracking-widest font-semibold">Charts & Photos</p>
<label class="cursor-pointer flex items-center gap-1.5 text-xs text-violet-400 hover:text-violet-300 transition px-2 py-1 rounded-lg hover:bg-violet-500/10">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
Add Photo
<input type="file" accept="image/*" class="hidden"
onchange="uploadTradePhoto(${trade.id}, this)">
</label>
</div>
${allPhotos.length > 0 ? `
<div class="grid grid-cols-2 gap-3">
${allPhotos.map(p => `
<div class="relative rounded-xl overflow-hidden border border-violet-500/30 cursor-zoom-in group"
onclick='viewScreenshot(${JSON.stringify(p.url)})'>
<img src="${p.url}"
class="w-full object-cover group-hover:opacity-90 transition"
style="max-height:180px;object-position:top"
alt="${p.label}">
<div class="bg-slate-900/80 px-3 py-1.5 flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full ${p.dot} flex-shrink-0"></span>
<span class="text-xs text-slate-400">${p.label}</span>
${p.removable ? `
<button onclick="event.stopPropagation(); removeTradePhoto(${trade.id}, ${JSON.stringify(p.url)})"
class="ml-auto text-xs text-red-400 hover:text-red-300 px-1 py-0.5 rounded transition">✕</button>
` : '<span class="ml-auto text-xs text-slate-600">tap to expand</span>'}
</div>
</div>
`).join('')}
<!-- Add more photo tile -->
<label class="cursor-pointer rounded-xl border-2 border-dashed border-violet-500/30 hover:border-violet-500/60 flex flex-col items-center justify-center gap-2 transition min-h-[120px] group"
style="max-height:180px">
<svg class="w-8 h-8 text-violet-500/50 group-hover:text-violet-400 transition" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span class="text-xs text-slate-500 group-hover:text-slate-400 transition">Add Photo</span>
<input type="file" accept="image/*" class="hidden"
onchange="uploadTradePhoto(${trade.id}, this)">
</label>
</div>
` : `
<label class="cursor-pointer w-full rounded-xl border-2 border-dashed border-violet-500/30 hover:border-violet-500/60 flex flex-col items-center justify-center gap-2 transition py-8 group">
<svg class="w-10 h-10 text-violet-500/50 group-hover:text-violet-400 transition" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<span class="text-sm text-slate-500 group-hover:text-slate-400 transition">Upload entry/exit charts or notes</span>
<input type="file" accept="image/*" class="hidden"
onchange="uploadTradePhoto(${trade.id}, this)">
</label>
`}
</div>
`;
})()}
<!-- ── Journal Sections ── -->
${trade.pre_trade_plan ? `
<div class="bg-blue-500/10 rounded-xl p-4 border border-blue-500/20">
<p class="text-xs text-blue-300 font-semibold uppercase tracking-wider mb-2">📝 Pre-Trade Plan</p>
<p class="text-sm text-slate-200 leading-relaxed">${trade.pre_trade_plan}</p>
</div>
` : ''}
${trade.comment ? `
<div class="bg-slate-900/50 rounded-xl p-4 border border-slate-700/60">
<p class="text-xs text-slate-400 font-semibold uppercase tracking-wider mb-2">💬 Notes</p>
<p class="text-sm text-slate-200 leading-relaxed">${trade.comment}</p>
</div>
` : ''}
${trade.post_trade_review ? `
<div class="bg-violet-500/10 rounded-xl p-4 border border-violet-500/20">
<p class="text-xs text-violet-300 font-semibold uppercase tracking-wider mb-2">📊 Post-Trade Review</p>
<p class="text-sm text-slate-200 leading-relaxed">${trade.post_trade_review}</p>
${trade.discipline_score ? `
<div class="mt-3 flex items-center gap-2">
<span class="text-xs text-violet-400">Discipline Score:</span>
<div class="flex gap-0.5">
${Array.from({length: 10}, (_, i) => `<div class="w-3 h-3 rounded-sm ${i < trade.discipline_score ? 'bg-violet-400' : 'bg-slate-700'}"></div>`).join('')}
</div>
<span class="text-xs text-violet-300 font-bold">${trade.discipline_score}/10</span>
</div>
` : ''}
</div>
` : ''}
${trade.mistake_type ? `
<div class="bg-red-500/10 rounded-xl p-4 border border-red-500/20">
<p class="text-xs text-red-300 font-semibold uppercase tracking-wider mb-2">⚠️ Mistake</p>
<p class="text-sm text-slate-200">${trade.mistake_type}</p>
${trade.lesson_learned ? `
<div class="mt-3 pt-3 border-t border-red-500/20">
<p class="text-xs text-yellow-300 font-semibold mb-1">💡 Lesson Learned</p>
<p class="text-sm text-slate-200">${trade.lesson_learned}</p>
</div>
` : ''}
</div>
` : ''}
<!-- ── Action Buttons ── -->
<div class="flex gap-2 pt-1 border-t border-violet-500/15">
${(trade.status === 'open' || !trade.status || trade.status === 'pending') && trade.entry_price > 0 ? `
<button onclick="closeAllModals(); setTimeout(() => openClosingTradeModal(window._tradeRegistry[${trade.id}]), 50)"
class="flex-1 bg-green-600 hover:bg-green-700 text-white py-2.5 rounded-xl text-sm font-semibold active:scale-95 transition">
Close Trade
</button>
` : ''}
<button onclick="closeAllModals(); setTimeout(() => openTradeModal(window._tradeRegistry[${trade.id}]), 50)"
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white py-2.5 rounded-xl text-sm font-semibold active:scale-95 transition">
✏️ Edit
</button>
<button onclick="closeAllModals(); confirmDeleteTrade(${trade.id})"
class="bg-slate-800 hover:bg-red-900/50 border border-slate-700 hover:border-red-500/40 text-slate-400 hover:text-red-400 px-4 py-2.5 rounded-xl text-sm font-semibold active:scale-95 transition"
title="Delete Trade">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
</div>
`;
createModal(modalContent, 'trade-detail-modal');
}
// ============================================
// SELECTION & BULK DELETE
// ============================================
function toggleTradeSelection(id) {
if (historySelection.has(id)) {
historySelection.delete(id);
} else {
historySelection.add(id);
}
updateBulkDeleteBar();
}
function selectAllTrades(checked) {
// checked: true → select all, false → deselect all
// Uses module-level _historyTrades to avoid JSON embedding in HTML attributes
const ids = _historyTrades.map(t => t.id);
if (checked) {
ids.forEach(id => historySelection.add(id));
document.querySelectorAll('.trade-checkbox').forEach(cb => cb.checked = true);
} else {
ids.forEach(id => historySelection.delete(id));
document.querySelectorAll('.trade-checkbox').forEach(cb => cb.checked = false);
}
updateBulkDeleteBar();
}
function clearTradeSelection() {
historySelection.clear();
document.querySelectorAll('.trade-checkbox').forEach(cb => cb.checked = false);
const selectAllCb = document.getElementById('select-all-cb');
if (selectAllCb) selectAllCb.checked = false;
updateBulkDeleteBar();
}
function updateBulkDeleteBar() {
const bar = document.getElementById('bulk-delete-bar');
const count = document.getElementById('bulk-delete-count');
if (!bar) return;
if (historySelection.size > 0) {
bar.classList.remove('hidden');
bar.classList.add('flex');
if (count) count.textContent = `${historySelection.size} selected`;
} else {
bar.classList.add('hidden');
bar.classList.remove('flex');
}
// Sync select-all checkbox visual state
const allCbs = document.querySelectorAll('.trade-checkbox');
const allChecked = allCbs.length > 0 && Array.from(allCbs).every(cb => cb.checked);
const selectAllCb = document.getElementById('select-all-cb');
if (selectAllCb) {
selectAllCb.checked = allChecked;
selectAllCb.indeterminate = !allChecked && historySelection.size > 0;
}
}
async function deleteSelectedTrades() {
const ids = Array.from(historySelection);
if (ids.length === 0) return;
if (!confirm(`Delete ${ids.length} trade(s)? This cannot be undone.`)) return;
showToast(`Deleting ${ids.length} trade(s)...`, 'info');
let deleted = 0;
for (const id of ids) {
const result = await apiPost('deleteTrade', { id });
if (result.success) deleted++;
}
historySelection.clear();
showToast(`Deleted ${deleted} trade(s)`, 'success');
await loadTrades();
await loadStats();
render();
}
// Make functions global
window.renderHistory = renderHistory;
window.renderTradesTable = renderTradesTable;
window.renderTradeRow = renderTradeRow;
window.openTradeDetailModal = openTradeDetailModal;
window.openTradeModal = openTradeModal;
window.updateTradePrices = updateTradePrices;
window.updateStatus = updateStatus;
window.saveTradeReview = saveTradeReview;
window.confirmDeleteTrade = confirmDeleteTrade;
window.deleteTrade = deleteTrade;
window.exportTrades = exportTrades;
window.showFilterMenu = showFilterMenu;
window.viewScreenshot = viewScreenshot;
window.uploadTradePhoto = uploadTradePhoto;
window.removeTradePhoto = removeTradePhoto;
window.handleExitScreenshot = handleExitScreenshot;
window.clearExitScreenshot = clearExitScreenshot;
window.openClosingTradeModal = openClosingTradeModal;
window.updatePnLSuggestion = updatePnLSuggestion;
window.updateBudgetImpact = updateBudgetImpact;
window.closeTrade = closeTrade;
window.toggleTradeSelection = toggleTradeSelection;
window.selectAllTrades = selectAllTrades;
window.clearTradeSelection = clearTradeSelection;
window.updateBulkDeleteBar = updateBulkDeleteBar;
window.deleteSelectedTrades = deleteSelectedTrades;
// Add event listener for P&L input to update budget preview
document.addEventListener('input', function(e) {
if (e.target.id && e.target.id.startsWith('actual-pnl-')) {
updateBudgetImpact();
}
});