let portfolioData = []; let coinColors = {}; let addTransactionModal, editTransactionModal, colorConfigModal, noteModal; let buyForm, sellForm; let portfolioList, pieChartCanvas, chartLegend; let editIdGlobal = null; let currentNoteId = null; let currentNoteType = null; function formatPrice(value) { if (isNaN(value)) return "-"; if (value < 0.01) return value.toFixed(6); if (value < 1) return value.toFixed(4); return value.toFixed(2); } document.addEventListener("DOMContentLoaded", () => { addTransactionModal = document.getElementById("addTransactionModal"); editTransactionModal = document.getElementById("editTransactionModal"); colorConfigModal = document.getElementById("colorConfigModal"); noteModal = document.getElementById("noteModal"); buyForm = document.getElementById("buyForm"); sellForm = document.getElementById("sellForm"); portfolioList = document.getElementById("portfolioList"); pieChartCanvas = document.getElementById("portfolioPieChart"); chartLegend = document.getElementById("chartLegend"); window.addEventListener("click", (event) => { if (event.target === addTransactionModal) closeAddTransactionModal(); if (event.target === editTransactionModal) closeEditTransactionModal(); if (event.target === colorConfigModal) closeColorConfigModal(); if (event.target === noteModal) closeNoteModal(); }); loadCoinColorsFromStorage(); loadPortfolioData(); setInterval(() => { updatePrices(); }, 10000); }); function loadPortfolioData() { fetch("/api/portfolio", { credentials: "include" }) .then(r => r.json()) .then(d => { portfolioData = d.transactions || []; updatePrices(); }); } function updatePrices() { fetch("/api/portfolio/prices", { credentials: "include" }) .then(r => r.json()) .then(arr => { portfolioData = arr.map(x => { x.id = String(x.id); return x; }); renderPortfolio(); drawPieChart(); }); } function renderPortfolio() { portfolioList.innerHTML = ""; let grouped = {}; portfolioData.forEach(t => { if (!grouped[t.symbol]) grouped[t.symbol] = []; grouped[t.symbol].push(t); }); for (let sym in grouped) { const section = document.createElement("div"); section.className = "coin-section"; const heading = document.createElement("h2"); heading.textContent = sym; section.appendChild(heading); const table = document.createElement("table"); const thead = document.createElement("thead"); thead.innerHTML = ` Symbol Amount Buy Price Current Invest Profit / Loss % Change Buy Date `; table.appendChild(thead); const tbody = document.createElement("tbody"); let totalAmount = 0; let totalCost = 0; grouped[sym].forEach(tx => { const currentTotal = tx.amount * tx.currentPrice; const initialTotal = tx.amount * tx.buyPrice; const diff = currentTotal - initialTotal; const pct = initialTotal === 0 ? 0 : (diff / initialTotal) * 100; totalAmount += tx.amount; totalCost += initialTotal; const row = document.createElement("tr"); const diffClass = diff >= 0 ? "positive" : "negative"; const editButton = ``; const noteButton = ``; row.innerHTML = ` ${tx.symbol} ${tx.amount} ${formatPrice(tx.buyPrice)} ${formatPrice(tx.currentPrice)} ${(tx.amount * tx.buyPrice).toFixed(2)} ${diff.toFixed(2)} ${pct.toFixed(2)}% ${tx.date}${editButton}${noteButton} `; tbody.appendChild(row); }); const totalCurrent = totalAmount * (grouped[sym][0].currentPrice || 0); const totalDiff = totalCurrent - totalCost; const totalPct = totalCost === 0 ? 0 : (totalDiff / totalCost) * 100; const avgBuyPrice = totalAmount === 0 ? 0 : totalCost / totalAmount; const totalRow = document.createElement("tr"); totalRow.className = "summary-row"; const summaryClass = totalDiff >= 0 ? "positive" : "negative"; totalRow.innerHTML = ` ${sym} (Total) ${totalAmount.toFixed(6)} Ø ${formatPrice(avgBuyPrice)} ${formatPrice(grouped[sym][0].currentPrice || 0)} ${totalCost.toFixed(2)} ${totalDiff.toFixed(2)} ${totalPct.toFixed(2)}% - `; tbody.appendChild(totalRow); table.appendChild(tbody); section.appendChild(table); portfolioList.appendChild(section); } calculateTotals(); } function calculateTotals() { let grandCost = 0; let grandValue = 0; portfolioData.forEach(t => { grandCost += t.buyPrice * t.amount; grandValue += t.currentPrice * t.amount; }); const diff = grandValue - grandCost; const pct = grandCost === 0 ? 0 : (diff / grandCost) * 100; document.getElementById("totalInvest").textContent = grandCost.toFixed(2) + " USDT"; document.getElementById("totalProfitLoss").textContent = diff.toFixed(2) + " USDT"; document.getElementById("totalPercentChange").textContent = pct.toFixed(2) + "%"; } function drawPieChart() { if (!pieChartCanvas) return; let grouped = {}; portfolioData.forEach(t => { if (!grouped[t.symbol]) grouped[t.symbol] = 0; grouped[t.symbol] += t.currentPrice * t.amount; }); let totalAll = 0; let coinValues = []; for (let s in grouped) { totalAll += grouped[s]; } for (let s in grouped) { coinValues.push({ symbol: s, value: grouped[s] }); } const ctx = pieChartCanvas.getContext("2d"); ctx.clearRect(0, 0, pieChartCanvas.width, pieChartCanvas.height); const centerX = pieChartCanvas.width / 2; const centerY = pieChartCanvas.height / 2; const radius = Math.min(centerX, centerY) - 10; let startAngle = 0; coinValues.forEach(item => { const sliceAngle = totalAll === 0 ? 0 : (item.value / totalAll) * 2 * Math.PI; if (!coinColors[item.symbol]) { coinColors[item.symbol] = randomColor(); } ctx.fillStyle = coinColors[item.symbol]; ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle); ctx.closePath(); ctx.fill(); startAngle += sliceAngle; }); renderChartLegend(coinValues, totalAll); } function renderChartLegend(coinValues, totalAll) { chartLegend.innerHTML = ""; coinValues.forEach(item => { const percent = totalAll === 0 ? 0 : (item.value / totalAll) * 100; const div = document.createElement("div"); const colorBox = document.createElement("span"); colorBox.className = "legend-color-box"; colorBox.style.backgroundColor = coinColors[item.symbol]; div.appendChild(colorBox); div.appendChild(document.createTextNode(item.symbol + " - " + percent.toFixed(2) + "%")); chartLegend.appendChild(div); }); } function openNoteModal(id, type) { currentNoteId = id; currentNoteType = type; noteModal.style.display = "block"; if (type === 'portfolio') { const tx = portfolioData.find(x => String(x.id) === String(id)); document.getElementById("noteText").value = tx?.note || ""; } } function closeNoteModal() { noteModal.style.display = "none"; } function saveNote() { const noteText = document.getElementById("noteText").value; const endpoint = currentNoteType === 'portfolio' ? "/api/portfolio/note" : "/api/trade_summary/note"; fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: currentNoteId, note: noteText }), credentials: "include" }) .then(r => r.json()) .then(j => { if (j.error) { alert(j.error); } else { closeNoteModal(); updatePrices(); } }) .catch(() => alert("Network error")); } function openAddTransactionModal() { addTransactionModal.style.display = "block"; showBuyForm(); } function closeAddTransactionModal() { addTransactionModal.style.display = "none"; } function showBuyForm() { buyForm.style.display = "block"; sellForm.style.display = "none"; } function showSellForm() { buyForm.style.display = "none"; sellForm.style.display = "block"; populateSellDropdown(); } function confirmBuy() { const symbol = document.getElementById("buySymbol").value.trim().toUpperCase(); const amount = parseFloat(document.getElementById("buyAmount").value); const buyPrice = parseFloat(document.getElementById("buyPrice").value); const buyDate = document.getElementById("buyDate").value; if (!symbol || isNaN(amount) || isNaN(buyPrice)) { alert("Please enter valid values"); return; } const newId = Date.now() + Math.floor(Math.random() * 999999); fetch("/api/coinPrice?symbol=" + symbol, { credentials: "include" }) .then(r => r.json()) .then(j => { if (j.error) { alert("This coin is not supported"); } else { fetch("/api/portfolio/buy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: newId, symbol, amount, buyPrice, date: buyDate }), credentials: "include" }) .then(rr => rr.json()) .then(result => { if (result.error) { alert(result.error); } else { updatePrices(); closeAddTransactionModal(); } }); } }) .catch(() => alert("Network error")); } function confirmSell() { const sel = document.getElementById("sellSelectTransaction"); const val = sel.value; if (!val) return; const sellAmount = parseFloat(document.getElementById("sellAmount").value); const sellPrice = parseFloat(document.getElementById("sellPrice").value); const sellDate = document.getElementById("sellDate").value; if (isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) { alert("Please enter valid values"); return; } const parts = val.split("|"); const sym = parts[0]; const idx = parseInt(parts[1], 10); let selectedTransaction = null; let count = 0; for (let i = 0; i < portfolioData.length; i++) { if (portfolioData[i].symbol === sym) { if (count === idx) { selectedTransaction = portfolioData[i]; break; } count++; } } if (!selectedTransaction) { alert("Invalid"); return; } fetch("/api/portfolio/sell", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: selectedTransaction.id, sellAmount: sellAmount, sellPrice: sellPrice, sellDate: sellDate }), credentials: "include" }) .then(r => r.json()) .then(j => { if (j.error) { alert(j.error); } else { updatePrices(); closeAddTransactionModal(); } }) .catch(() => alert("Error during sell")); } function populateSellDropdown() { const sel = document.getElementById("sellSelectTransaction"); sel.innerHTML = ""; let grouped = {}; portfolioData.forEach(t => { if (!grouped[t.symbol]) grouped[t.symbol] = []; grouped[t.symbol].push(t); }); for (let sym in grouped) { grouped[sym].forEach((tx, i) => { const opt = document.createElement("option"); opt.value = sym + "|" + i; opt.text = sym + " - " + tx.amount + "@" + tx.buyPrice + " (" + tx.date + ")"; sel.appendChild(opt); }); } } function openEditTransactionModal(id) { editTransactionModal.style.display = "block"; editIdGlobal = id; let t = portfolioData.find(x => String(x.id) === String(id)); if (!t) { alert("No valid transaction found"); return; } document.getElementById("editSymbol").value = t.symbol; document.getElementById("editAmount").value = t.amount; document.getElementById("editBuyPrice").value = t.buyPrice; document.getElementById("editDate").value = t.date; } function closeEditTransactionModal() { editTransactionModal.style.display = "none"; } function saveEditedTransaction() { let t = portfolioData.find(x => String(x.id) === String(editIdGlobal)); if (!t) { alert("No valid transaction found"); return; } const newAmount = parseFloat(document.getElementById("editAmount").value); const newBuyPrice = parseFloat(document.getElementById("editBuyPrice").value); const newDate = document.getElementById("editDate").value; if (isNaN(newAmount) || isNaN(newBuyPrice)) { alert("Please enter valid values"); return; } fetch("/api/portfolio/edit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: t.id, symbol: t.symbol, newAmount, newBuyPrice, newDate }), credentials: "include" }) .then(r => r.json()) .then(j => { if (j.error) { alert(j.error); } else { closeEditTransactionModal(); updatePrices(); } }) .catch(() => alert("Error during edit")); } function deleteTransaction() { openDeleteConfirmationModal(); } function openDeleteConfirmationModal() { document.getElementById("deleteConfirmationModal").style.display = "block"; } function closeDeleteConfirmationModal() { document.getElementById("deleteConfirmationModal").style.display = "none"; } function confirmDeleteTransaction() { let t = portfolioData.find(x => String(x.id) === String(editIdGlobal)); if (!t) { alert("No valid transaction found"); closeDeleteConfirmationModal(); return; } fetch("/api/portfolio/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: t.id, symbol: t.symbol }), credentials: "include" }) .then(r => r.json()) .then(j => { if (j.error) { alert(j.error); } else { closeDeleteConfirmationModal(); closeEditTransactionModal(); updatePrices(); } }) .catch(() => alert("Error deleting transaction")); } function openColorConfigModal() { colorConfigModal.style.display = "block"; renderColorConfigList(); } function closeColorConfigModal() { colorConfigModal.style.display = "none"; } function renderColorConfigList() { const container = document.getElementById("colorConfigList"); container.innerHTML = ""; let syms = {}; portfolioData.forEach(t => { syms[t.symbol] = true; }); Object.keys(syms).forEach(s => { const div = document.createElement("div"); div.style.marginBottom = "0.5rem"; const label = document.createElement("label"); label.textContent = s + ": "; div.appendChild(label); const input = document.createElement("input"); input.type = "color"; if (!coinColors[s]) { coinColors[s] = randomColor(); } input.value = rgbToHex(coinColors[s]); input.addEventListener("input", () => { coinColors[s] = input.value; }); div.appendChild(input); container.appendChild(div); }); } function saveColorConfig() { closeColorConfigModal(); saveCoinColorsToStorage(); drawPieChart(); } function randomColor() { const r = Math.floor(Math.random() * 200 + 55); const g = Math.floor(Math.random() * 200 + 55); const b = Math.floor(Math.random() * 200 + 55); return `rgb(${r},${g},${b})`; } function rgbToHex(str) { if (str.indexOf("#") === 0) return str; const p = str.replace(/[^\d,]/g, "").split(","); const r = parseInt(p[0], 10); const g = parseInt(p[1], 10); const b = parseInt(p[2], 10); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } function loadCoinColorsFromStorage() { const s = localStorage.getItem("coinColors"); if (s) coinColors = JSON.parse(s); } function saveCoinColorsToStorage() { localStorage.setItem("coinColors", JSON.stringify(coinColors)); }