"use strict"; (() => { // Helper functions const $ = id => { const element = document.getElementById(id); if (!element && console && console.warn) { console.warn(`Element with ID '${id}' not found`); } return element; }; const adjustValue = (id, delta) => { const input = $(id); if (!input) return; const currentValue = parseFloat(input.value) || 0; const step = parseFloat(input.getAttribute('data-step')) || 1; input.value = currentValue + delta * step; calculate(); }; // Register Chart.js plugins Chart.register(ChartDataLabels); let chart; let investedChart; let incomeChart; // Global variable for income chart let coinTrackerCounter = 0; let investedHistory = { labels: [], data: [] }; let dailyYieldChart; // global reference // Update coin trackers on the backend with the given data const updateCoinTrackersToBackend = (trackers) => { console.log("Saving trackers to backend:", trackers); fetch('/api/coinTrackers', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ data: trackers }) }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { console.log("Trackers saved successfully:", data); }) .catch(error => { console.error("Error saving trackers:", error); }); }; // Main calculation for the compounding interest chart, left panel summary, and analysis table const calculate = () => { const initialPrincipal = parseFloat($('initialPrincipal').value) || 0; const dailyRate = parseFloat($('dailyRate').value) || 0; const termDays = parseInt($('termDays').value, 10) || 0; const reinvestRate = parseFloat($('reinvestRate').value) || 0; const labels = []; const reinvestArray = []; const cashArray = []; const earningsDaily = []; const tableRows = []; let runningPrincipal = initialPrincipal; let cumulativeCash = 0; let currentDate = new Date(); currentDate.setDate(currentDate.getDate() + 1); for (let i = 0; i < termDays; i++) { const dateStr = `${currentDate.getMonth()+1}/${currentDate.getDate()}/${currentDate.getFullYear()}`; labels.push(`Day ${i + 1}`); const earnings = runningPrincipal * (dailyRate / 100); const reinvestment = earnings * (reinvestRate / 100); const cashFlow = earnings - reinvestment; reinvestArray.push(Number(reinvestment.toFixed(2))); cashArray.push(Number(cashFlow.toFixed(2))); earningsDaily.push(Number(earnings.toFixed(2))); runningPrincipal += reinvestment; cumulativeCash += cashFlow; tableRows.push({ day: i + 1, date: dateStr, earnings: earnings, reinvestment: reinvestment, cashFlow: cashFlow, totalPrincipal: runningPrincipal, totalCash: cumulativeCash, reinvestRate: reinvestRate }); currentDate.setDate(currentDate.getDate() + 1); } // Update left panel summary $('sumInvestment').innerText = `Initial Investment: $${initialPrincipal.toFixed(2)}`; $('sumRate').innerText = `Interest Rate: ${dailyRate.toFixed(2)}%`; $('sumDays').innerText = `Days: ${termDays}`; $('sumInterest').innerText = `Interest Earned: $${((runningPrincipal - initialPrincipal) + cumulativeCash).toFixed(2)}`; $('sumNewBalance').innerText = `New Balance: $${runningPrincipal.toFixed(2)}`; $('sumCash').innerText = `Cash Withdrawals: $${cumulativeCash.toFixed(2)}`; // Draw compounding interest chart const ctx = $('interestChart').getContext('2d'); if (chart) { chart.destroy(); } const gradient = ctx.createLinearGradient(0, 0, 0, 300); gradient.addColorStop(0, 'rgba(0,123,255,0.8)'); gradient.addColorStop(1, 'rgba(0,123,255,0.2)'); chart = new Chart(ctx, { type: 'bar', data: { labels, datasets: [ { label: 'Re-investment', data: reinvestArray, backgroundColor: '#fd7e14', stack: 'combined', datalabels: { anchor: 'center', align: 'center', formatter: (value, context) => reinvestArray[context.dataIndex].toFixed(2), color: '#e0e0e0', font: { weight: 'bold', size: 14 } } }, { label: 'Cash Flow', data: cashArray, backgroundColor: '#28a745', stack: 'combined', datalabels: { labels: { cashFlowLabel: { anchor: 'center', align: 'center', formatter: (value, context) => cashArray[context.dataIndex].toFixed(2), color: '#e0e0e0', font: { weight: 'bold', size: 14 } }, totalEarningsLabel: { anchor: 'end', align: 'end', offset: 10, formatter: (value, context) => earningsDaily[context.dataIndex].toFixed(2), color: '#e0e0e0', font: { weight: 'bold', size: 14 } } } } } ] }, options: { responsive: true, animation: { duration: 0 }, scales: { x: { stacked: true, grid: { display: false }, ticks: { color: '#e0e0e0' } }, y: { stacked: true, grid: { color: '#555' }, ticks: { color: '#e0e0e0' } } }, plugins: { legend: { display: true, labels: { color: '#e0e0e0' } }, tooltip: { backgroundColor: '#333', titleColor: '#e0e0e0', bodyColor: '#e0e0e0', borderColor: '#007bff', borderWidth: 1, cornerRadius: 4, padding: 10 } } } }); // Populate the Analysis Table for Compounding Interest let tableHTML = ''; const fmt = num => '$' + num.toFixed(2); tableRows.forEach(row => { const principalCashCell = `${fmt(row.reinvestment)} / ${fmt(row.cashFlow)}`; tableHTML += ` ${row.day} ${row.date} ${fmt(row.earnings)} ${row.reinvestRate}% ${principalCashCell} ${fmt(row.totalPrincipal)} ${fmt(row.totalCash)} `; }); $('analysisTable').querySelector('tbody').innerHTML = tableHTML; updateTotalInvested(); }; // Percentage Calculators const calculatePercentage = () => { const total = parseFloat($('totalAmountPercent').value) || 0; const given = parseFloat($('givenAmount').value) || 0; if (total > 0) { const perc = (given / total) * 100; $('percentageResult').innerText = `${given.toFixed(2)} is ${perc.toFixed(2)}% of ${total.toFixed(2)}`; } else { $('percentageResult').innerText = "Enter a valid total amount"; } }; const calculatePercentage2 = () => { const total2 = parseFloat($('totalAmountPercent2').value) || 0; const perc2 = parseFloat($('percentInput2').value) || 0; if (total2 > 0) { const dollarValue = (perc2 / 100) * total2; $('percentageResult2').innerText = `${perc2}% of $${total2.toFixed(2)} = $${dollarValue.toFixed(2)}`; } else { $('percentageResult2').innerText = "Enter a valid total amount"; } }; // Enhanced setupCollapsibles with proper support for independent sections: const setupCollapsibles = () => { document.querySelectorAll('.collapsible-header').forEach(header => { header.addEventListener("click", () => { const body = header.nextElementSibling; if (body) { // Toggle display manually body.style.display = (body.style.display === "none" || body.style.display === "") ? "block" : "none"; // Update aria-expanded attribute for accessibility const expanded = body.style.display === "block"; header.setAttribute('aria-expanded', expanded.toString()); } }); }); }; // Date/Time helpers const getToday = () => new Date().toISOString().split('T')[0]; const getBerlinTime = () => { const options = { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' }; return new Date().toLocaleTimeString('en-GB', options); }; const sortCoinTrackerRows = trackerDiv => { const table = trackerDiv.querySelector("table.coin-table"); const tbody = table.querySelector("tbody"); const rows = Array.from(tbody.rows); rows.sort((a, b) => { const dateA = new Date(a.cells[0].querySelector("input").value); const dateB = new Date(b.cells[0].querySelector("input").value); return dateA - dateB; }); rows.forEach(row => tbody.appendChild(row)); saveCoinTrackers(); updateCoinTrackerSummary(trackerDiv.id); }; // Create coin tracker element with a toggle for chart display. // Persist the hide/show state by using a "showInChart" property. const addCoinTracker = data => { let trackerId; if (data && data.id) { trackerId = data.id; const num = parseInt(trackerId.replace("coinTracker", "")); if (!isNaN(num) && num > coinTrackerCounter) { coinTrackerCounter = num; } } else { coinTrackerCounter++; trackerId = `coinTracker${coinTrackerCounter}`; } const trackerDiv = document.createElement("div"); trackerDiv.className = "coin-tracker"; trackerDiv.id = trackerId; // Set persistent show/hide state if provided, otherwise default to "true" if (data && data.showInChart !== undefined) { trackerDiv.dataset.showInChart = data.showInChart; } else { trackerDiv.dataset.showInChart = "true"; } // Set active state from loaded data or default to "true if (data && data.active !== undefined) { trackerDiv.dataset.active = data.active; } else { trackerDiv.dataset.active = "true"; } // Enable manual sorting by default trackerDiv.dataset.manualSort = "true"; const headerDiv = document.createElement("div"); headerDiv.classList.add("coin-tracker-header"); const coinNameInput = document.createElement("input"); coinNameInput.type = "text"; coinNameInput.value = data && data.coinName ? data.coinName : "Unnamed Coin"; coinNameInput.classList.add("coin-name-input"); coinNameInput.placeholder = "Coin/Coin pair"; coinNameInput.addEventListener("click", e => e.stopPropagation()); coinNameInput.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); updateTransferDropdown(); }); headerDiv.appendChild(coinNameInput); const platformInput = document.createElement("input"); platformInput.type = "text"; platformInput.setAttribute("list", "platformListDatalist"); // <-- add this platformInput.placeholder = "Platform"; platformInput.value = data && data.platform ? data.platform : ""; platformInput.addEventListener("change", () => { // Could save the platform if desired saveCoinTrackers(); updatePlatformDatalist(platformInput.value); }); headerDiv.appendChild(platformInput); const lastValueSpan = document.createElement("span"); lastValueSpan.className = "last-value"; lastValueSpan.textContent = "Current Value: $0.00"; // Add label "Current Value: " lastValueSpan.style.marginLeft = "10px"; // adjust spacing as needed headerDiv.appendChild(lastValueSpan); // Add new span for total income const totalIncomeSpan = document.createElement("span"); totalIncomeSpan.className = "total-income"; totalIncomeSpan.textContent = "Total Income: $0.00"; totalIncomeSpan.style.marginLeft = "10px"; headerDiv.appendChild(totalIncomeSpan); const pnlDiv = document.createElement("div"); pnlDiv.className = "pnl-div"; pnlDiv.style.marginLeft = "10px"; const pnlCurrentValueSpan = document.createElement("span"); pnlCurrentValueSpan.className = "pnl-current-value"; pnlCurrentValueSpan.textContent = "PnL (Current Value): $0.00"; pnlDiv.appendChild(pnlCurrentValueSpan); const pnlTotalValueSpan = document.createElement("span"); pnlTotalValueSpan.className = "pnl-total-value"; pnlTotalValueSpan.textContent = "PnL (Total Value): $0.00"; pnlDiv.appendChild(pnlTotalValueSpan); headerDiv.appendChild(pnlDiv); const interestSpan = document.createElement("span"); interestSpan.className = "tracker-interest"; headerDiv.appendChild(interestSpan); const deleteTrackerBtn = document.createElement("button"); deleteTrackerBtn.textContent = "Delete Tracker"; deleteTrackerBtn.className = "deleteTracker"; deleteTrackerBtn.addEventListener("click", e => { e.stopPropagation(); if (confirm("Are you sure you want to delete this coin tracker?")) { trackerDiv.remove(); saveCoinTrackers(); updateTransferDropdown(); } }); headerDiv.appendChild(deleteTrackerBtn); // Toggle button for chart display with persistent state const toggleChartBtn = document.createElement("button"); toggleChartBtn.style.marginLeft = "10px"; toggleChartBtn.textContent = trackerDiv.dataset.showInChart === "true" ? "Hide from Chart" : "Show in Chart"; toggleChartBtn.addEventListener("click", () => { if (trackerDiv.dataset.showInChart === "false") { trackerDiv.dataset.showInChart = "true"; toggleChartBtn.textContent = "Hide from Chart"; } else { trackerDiv.dataset.showInChart = "false"; toggleChartBtn.textContent = "Show in Chart"; } updateInvestedChart(); saveCoinTrackers(); // persist the new state }); headerDiv.appendChild(toggleChartBtn); const toggleActiveBtn = document.createElement("button"); toggleActiveBtn.style.marginLeft = "10px"; toggleActiveBtn.textContent = trackerDiv.dataset.active === "false" ? "Activate" : "Deactivate"; toggleActiveBtn.addEventListener("click", () => { if (trackerDiv.dataset.active === "false") { trackerDiv.dataset.active = "true"; toggleActiveBtn.textContent = "Deactivate"; } else { trackerDiv.dataset.active = "false"; toggleActiveBtn.textContent = "Activate"; } updateTotalInvested(); updateIncomeChart(); saveCoinTrackers(); }); headerDiv.appendChild(toggleActiveBtn); headerDiv.addEventListener("click", e => { if (!["INPUT", "BUTTON", "SPAN"].includes(e.target.tagName)) { const contentDiv = trackerDiv.querySelector(".content"); contentDiv.style.display = (contentDiv.style.display === "none") ? "block" : "none"; } }); trackerDiv.appendChild(headerDiv); const contentDiv = document.createElement("div"); contentDiv.className = "content"; contentDiv.style.display = "none"; // Create initialCapitalDiv const initialCapitalDiv = document.createElement("div"); initialCapitalDiv.style.marginBottom = "10px"; const initialCapitalLabel = document.createElement("label"); initialCapitalLabel.textContent = 'Initial Capital: '; const initialCapitalInput = document.createElement("input"); initialCapitalInput.type = "number"; initialCapitalInput.step = "0.01"; initialCapitalInput.value = data && data.initialCapital ? data.initialCapital : "0.00"; initialCapitalDiv.appendChild(initialCapitalLabel); initialCapitalDiv.appendChild(initialCapitalInput); contentDiv.appendChild(initialCapitalDiv); // Create a container with positioning context for the table const tableWrapper = document.createElement("div"); tableWrapper.className = "table-wrapper"; tableWrapper.style.position = "relative"; const table = document.createElement("table"); table.className = "income-table coin-table"; const thead = document.createElement("thead"); thead.innerHTML = ` Date Time Current Value Income Compound Daily Yield (%) Actions Notes `; table.appendChild(thead); const tbody = document.createElement("tbody"); table.appendChild(tbody); if (data && data.rows && data.rows.length > 0) { data.rows.forEach(row => { addDynamicEntry(tbody, row, trackerId); }); } // Add the table to the wrapper tableWrapper.appendChild(table); // No need for a separate yield projection box anymore // We're removing the summaryDiv creation and positioning code tableWrapper.style.marginBottom = "20px"; // Add the wrapper to the content div contentDiv.appendChild(tableWrapper); const addRowBtn = document.createElement("button"); addRowBtn.textContent = "Add Row"; addRowBtn.className = "add-row-btn"; // Add a class for styling addRowBtn.style.display = "block"; // Ensure it's a block element addRowBtn.style.marginTop = "5px"; // Position right at the edge of the box addRowBtn.style.marginBottom = "0"; // Remove any bottom margin addRowBtn.addEventListener("click", () => { addDynamicEntry(tbody, { date: getToday(), time: getBerlinTime(), income: "", dailyYield: "", currentValue: 0 }, trackerId); saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); contentDiv.appendChild(addRowBtn); trackerDiv.appendChild(contentDiv); $('coinTrackerContainer').appendChild(trackerDiv); // Initialize Sortable on this table by default if (window.Sortable) { const tbody = table.querySelector("tbody"); Sortable.create(tbody, { animation: 150, onEnd: () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); } }); } updateCoinTrackerSummary(trackerId); updateTransferDropdown(); // Add this line to save the new tracker immediately after adding it saveCoinTrackers(); }; // Modified addDynamicEntry: assign a creation timestamp so we can track insertion order const addDynamicEntry = (tbody, rowData, trackerId) => { const tr = document.createElement("tr"); // Use existing created value if provided, otherwise use current timestamp tr.dataset.created = rowData.created || Date.now(); // Store transaction ID if provided (for linked entries) if (rowData.transactionId) { tr.dataset.transactionId = rowData.transactionId; } // Store transaction type if provided if (rowData.transactionType) { tr.dataset.transactionType = rowData.transactionType; } const tdDate = document.createElement("td"); const inputDate = document.createElement("input"); inputDate.type = "date"; inputDate.value = rowData.date || getToday(); inputDate.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); const trackerDiv = $(trackerId); if (!trackerDiv.dataset.manualSort || trackerDiv.dataset.manualSort === "false") { sortCoinTrackerRows(trackerDiv); } }); tdDate.appendChild(inputDate); tr.appendChild(tdDate); const tdTime = document.createElement("td"); const inputTime = document.createElement("input"); inputTime.type = "time"; inputTime.value = rowData.time || getBerlinTime(); inputTime.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdTime.appendChild(inputTime); tr.appendChild(tdTime); const tdCurrent = document.createElement("td"); tdCurrent.innerHTML = "$"; const inputCurrent = document.createElement("input"); inputCurrent.type = "number"; inputCurrent.step = "0.01"; inputCurrent.value = rowData.currentValue ? Number(rowData.currentValue).toFixed(2) : "0.00"; inputCurrent.addEventListener("input", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); inputCurrent.addEventListener("keyup", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); inputCurrent.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdCurrent.appendChild(inputCurrent); tr.appendChild(tdCurrent); const tdIncome = document.createElement("td"); const inputIncome = document.createElement("input"); inputIncome.type = "number"; inputIncome.step = "0.01"; inputIncome.value = rowData.income || ""; inputIncome.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdIncome.appendChild(inputIncome); tr.appendChild(tdIncome); const tdCompound = document.createElement("td"); const inputCompound = document.createElement("input"); inputCompound.type = "number"; inputCompound.step = "0.01"; inputCompound.value = rowData.compound || ""; inputCompound.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdCompound.appendChild(inputCompound); tr.appendChild(tdCompound); const tdYield = document.createElement("td"); const inputYield = document.createElement("input"); inputYield.type = "number"; inputYield.step = "0.01"; inputYield.value = rowData.dailyYield || ""; inputYield.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); updateRowYieldProjections(tr); }); tdYield.appendChild(inputYield); // Add a div to hold the yield projections within the cell const projectionDiv = document.createElement("div"); projectionDiv.className = "inline-yield-projection"; tdYield.appendChild(projectionDiv); tr.appendChild(tdYield); const tdActions = document.createElement("td"); const delBtn = document.createElement("button"); delBtn.textContent = "Delete"; delBtn.className = "deleteRowBtn"; delBtn.addEventListener("click", () => { tr.remove(); saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdActions.appendChild(delBtn); tr.appendChild(tdActions); // Add Notes cell const tdNotes = document.createElement("td"); const notesInput = document.createElement("input"); notesInput.type = "text"; notesInput.value = rowData.notes || ""; notesInput.placeholder = "Transaction notes"; notesInput.style.width = "100%"; notesInput.addEventListener("change", () => { rowData.notes = notesInput.value; saveCoinTrackers(); }); tdNotes.appendChild(notesInput); tr.appendChild(tdNotes); tbody.appendChild(tr); updateRowYieldProjections(tr); }; // Enhanced version of addDynamicEntry that supports transaction types const addDynamicEntryWithData = (tbody, rowData, trackerId) => { const tr = document.createElement("tr"); tr.dataset.created = rowData.created || Date.now(); // Store transaction metadata if (rowData.transactionType) { tr.dataset.transactionType = rowData.transactionType; } if (rowData.linkedTrackerId) { tr.dataset.linkedTrackerId = rowData.linkedTrackerId; } if (rowData.conversionAmount) { tr.dataset.conversionAmount = rowData.conversionAmount; } // Create date cell const tdDate = document.createElement("td"); const inputDate = document.createElement("input"); inputDate.type = "date"; inputDate.value = rowData.date || getToday(); // Add event listeners as in your existing code inputDate.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); const trackerDiv = $(trackerId); if (!trackerDiv.dataset.manualSort || trackerDiv.dataset.manualSort === "false") { sortCoinTrackerRows(trackerDiv); } }); tdDate.appendChild(inputDate); tr.appendChild(tdDate); const tdTime = document.createElement("td"); const inputTime = document.createElement("input"); inputTime.type = "time"; inputTime.value = rowData.time || getBerlinTime(); inputTime.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdTime.appendChild(inputTime); tr.appendChild(tdTime); const tdCurrent = document.createElement("td"); tdCurrent.innerHTML = "$"; const inputCurrent = document.createElement("input"); inputCurrent.type = "number"; inputCurrent.step = "0.01"; inputCurrent.value = rowData.currentValue ? Number(rowData.currentValue).toFixed(2) : "0.00"; inputCurrent.addEventListener("input", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); inputCurrent.addEventListener("keyup", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); inputCurrent.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdCurrent.appendChild(inputCurrent); tr.appendChild(tdCurrent); const tdIncome = document.createElement("td"); const inputIncome = document.createElement("input"); inputIncome.type = "number"; inputIncome.step = "0.01"; inputIncome.value = rowData.income || ""; inputIncome.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdIncome.appendChild(inputIncome); tr.appendChild(tdIncome); const tdCompound = document.createElement("td"); const inputCompound = document.createElement("input"); inputCompound.type = "number"; inputCompound.step = "0.01"; inputCompound.value = rowData.compound || ""; inputCompound.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdCompound.appendChild(inputCompound); tr.appendChild(tdCompound); const tdYield = document.createElement("td"); const inputYield = document.createElement("input"); inputYield.type = "number"; inputYield.step = "0.01"; inputYield.value = rowData.dailyYield || ""; inputYield.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); updateRowYieldProjections(tr); }); tdYield.appendChild(inputYield); // Add a div to hold the yield projections within the cell const projectionDiv = document.createElement("div"); projectionDiv.className = "inline-yield-projection"; tdYield.appendChild(projectionDiv); tr.appendChild(tdYield); const tdActions = document.createElement("td"); const delBtn = document.createElement("button"); delBtn.textContent = "Delete"; delBtn.className = "deleteRowBtn"; delBtn.addEventListener("click", () => { tr.remove(); saveCoinTrackers(); updateCoinTrackerSummary(trackerId); }); tdActions.appendChild(delBtn); tr.appendChild(tdActions); // Add a notes column const tdNotes = document.createElement("td"); const notesInput = document.createElement("input"); notesInput.type = "text"; notesInput.value = rowData.notes || ""; notesInput.placeholder = "Transaction notes"; if (rowData.notes) { console.log("Loading notes for row:", rowData.notes); } notesInput.style.width = "100%"; notesInput.addEventListener("change", () => { rowData.notes = notesInput.value; saveCoinTrackers(); }); tdNotes.appendChild(notesInput); tr.appendChild(tdNotes); // Color code rows based on transaction type if (rowData.transactionType === "conversion-out") { tr.style.backgroundColor = "rgba(255, 99, 71, 0.2)"; // Light red for outgoing } else if (rowData.transactionType === "conversion-in") { tr.style.backgroundColor = "rgba(144, 238, 144, 0.2)"; // Light green for incoming } tbody.appendChild(tr); updateRowYieldProjections(tr); }; // New function to update the projections for a single row const updateRowYieldProjections = (row) => { const currentVal = parseFloat(row.cells[2].querySelector("input").value) || 0; const yieldVal = parseFloat(row.cells[5].querySelector("input").value) || 0; const projectionDiv = row.cells[5].querySelector(".inline-yield-projection"); if (currentVal > 0 && yieldVal > 0) { const dailyIncome = currentVal * (yieldVal / 100); const projection24h = dailyIncome; const projection7d = dailyIncome * 7; const projection30d = dailyIncome * 30; projectionDiv.innerHTML = `24h: $${projection24h.toFixed(2)} | 7d: $${projection7d.toFixed(2)} | 30d: $${projection30d.toFixed(2)}`; } else { projectionDiv.innerHTML = ""; } }; // Update Invested Value Chart const updateInvestedChart = () => { // Load existing data from localStorage let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}"); let dailyYieldHistory = JSON.parse(localStorage.getItem("persistedDailyYieldHistory") || "{}"); const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); // Collect new totals from active trackers let currentTotals = {}; let portfolioValues = {}; let totalDeposited = {}; let dailyYieldTotals = {}; // Add this to track daily yields // Calculate total initial capital across all trackers let totalInitialCapital = 0; Array.from(trackerDivs).forEach(div => { if (div.dataset.showInChart === "false") return; // Get initialCapital value const initialCapitalInput = div.querySelector(".content input[type='number']"); if (initialCapitalInput) { totalInitialCapital += parseFloat(initialCapitalInput.value || "0") || 0; } }); Array.from(trackerDivs).forEach(div => { if (div.dataset.showInChart === "false") return; const table = div.querySelector("table.coin-table"); if (table) { const tbody = table.querySelector("tbody"); let trackerDaily = {}; Array.from(tbody.rows).forEach(tr => { const dateInput = tr.cells[0].querySelector("input"); const timeInput = tr.cells[1].querySelector("input"); if (dateInput && dateInput.value) { const date = dateInput.value; // "YYYY-MM-DD" const time = (timeInput && timeInput.value) ? timeInput.value : "00:00"; const currentInput = tr.cells[2].querySelector("input"); const dailyYieldPercentInput = tr.cells[5].querySelector("input"); const currentVal = parseFloat(currentInput.value.replace("$", "")) || 0; const dailyYieldPercent = parseFloat(dailyYieldPercentInput.value || "0") || 0; // Store only the largest (latest) time entry for each date if (!trackerDaily[date] || time > trackerDaily[date].time) { trackerDaily[date] = { time, current: currentVal }; } // Track portfolio value portfolioValues[date] = (portfolioValues[date] || 0) + currentVal; // Calculate daily yield in dollars and accumulate for each date const yieldInDollars = currentVal * (dailyYieldPercent / 100); dailyYieldTotals[date] = (dailyYieldTotals[date] || 0) + yieldInDollars; // Set totalDeposited to the initial capital for all dates totalDeposited[date] = totalInitialCapital; } }); for (const date in trackerDaily) { currentTotals[date] = (currentTotals[date] || 0) + trackerDaily[date].current; } } }); // Update daily yield history with new calculations for (const date in dailyYieldTotals) { dailyYieldHistory[date] = dailyYieldTotals[date]; } // Save updated daily yield history to localStorage localStorage.setItem("persistedDailyYieldHistory", JSON.stringify(dailyYieldHistory)); // Merge the new totals into the existing history for (const date in currentTotals) { persistedHistory[date] = currentTotals[date]; } // Save merged data back to localStorage localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory)); // Build sorted arrays const dates = Object.keys(persistedHistory).sort(); let labels = dates.map(date => new Date(date).toLocaleDateString()); let dailyFeesData = dates.map(date => dailyYieldHistory[date] || 0); let portfolioData = dates.map(date => portfolioValues[date] || persistedHistory[date]); let depositedData = dates.map(date => totalDeposited[date] || persistedHistory[date] * 0.9); // Update or create the chart if (investedChart) { investedChart.destroy(); } const ctx = $('investedChart').getContext('2d'); investedChart = new Chart(ctx, { type: 'line', data: { labels, datasets: [ { label: 'Daily Fees', data: dailyFeesData, fill: false, backgroundColor: '#00FF00', borderColor: '#00FF00', borderWidth: 2, tension: 0.3, pointRadius: 3, yAxisID: 'y1' }, { label: 'Portfolio Value', data: portfolioData, fill: false, backgroundColor: '#1E90FF', borderColor: '#1E90FF', borderWidth: 2, tension: 0.3, pointRadius: 3, yAxisID: 'y' }, { label: 'Total Deposited Value', data: depositedData, fill: false, backgroundColor: '#FFD700', borderColor: '#FFD700', borderWidth: 2, tension: 0.3, pointRadius: 3, yAxisID: 'y' } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { color: '#333333' }, ticks: { color: '#CCCCCC' } }, y: { position: 'left', beginAtZero: true, grid: { color: '#333333' }, ticks: { color: '#CCCCCC', callback: value => '$' + value } }, y1: { position: 'right', beginAtZero: true, grid: { display: false }, ticks: { color: '#CCCCCC', callback: value => '$' + value } } }, plugins: { legend: { labels: { color: '#CCCCCC', boxWidth: 15, padding: 10 } }, datalabels: { color: '#FFFFFF', font: { weight: 'bold', size: 10 }, formatter: function(value) { return value.toFixed(2); }, align: 'top', anchor: 'end', offset: 0 }, tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += '$' + context.parsed.y.toFixed(2); } return label; } } } }, backgroundColor: '#1a1a1a' } }); }; // Update Income Chart (sums total income for each coin tracker) const updateIncomeChart = () => { console.log("updateIncomeChart called"); const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); const labels = []; const incomeData = []; const compoundData = []; Array.from(trackerDivs).forEach(div => { if (div.dataset.active === "false") return; const coinNameInput = div.querySelector(".coin-tracker-header input.coin-name-input"); const coinName = coinNameInput ? coinNameInput.value : "Unnamed Coin"; let totalIncome = 0; let totalCompound = 0; const table = div.querySelector("table.coin-table"); if (table) { const tbody = table.querySelector("tbody"); Array.from(tbody.rows).forEach(tr => { if (tr.cells.length > 3) { const incomeInput = tr.cells[3].querySelector("input"); const incomeVal = parseFloat(incomeInput.value || "0"); totalIncome += isNaN(incomeVal) ? 0 : incomeVal; const compoundInput = tr.cells[4].querySelector("input"); const compoundVal = parseFloat(compoundInput.value || "0"); totalCompound += isNaN(compoundVal) ? 0 : compoundVal; } }); } labels.push(coinName); incomeData.push(totalIncome); compoundData.push(totalCompound); }); console.log("Income Chart labels:", labels); console.log("Income Chart data:", incomeData); const ctx = $('incomeChart').getContext('2d'); if (incomeChart) { incomeChart.data.labels = labels; incomeChart.data.datasets[0].data = incomeData; incomeChart.data.datasets[1].data = compoundData; incomeChart.update(); } else { incomeChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Income', data: incomeData, backgroundColor: 'rgba(255, 99, 132, 0.5)', stack: 'combined', borderColor: 'rgba(255, 99, 132, 1)', borderWidth: 1 }, { label: 'Compound', data: compoundData, backgroundColor: 'rgba(54, 162, 235, 0.5)', stack: 'combined', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 } ] }, options: { responsive: true, scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true, ticks: { callback: value => '$' + value } } } } }); // FIXED: Added missing closing brace here } }; // Update Total Invested display (using last row's current value from each tracker) const updateTotalInvested = () => { const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); let totalInitialCapital = 0; // To track sum of all initial capital let currentTotalInvestment = 0; // Current value of investments let combinedYields = 0; Array.from(trackerDivs).forEach(div => { if (div.dataset.active === "false") return; // Sum up initial capital for this tracker const initialCapitalInput = div.querySelector(".content input[type='number']"); if (initialCapitalInput) { totalInitialCapital += parseFloat(initialCapitalInput.value || "0") || 0; } const table = div.querySelector("table.coin-table"); if (!table) return; const rows = table.querySelector("tbody").rows; if (rows.length > 0) { // Take current value from the last row const lastRow = rows[rows.length - 1]; const inputCurrent = lastRow.cells[2].querySelector("input"); currentTotalInvestment += parseFloat(inputCurrent.value || "0") || 0; // Only use the last row's daily yield for this tracker const yieldInput = lastRow.cells[5].querySelector("input"); const dailyYieldPercent = parseFloat(yieldInput.value || "0") || 0; const currentVal = parseFloat(inputCurrent.value || "0") || 0; combinedYields += (currentVal * dailyYieldPercent / 100); } }); // Determine if we're in profit or loss const isProfit = currentTotalInvestment > totalInitialCapital; const difference = currentTotalInvestment - totalInitialCapital; const differencePrefix = difference > 0 ? '+' : ''; // Update the displays $('totalInvestmentValueDisplay').textContent = `Initial Capital: $${totalInitialCapital.toFixed(2)}`; // Create or update the current investment value element let currentValueDisplay = $('currentInvestmentValueDisplay'); if (!currentValueDisplay) { currentValueDisplay = document.createElement('span'); currentValueDisplay.id = 'currentInvestmentValueDisplay'; currentValueDisplay.style.marginLeft = '15px'; $('totalInvestmentValueDisplay').parentNode.insertBefore( currentValueDisplay, $('combinedDailyYieldsDisplay') ); } // Set the current value with appropriate color and show difference in brackets currentValueDisplay.textContent = `Current Value: $${currentTotalInvestment.toFixed(2)} [${differencePrefix}$${difference.toFixed(2)}]`; currentValueDisplay.style.color = isProfit ? 'green' : (currentTotalInvestment < totalInitialCapital ? 'red' : 'inherit'); $('combinedDailyYieldsDisplay').textContent = `Combined Daily Yields: $${combinedYields.toFixed(2)}`; updateInvestedChart(); updateIncomeChart(); updateAnalysisTable(); }; // Save coin trackers by reading the DOM and updating the backend. const saveCoinTrackers = () => { const trackers = []; const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); Array.from(trackerDivs).forEach(div => { const trackerId = div.id; const headerDiv = div.querySelector("div"); const coinNameInput = headerDiv.querySelector("input.coin-name-input"); const coinName = coinNameInput.value; // Get the platform value const platformInput = headerDiv.querySelector("input[list='platformListDatalist']"); const platform = platformInput ? platformInput.value : ""; const showInChart = div.dataset.showInChart; // persist the state here const active = div.dataset.active; // save active state // Get initialCapital value const initialCapitalInput = div.querySelector(".content input[type='number']"); const initialCapital = initialCapitalInput ? initialCapitalInput.value : "0.00"; const table = div.querySelector("table.coin-table"); const tbody = table.querySelector("tbody"); const rows = []; Array.from(tbody.rows).forEach(tr => { const date = tr.cells[0].querySelector("input").value; const time = tr.cells[1].querySelector("input").value; const currentValue = parseFloat(tr.cells[2].querySelector("input").value.replace("$", "")) || 0; const income = parseFloat(tr.cells[3].querySelector("input").value) || 0; const compound = parseFloat(tr.cells[4].querySelector("input").value) || 0; const dailyYield = parseFloat(tr.cells[5].querySelector("input").value) || 0; const notes = tr.cells[7] ? (tr.cells[7].querySelector("input") ? tr.cells[7].querySelector("input").value : "") : ""; const created = tr.dataset.created; console.log("Saving row with notes:", notes, "for date:", date); rows.push({ date, time, currentValue, income, compound, dailyYield, notes, created }); }); const hasData = rows.some(row => row.currentValue !== 0 || row.income !== 0 || row.dailyYield !== 0); if (hasData) { trackers.push({ id: trackerId, coinName, platform, initialCapital, rows, showInChart, active }); } }); updateCoinTrackersToBackend(trackers); }; // Load coin trackers exclusively from the backend. const loadCoinTrackers = () => { console.log("Loading trackers from backend..."); fetch('/api/coinTrackers') .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(responseData => { console.log("Received tracker data:", responseData); if (responseData && responseData.data && Array.isArray(responseData.data)) { // Clear existing trackers from the DOM $('coinTrackerContainer').innerHTML = ''; // Add each tracker from the backend data responseData.data.forEach(tracker => { console.log("Loading tracker:", tracker); if (tracker.rows) { tracker.rows.forEach(row => { console.log("Loading row:", row); }); } addCoinTracker(tracker); }); // Update UI after loading all trackers updateTotalInvested(); // Update the asset conversion dropdowns updateAssetDropdowns(); } else { console.error("Invalid data structure received from backend"); } }) .catch(error => { console.error("Error loading trackers:", error); }); }; // Update coin tracker summary. // Now, to determine the β€œlast entered” daily yield, we use the row with the highest creation timestamp. const updateCoinTrackerSummary = trackerId => { const trackerDiv = $(trackerId); if (!trackerDiv) return; const table = trackerDiv.querySelector("table.coin-table"); const tbody = table.querySelector("tbody"); // Use the last row (in reverse order) that has a nonzero current value. let trackerValue = 0; let lastDailyYield = 0; const rowsReversed = Array.from(tbody.rows).reverse(); for (const row of rowsReversed) { const currentVal = parseFloat(row.cells[2].querySelector("input").value) || 0; if (currentVal > 0) { lastDailyYield = parseFloat(row.cells[5].querySelector("input").value) || 0; trackerValue = currentVal; break; } } // Update header "Current Value: $" display. const lastValueSpan = trackerDiv.querySelector('.last-value'); if (lastValueSpan) { lastValueSpan.textContent = "Current Value: $" + trackerValue.toFixed(2); } // Calculate and update total income let totalIncome = 0; let totalCompound = 0; Array.from(tbody.rows).forEach(row => { const incomeVal = parseFloat(row.cells[3].querySelector("input").value) || 0; totalIncome += incomeVal; const compoundVal = parseFloat(row.cells[4].querySelector("input").value) || 0; totalCompound += compoundVal; }); // Update total income display const totalIncomeSpan = trackerDiv.querySelector('.total-income'); if (totalIncomeSpan) { totalIncomeSpan.textContent = `Total Income: $${totalIncome.toFixed(2)}`; } // Update all rows' yield projections Array.from(tbody.rows).forEach(row => { updateRowYieldProjections(row); }); // Calculate and update PnL const initialCapitalInput = trackerDiv.querySelector(".content input[type='number']"); const initialCapital = parseFloat(initialCapitalInput.value) || 0; const pnlCurrentValue = trackerValue - initialCapital; const pnlTotalValue = (trackerValue + totalIncome + totalCompound) - initialCapital; const pnlCurrentValueSpan = trackerDiv.querySelector('.pnl-current-value'); if (pnlCurrentValueSpan) { pnlCurrentValueSpan.textContent = `PnL (Current Value): $${pnlCurrentValue.toFixed(2)}`; pnlCurrentValueSpan.style.color = pnlCurrentValue >= 0 ? 'green' : 'red'; } const pnlTotalValueSpan = trackerDiv.querySelector('.pnl-total-value'); if (pnlTotalValueSpan) { pnlTotalValueSpan.textContent = `PnL (Total Value): $${pnlTotalValue.toFixed(2)}`; pnlTotalValueSpan.style.color = pnlTotalValue >= 0 ? 'green' : 'red'; } updateTotalInvested(); }; const updateTransferDropdown = () => { const select = $('transferSelect'); // If element is not found, log a warning and exit. if (!select) { console.warn("transferSelect element is not found in the DOM."); return; } select.innerHTML = ''; const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); Array.from(trackerDivs).forEach(div => { const id = div.id; // Use the specific header element (using a more specific selector) const headerDiv = div.querySelector("div.coin-tracker-header"); if (!headerDiv) return; const coinNameInput = headerDiv.querySelector("input.coin-name-input"); if (!coinNameInput) return; const option = document.createElement("option"); option.value = id; option.text = coinNameInput.value; select.appendChild(option); }); const createOption = document.createElement("option"); createOption.value = "create"; createOption.text = "Create New Coin Tracker"; select.appendChild(createOption); }; const transferSettings = () => { const select = $('transferSelect'); const selection = select.value; const currentValue = $('initialPrincipal').value; const rateValue = $('dailyRate').value; if (!selection) { alert("Please select a coin tracker or create a new one."); return; } let trackerDiv; if (selection === "create") { addCoinTracker(); const container = $('coinTrackerContainer'); trackerDiv = container.lastElementChild; } else { trackerDiv = $(selection); } if (trackerDiv) { const table = trackerDiv.querySelector("table.coin-table"); const tbody = table.querySelector("tbody"); Array.from(tbody.rows).forEach(tr => { const currentCellInput = tr.cells[2].querySelector("input"); const currentVal = parseFloat(currentCellInput.value.replace("$", "")) || 0; if (currentVal === 0) { currentCellInput.value = parseFloat(currentValue).toFixed(2); } }); const headerDiv = trackerDiv.querySelector("div"); const interestSpan = headerDiv.querySelector(".tracker-interest"); interestSpan.innerText = `(Interest Rate: ${rateValue}%)`; const earnedValue = (parseFloat(currentValue) * parseFloat(rateValue) / 100).toFixed(2); addDynamicEntry(tbody, { date: getToday(), time: getBerlinTime(), income: earnedValue, dailyYield: rateValue, currentValue: parseFloat(currentValue) }, trackerDiv.id); alert("Settings transferred!"); saveCoinTrackers(); updateCoinTrackerSummary(trackerDiv.id); updateTransferDropdown(); } }; // New function: reset (subtract) a specified dollar amount from a specific day in the graph. const resetGraphValue = () => { const resetDate = $('resetDate').value; const resetAmount = parseFloat($('resetAmount').value) || 0; console.log('Reset clicked:', resetDate, resetAmount); if (!resetDate || resetAmount <= 0) { alert("Please enter a valid date and amount."); return; } let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}"); console.log('Persisted history before update:', persistedHistory); if (persistedHistory.hasOwnProperty(resetDate)) { persistedHistory[resetDate] = Math.max(0, persistedHistory[resetDate] - resetAmount); localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory)); updateInvestedChart(); alert(`Updated value for ${resetDate}.`); } else { alert("No value exists for that date."); } }; // New function: delete all datapoints for a specific date from the graph. const deleteGraphDataPoint = () => { const resetDate = $('resetDate').value; if (!resetDate) { alert("Please select a valid date."); return; } let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}"); if (persistedHistory.hasOwnProperty(resetDate)) { delete persistedHistory[resetDate]; localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory)); updateInvestedChart(); alert(`Deleted data for ${resetDate}.`); } else { alert("No data exists for that date."); } }; function updatePlatformDatalist(platform) { const list = document.getElementById("platformListDatalist"); if (!list) return; const existingOption = Array.from(list.options).find(opt => opt.value === platform); if (!existingOption && platform.trim().length > 0) { const newOption = document.createElement("option"); newOption.value = platform; list.appendChild(newOption); } } const updateDailyYieldChart = () => { // Load existing data from localStorage let persistedDailyYieldHistory = JSON.parse(localStorage.getItem("persistedDailyYieldHistory") || "{}"); const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); // Collect new yields from active trackers let dailyTotals = {}; Array.from(trackerDivs).forEach(div => { if (div.dataset.active === "false") return; const table = div.querySelector("table.coin-table"); if (table) { const tbody = table.querySelector("tbody"); // CHANGED: Process ALL rows instead of just the last one Array.from(tbody.rows).forEach(tr => { const dateInput = tr.cells[0].querySelector("input"); if (dateInput && dateInput.value) { const date = dateInput.value; // "YYYY-MM-DD" const currentInput = tr.cells[2].querySelector("input"); const dailyYieldPercentInput = tr.cells[5].querySelector("input"); const currentVal = parseFloat(currentInput.value.replace("$", "")) || 0; const dailyYieldPercent = parseFloat(dailyYieldPercentInput.value || "0") || 0; // Convert percentage to dollar amount const yieldInDollars = currentVal * (dailyYieldPercent / 100); // Sum yields for each date across trackers dailyTotals[date] = (dailyTotals[date] || 0) + yieldInDollars; } }); } }); // Merge new yields into persistedDailyYieldHistory for (const date in dailyTotals) { persistedDailyYieldHistory[date] = dailyTotals[date] // Just assign, don't add } // Save merged data back to localStorage localStorage.setItem("persistedDailyYieldHistory", JSON.stringify(persistedDailyYieldHistory)); // Build sorted arrays const dates = Object.keys(persistedDailyYieldHistory).sort(); const labels = dates.map(date => new Date(date).toLocaleDateString()); const data = dates.map(date => persistedDailyYieldHistory[date]); // Create or update the chart if (dailyYieldChart) { dailyYieldChart.data.labels = labels; dailyYieldChart.data.datasets[0].data = data; dailyYieldChart.update(); } else { const ctx = $('dailyYieldChart').getContext('2d'); dailyYieldChart = new Chart(ctx, { type: 'line', data: { labels, datasets: [ { label: 'Daily Yield ($)', data, fill: true, backgroundColor: context => { const gradient = context.chart gradient.addColorStop(0, 'rgba(255, 193, 7, 0.4)'); gradient.addColorStop(1, 'rgba(255, 193, 7, 0.1)'); return gradient; }, borderColor: '#ffc107', borderWidth: 2, tension: 0.3 } ] }, options: { responsive: true, scales: { y: { beginAtZero: true, ticks: { callback: value => '$' + value } } } } }); } }; // Function to update the analysis table based on coin tracker data const updateAnalysisTable = () => { console.log("updateAnalysisTable called"); console.trace("Call stack for updateAnalysisTable"); // Get reinvestment rate from the dedicated analysis input - NOT the main calculator const reinvestRateElement = document.getElementById('analysisReinvestRate'); const analysisDaysElement = document.getElementById('analysisDays'); console.log("Elements found:", !!reinvestRateElement, !!analysisDaysElement); console.log("Elements:", reinvestRateElement, analysisDaysElement); if (reinvestRateElement) { console.log("Reinvest rate value:", reinvestRateElement.value); console.log("Reinvest rate attribute:", reinvestRateElement.getAttribute('value')); console.log("Reinvest rate type:", typeof reinvestRateElement.value); } if (analysisDaysElement) { console.log("Days value:", analysisDaysElement.value); console.log("Days attribute:", analysisDaysElement.getAttribute('value')); console.log("Days type:", typeof analysisDaysElement.value); } const reinvestRate = reinvestRateElement ? parseFloat(reinvestRateElement.value) || 0 : 0; const days = analysisDaysElement ? parseInt(analysisDaysElement.value) || 30 : 30; console.log("Parsed values - Reinvest Rate:", reinvestRate, "Days:", days); // Get data from active coin trackers const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); // Initialize total investment and weighted yield let totalInvestment = 0; let weightedYieldSum = 0; // Calculate total investment and weighted yield Array.from(trackerDivs).forEach(div => { if (div.dataset.active === "false") return; const table = div.querySelector("table.coin-table"); if (!table) return; const rows = table.querySelector("tbody").rows; if (rows.length > 0) { const lastRow = rows[rows.length - 1]; const inputCurrent = lastRow.cells[2].querySelector("input"); const currentVal = parseFloat(inputCurrent.value || "0") || 0; const yieldInput = lastRow.cells[5].querySelector("input"); const dailyYieldPercent = parseFloat(yieldInput.value || "0") || 0; totalInvestment += currentVal; weightedYieldSum += (currentVal * dailyYieldPercent); } }); // Calculate weighted average daily yield const weightedAverageDailyYield = totalInvestment > 0 ? (weightedYieldSum / totalInvestment) : 0; console.log("Analysis values - Reinvest Rate:", reinvestRate, "Days:", days); console.log("Total Investment:", totalInvestment, "Weighted Yield:", weightedAverageDailyYield); // Calculate compounding for the next 30 days (or whatever period you want) let runningPrincipal = totalInvestment; let cumulativeCash = 0; let tableHTML = ''; let currentDate = new Date(); for (let i = 0; i < days; i++) { // Move to next day currentDate.setDate(currentDate.getDate() + 1); const dateStr = currentDate.toLocaleDateString(); // Calculate daily earnings based on the current principal const earnings = runningPrincipal * (weightedAverageDailyYield / 100); // Calculate reinvestment and cashout based on reinvest rate const reinvestment = earnings * (reinvestRate / 100); const cashout = earnings - reinvestment; // Add reinvestment to principal runningPrincipal += reinvestment; // Add cashout to cumulative cash cumulativeCash += cashout; // Create table row tableHTML += ` ${i + 1} ${dateStr} $${earnings.toFixed(2)} ${reinvestRate}% $${reinvestment.toFixed(2)} / $${cashout.toFixed(2)} $${runningPrincipal.toFixed(2)} $${cumulativeCash.toFixed(2)} `; } // Update the table $('analysisTable').querySelector('tbody').innerHTML = tableHTML; }; // Function to create a manual backup const createManualBackup = () => { const backupNameInput = document.querySelector('input[placeholder="Backup name (optional)"]'); const backupName = backupNameInput ? backupNameInput.value : ''; console.log("Creating manual backup with name:", backupName || "(unnamed)"); // Show loading indicator if (backupNameInput) { backupNameInput.disabled = true; } document.getElementById('createBackupBtn').disabled = true; document.getElementById('createBackupBtn').textContent = 'Creating backup...'; // Make request to API fetch(`/api/backup/create${backupName ? `?name=${encodeURIComponent(backupName)}` : ''}`) .then(response => { if (!response.ok) { throw new Error(`Network error: ${response.status}`); } return response.json(); }) .then(data => { console.log('Backup created successfully:', data); alert('Backup created successfully!'); // Refresh backup list loadBackupList(); }) .catch(error => { console.error('Error creating backup:', error); alert(`Failed to create backup: ${error.message}`); }) .finally(() => { // Reset UI if (backupNameInput) { backupNameInput.disabled = false; backupNameInput.value = ''; } document.getElementById('createBackupBtn').disabled = false; document.getElementById('createBackupBtn').textContent = 'Create Manual Backup'; }); }; // Function to load the list of available backups const loadBackupList = () => { fetch('/api/backup/list') .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { const backupCalendar = $('backupCalendar'); backupCalendar.innerHTML = ''; if (data.backups && data.backups.length > 0) { // Group backups by date const backupsByDate = {}; data.backups.forEach(backup => { const date = new Date(backup.date); const dateKey = date.toISOString().split('T')[0]; if (!backupsByDate[dateKey]) { backupsByDate[dateKey] = []; } backupsByDate[dateKey].push(backup); }); // Sort dates in reverse order (newest first) const sortedDates = Object.keys(backupsByDate).sort().reverse(); sortedDates.forEach(dateKey => { const backupsForDate = backupsByDate[dateKey]; // Create date header const dateHeader = document.createElement('div'); dateHeader.className = 'backup-date-header'; dateHeader.style.gridColumn = '1 / -1'; dateHeader.style.background = '#333'; dateHeader.style.padding = '5px'; dateHeader.style.fontWeight = 'bold'; dateHeader.style.borderRadius = '4px'; dateHeader.style.marginTop = '10px'; dateHeader.textContent = new Date(dateKey).toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); backupCalendar.appendChild(dateHeader); // Add the backups for this date backupsForDate.forEach(backup => { const backupCard = document.createElement('div'); backupCard.className = 'backup-card'; backupCard.style.padding = '10px'; backupCard.style.background = '#555'; backupCard.style.borderRadius = '4px'; backupCard.style.cursor = 'pointer'; backupCard.style.transition = 'background-color 0.3s'; // Format timestamp nicely const time = new Date(backup.date).toLocaleTimeString(); const size = (backup.size / 1024).toFixed(1) + ' KB'; // Get backup name if available const fileName = backup.file; let backupName = fileName; if (fileName.includes('_name_')) { backupName = fileName.split('_name_')[1].split('.json')[0]; } backupCard.innerHTML = `
${time}
${backupName}
${size}
`; backupCard.addEventListener('click', () => { // Remove selected class from all cards document.querySelectorAll('.backup-card').forEach(card => { card.style.background = '#555'; }); // Add selected class to this card backupCard.style.background = '#007bff'; // Show backup details $('selectedBackupInfo').style.display = 'block'; $('backupDetails').innerHTML = `

Date: ${new Date(backup.date).toLocaleString()}

Name: ${backupName}

Size: ${size}

Filename: ${backup.file}

`; // Store the selected backup filename $('restoreBackupBtn').dataset.file = backup.file; }); backupCalendar.appendChild(backupCard); }); }); // Show the backup calendar $('backupCalendarContainer').style.display = 'block'; } else { backupCalendar.innerHTML = '

No backups available.

'; $('backupCalendarContainer').style.display = 'block'; } }) .catch(error => { console.error('Error loading backups:', error); $('backupCalendar').innerHTML = '

Failed to load backups. See console for details.

'; $('backupCalendarContainer').style.display = 'block'; }); }; // Function to restore from a backup const restoreFromBackup = (filename) => { if (!filename) { alert('No backup selected'); return; } if (!confirm(`Are you sure you want to restore from backup "${filename}"? This will replace all current data.`)) { return; } fetch(`/api/backup/restore/${filename}`) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { alert('Backup restored successfully! The page will reload.'); window.location.reload(); }) .catch(error => { console.error('Error restoring backup:', error); alert('Failed to restore backup. See console for details.'); }); }; // Function to log conversions to history const logConversion = (conversionData) => { // Get existing history let conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]"); // Add new conversion with timestamp conversionData.timestamp = Date.now(); conversionHistory.push(conversionData); // Save back to localStorage localStorage.setItem("conversionHistory", JSON.stringify(conversionHistory)); // Update the conversion history table - with error handling try { updateConversionHistoryTable(); } catch (error) { console.error("Error updating conversion history table:", error); // Don't show alert to user as this is not critical } }; // Function to update the conversion history table const updateConversionHistoryTable = () => { const tableBody = document.querySelector("#conversionHistoryTable tbody"); if (!tableBody) { console.warn("Conversion history table tbody not found in the DOM"); return; } // Get conversion history const conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]"); // Sort by date, newest first conversionHistory.sort((a, b) => new Date(b.date) - new Date(a.date)); // Clear existing rows tableBody.innerHTML = ""; // Add rows for each conversion conversionHistory.forEach((conversion, index) => { // Create row elements only if conversion data exists if (!conversion) return; const row = document.createElement("tr"); row.dataset.index = index; // Store the index for easy deletion const dateCell = document.createElement("td"); dateCell.textContent = `${conversion.date} ${conversion.time}`; row.appendChild(dateCell); const fromCell = document.createElement("td"); fromCell.textContent = conversion.fromName; row.appendChild(fromCell); const amountCell = document.createElement("td"); // For backward compatibility with existing data const dollarAmount = conversion.dollarAmount || conversion.sentAmount || 0; // If this is an income transfer, show that in the cell if (conversion.transferType === 'income') { amountCell.textContent = `$${dollarAmount.toFixed(2)} (Income)`; amountCell.style.color = '#4CAF50'; // Green for income transfers } else { amountCell.textContent = `$${dollarAmount.toFixed(2)}`; } row.appendChild(amountCell); const toCell = document.createElement("td"); toCell.textContent = conversion.toName; row.appendChild(toCell); const receivedCell = document.createElement("td"); // Same dollar amount, preserves consistency with existing data receivedCell.textContent = `$${dollarAmount.toFixed(2)}`; row.appendChild(receivedCell); // Add delete button const actionsCell = document.createElement("td"); const deleteBtn = document.createElement("button"); deleteBtn.textContent = "Delete"; deleteBtn.style.padding = "3px 8px"; deleteBtn.style.background = "#ff4444"; deleteBtn.style.color = "white"; deleteBtn.style.border = "none"; deleteBtn.style.borderRadius = "3px"; deleteBtn.style.cursor = "pointer"; deleteBtn.addEventListener("click", function() { if (confirm("CONFIRM DELETION: This will:\n\n1. REMOVE the destination entry (the target coin purchase)\n2. KEEP the source entry but clear its transfer note\n\nSource rows will NEVER be deleted, only their transfer notes will be cleared.")) { // Always re-read localStorage to avoid index bugs let conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]"); // CRITICAL FIX: The table displays sorted history, but we need to find the record by its actual properties, not index // because the displayed index might not match the storage array index due to sorting const displayedConversions = [...conversionHistory].sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0)); const conversionRecord = displayedConversions[index]; // Find the actual index in the original unsorted array const actualIndex = conversionHistory.findIndex(record => record.transactionId === conversionRecord.transactionId || (record.date === conversionRecord.date && record.time === conversionRecord.time && record.fromName === conversionRecord.fromName && record.toName === conversionRecord.toName && record.dollarAmount === conversionRecord.dollarAmount) ); console.log("=== SCRIPT.JS DELETION DEBUG START ==="); console.log("Displayed index (from sorted array):", index); console.log("Actual index (in storage array):", actualIndex); console.log("Conversion record being deleted:", conversionRecord); console.log("Total conversion history length:", conversionHistory.length); console.log("=== SCRIPT.JS DELETION DEBUG END ==="); // If the conversion has a transaction ID, find and handle related tracker entries with protection if (conversionRecord && conversionRecord.transactionId) { const transactionId = conversionRecord.transactionId; const fromName = conversionRecord.fromName; const toName = conversionRecord.toName; console.log(`Processing deletion for transaction ${transactionId}: ${fromName} β†’ ${toName}`); // Get all coin trackers const trackers = document.querySelectorAll('.coin-tracker'); // STEP 1: First find and process ONLY the destination entries across all trackers trackers.forEach(tracker => { const tbody = tracker.querySelector('table.coin-table tbody'); if (!tbody) return; // Find destination entries first - we ONLY delete these const destRows = tbody.querySelectorAll(`tr[data-transaction-id="${transactionId}"][data-transaction-type="conversion-in"], tr[data-transaction-id="${transactionId}"][data-transaction-type="income-transfer-in"]`); console.log(`Found ${destRows.length} destination rows in tracker ${tracker.id}`); // Delete ONLY destination rows with extreme caution destRows.forEach(row => { // TRIPLE-CHECK that this is NOT a source row under any circumstances const isSafeToDelete = // 1. Check it does not have the source marker row.dataset.sourceRow !== 'true' && // 2. Check it is explicitly a destination type (row.dataset.transactionType === 'conversion-in' || row.dataset.transactionType === 'income-transfer-in'); // Make sure this is a tracker for the destination asset before deleting const trackerNameInput = tracker.querySelector(".coin-tracker-header .coin-name-input"); const trackerName = trackerNameInput ? trackerNameInput.value : ""; if (!isSafeToDelete) { console.log(`SAFETY BLOCK: Prevented deletion of row that might be a source row`); } else if (trackerName === toName) { console.log(`Removing destination row in ${toName}`); row.remove(); // ONLY destination rows are deleted } else { console.log(`NOT removing destination row in ${trackerName || tracker.id} because it's not the target asset ${toName}`); } }); }); // STEP 2: Now find and modify ONLY source entries across all trackers console.log("STEP 2: Processing source entries - These should NEVER be deleted, only notes cleared"); trackers.forEach(tracker => { const tbody = tracker.querySelector('table.coin-table tbody'); if (!tbody) return; // Get all rows that might be source entries const allRows = tbody.querySelectorAll('tr'); allRows.forEach(row => { // Method 1: Check by explicit source row marker (highest priority) const isSourceByMarker = row.dataset && row.dataset.sourceRow === 'true'; // Method 2: Check by transaction ID and transaction type const isSourceByID = row.dataset && row.dataset.transactionId === transactionId && row.dataset.transactionType !== 'conversion-in' && row.dataset.transactionType !== 'income-transfer-in'; // Method 3: Check by specific source transaction types const isSourceByType = row.dataset && row.dataset.transactionType && (row.dataset.transactionType === 'conversion-out' || row.dataset.transactionType === 'income-source' || row.dataset.transactionType === 'income-transfer-note' || row.dataset.transactionType === 'source-recovered'); // Method 4: Check by notes content let isSourceByNotes = false; const notesCell = row.cells && row.cells[7] ? row.cells[7] : null; const notesInput = notesCell ? notesCell.querySelector("input") : null; const notes = notesInput ? notesInput.value : ""; if (notes.includes(`TRANSFERRED: $`) && notes.includes(`to buy ${toName}`)) { isSourceByNotes = true; } if (notes.includes(`Moved $`) && notes.includes(`to ${toName}`)) { isSourceByNotes = true; } // Combine all source detection methods const isSourceRowFound = isSourceByMarker || isSourceByID || isSourceByType || isSourceByNotes; // CRITICAL PROTECTION: Immediately mark any potential source row to prevent deletion if (isSourceRowFound) { // Add the source marker BEFORE doing anything else to protect this row row.dataset.sourceRow = "true"; console.log(`PROTECTED SOURCE ROW FOUND in ${tracker.id}: ${ isSourceByMarker ? "by explicit marker" : isSourceByID ? "by transaction ID" : isSourceByType ? "by transaction type" : "by notes content" }`); // NEVER delete source rows, just clear the transfer note if (notesInput) { // Only modify notes if they contain transfer information relating to this transaction const containsRelevantTransfer = (notes.includes(`TRANSFERRED: $`) && notes.includes(`to buy ${toName}`)) || (notes.includes(`Moved $`) && notes.includes(`to ${toName}`)); if (containsRelevantTransfer) { const transferTextPattern = /TRANSFERRED: \$[\d.]+( to buy [^;]+)?/; const movedTextPattern = /Moved \$[\d.]+( to [^;]+)?/; let newNotes = notes; newNotes = newNotes.replace(transferTextPattern, ""); newNotes = newNotes.replace(movedTextPattern, ""); // Clean up any trailing or leading semicolons and double semicolons newNotes = newNotes.replace(/^;\s*/, "").replace(/;\s*$/, "").replace(/;\s*;/g, ";"); notesInput.value = newNotes; console.log(`Source row notes after: "${newNotes}"`); console.log(`KEPT source row - only cleared transfer note`); } else { console.log(`Source row notes don't contain relevant transfer info for ${toName}, keeping unchanged`); } // Tag this row if it wasn't already tagged if (!isSourceByID) { row.dataset.transactionId = transactionId; row.dataset.transactionType = "source-recovered"; console.log("Tagged source row for future operations"); } } } }); }); // Update all trackers after deletion document.querySelectorAll('.coin-tracker').forEach(tracker => { const trackerId = tracker.id; updateCoinTrackerSummary(trackerId); }); // Save all trackers saveCoinTrackers(); } // Remove from the array using the actual index, not the display index if (actualIndex !== -1) { conversionHistory.splice(actualIndex, 1); console.log(`Removed conversion record at actual index ${actualIndex}`); } else { console.error("Could not find actual index for conversion record, deletion skipped"); } // Save back to local storage localStorage.setItem("conversionHistory", JSON.stringify(conversionHistory)); // Remove the row from the table row.remove(); // Update conversion stats updateConversionStats(); // Update total investment display if (typeof updateTotalInvested === 'function') { updateTotalInvested(); } } }); actionsCell.appendChild(deleteBtn); row.appendChild(actionsCell); // Append the row to the table body tableBody.appendChild(row); }); }; // Function to update conversion statistics const updateConversionStats = () => { try { const statsTable = document.querySelector("#conversionStatsTable tbody"); if (!statsTable) { console.warn("Conversion stats table tbody not found in the DOM"); return; } // Get conversion history const conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]"); // Track flows by asset const assetFlows = {}; // Process all conversions conversionHistory.forEach(conversion => { // Skip invalid conversions if (!conversion || !conversion.fromName || !conversion.toName) return; // Initialize asset records if they don't exist if (!assetFlows[conversion.fromName]) { assetFlows[conversion.fromName] = { in: 0, out: 0 }; } if (!assetFlows[conversion.toName]) { assetFlows[conversion.toName] = { in: 0, out: 0 }; } // Determine the dollar amount (support both new and old data format) const dollarAmount = conversion.dollarAmount || conversion.sentAmount || 0; // Record the flow (same dollar amount for both directions) assetFlows[conversion.fromName].out += dollarAmount; assetFlows[conversion.toName].in += dollarAmount; }); // Clear existing rows statsTable.innerHTML = ""; // Create rows for each asset Object.keys(assetFlows).sort().forEach(asset => { const flow = assetFlows[asset]; const netFlow = flow.in - flow.out; const row = document.createElement("tr"); const assetCell = document.createElement("td"); assetCell.textContent = asset; row.appendChild(assetCell); const inCell = document.createElement("td"); inCell.textContent = `$${flow.in.toFixed(2)}`; row.appendChild(inCell); const outCell = document.createElement("td"); outCell.textContent = `$${flow.out.toFixed(2)}`; row.appendChild(outCell); const netCell = document.createElement("td"); netCell.textContent = `$${netFlow.toFixed(2)}`; netCell.style.color = netFlow >= 0 ? 'green' : 'red'; row.appendChild(netCell); statsTable.appendChild(row); }); } catch (error) { console.error("Error updating conversion stats:", error); // Don't show alert to user as this is not critical } }; // Add these functions after your existing functions but before the DOMContentLoaded event // Record asset transfer between trackers - dollar value based const recordAssetTransfer = (fromId, toId, dollarAmount, transferType = 'principal', selectedIncomeData = null) => { console.log("Recording asset transfer:", {fromId, toId, dollarAmount, transferType, selectedIncomeData}); console.log("fromId type:", typeof fromId, "toId type:", typeof toId); // Make sure we can find the tracker elements console.log("Looking for element with ID:", fromId); const fromElement = document.getElementById(fromId); console.log("Direct document.getElementById result:", fromElement); const fromTracker = $(fromId); const toTracker = $(toId); console.log("Tracker elements:", {fromTracker, toTracker}); if (!fromTracker || !toTracker || dollarAmount <= 0) { alert("Please select valid source and destination trackers and enter a positive dollar amount"); console.error("Invalid transfer parameters:", {fromTracker, toTracker, dollarAmount}); return; } // Get coin names for logging const fromName = fromTracker.querySelector(".coin-tracker-header input.coin-name-input").value; const toName = toTracker.querySelector(".coin-tracker-header input.coin-name-input").value; const sourceTable = fromTracker.querySelector("table.coin-table"); const sourceTbody = sourceTable.querySelector("tbody"); const today = getToday(); const currentTime = getBerlinTime(); // Record the transaction ID for linked entries (used for deletion) const transactionId = `transfer_${Date.now()}`; if (transferType === 'income') { // For income transfer - ALWAYS try to add note to a source row instead of creating a negative entry if (selectedIncomeData) { // User has selected a specific income row to use const incomeRowIndex = selectedIncomeData.rowIndex; const sourceRows = Array.from(sourceTbody.rows); if (incomeRowIndex >= 0 && incomeRowIndex < sourceRows.length) { const incomeRow = sourceRows[incomeRowIndex]; // Update the notes field in the income row const notesCell = incomeRow.cells[7]; // Notes cell const notesInput = notesCell ? notesCell.querySelector("input") : null; if (notesInput) { // Make very visible notes about the transfer const currentNotes = notesInput.value; const transferNote = `TRANSFERRED: $${dollarAmount.toFixed(2)} to buy ${toName}`; notesInput.value = currentNotes ? `${currentNotes}; ${transferNote}` : transferNote; // Set data attribute to track this transaction incomeRow.dataset.transactionId = transactionId; incomeRow.dataset.incomeUsed = dollarAmount; incomeRow.dataset.usedForTracker = toId; // Optionally update the row style to indicate it was used in a transfer incomeRow.style.backgroundColor = "rgba(255, 165, 0, 0.1)"; // Light orange background } } } else { // No specific row selected - try to find any positive income row to add note to // Get rows as an array and reverse to start with the newest rows first const sourceRows = Array.from(sourceTbody.rows).reverse(); let foundRow = false; console.log(`Checking ${sourceRows.length} source rows for income to transfer`); // Try to find a row with positive income, starting with the newest rows for (const row of sourceRows) { const incomeInput = row.cells[3].querySelector("input"); const income = parseFloat(incomeInput?.value) || 0; console.log(`Checking row with income: ${income}, need: ${dollarAmount}`); if (income > 0 && income >= dollarAmount) { // Found a suitable income row, update its notes const notesCell = row.cells[7]; // Notes cell const notesInput = notesCell ? notesCell.querySelector("input") : null; if (notesInput) { const currentNotes = notesInput.value; const transferNote = `TRANSFERRED: $${dollarAmount.toFixed(2)} to buy ${toName}`; notesInput.value = currentNotes ? `${currentNotes}; ${transferNote}` : transferNote; console.log(`Added transfer note to row with income ${income}`); // Mark this row as involved in the transaction row.dataset.transactionId = transactionId; row.dataset.incomeUsed = dollarAmount; row.dataset.usedForTracker = toId; // Make the row visually distinct row.style.backgroundColor = "rgba(255, 165, 0, 0.1)"; // Light orange background foundRow = true; break; } } } // Only if we couldn't find a suitable income row, create a new entry if (!foundRow) { console.log("No suitable income row found, creating a note row without negative income"); // Create a note entry rather than a negative income entry addDynamicEntry(sourceTbody, { date: today, time: currentTime, currentValue: 0, // Don't change the principal amount income: 0, // No negative income - just a note compound: 0, dailyYield: 0, notes: `TRANSFERRED: $${dollarAmount.toFixed(2)} to buy ${toName}`, transactionType: "income-transfer-note", transactionId: transactionId // Add transaction ID for linked entries }, fromId); } } } else { // Regular principal transfer - subtract from current value of source addDynamicEntry(sourceTbody, { date: today, time: currentTime, currentValue: -dollarAmount, // Use negative value to represent money leaving this tracker income: 0, compound: 0, dailyYield: 0, notes: `Moved $${dollarAmount.toFixed(2)} to ${toName}`, transactionType: "conversion-out", transactionId: transactionId // Add transaction ID for linked entries }, fromId); } // 2. Add a "conversion in" row to destination tracker const destTable = toTracker.querySelector("table.coin-table"); const destTbody = destTable.querySelector("tbody"); // Get the last current value from the destination tracker // This is key to the first improvement - add to the last known value instead of standalone value let lastDestValue = 0; const destRows = Array.from(destTbody.querySelectorAll("tr")); if (destRows.length > 0) { // Get the value from the last row in the destination tracker const lastRow = destRows[destRows.length - 1]; const currentValueInput = lastRow.cells[2].querySelector("input"); if (currentValueInput) { lastDestValue = parseFloat(currentValueInput.value) || 0; } } // Calculate the new destination value const newDestValue = lastDestValue + dollarAmount; // For destination, add to the last current value addDynamicEntry(destTbody, { date: today, time: currentTime, currentValue: newDestValue, // Add to the last destination value income: 0, compound: 0, dailyYield: 0, notes: transferType === 'income' ? `BOUGHT: ${toName} using $${dollarAmount.toFixed(2)} income from ${fromName} (previous value: $${lastDestValue.toFixed(2)})` : `BOUGHT: ${toName} using $${dollarAmount.toFixed(2)} from ${fromName} (previous value: $${lastDestValue.toFixed(2)})`, transactionType: transferType === 'income' ? "income-transfer-in" : "conversion-in", transactionId: transactionId // Add transaction ID for linked entries }, toId); // Log the conversion to history with additional details for better tracking logConversion({ date: today, time: currentTime, fromName: fromName, toName: toName, dollarAmount: dollarAmount, transferType: transferType, destPreviousValue: lastDestValue, // Store the previous value for reference destNewValue: newDestValue, // Store the new combined value transactionId: transactionId // Store the transaction ID for linked deletion }); // Update both trackers updateCoinTrackerSummary(fromId); updateCoinTrackerSummary(toId); saveCoinTrackers(); // Update conversion stats - wrap in try/catch to prevent errors try { updateConversionStats(); } catch (error) { console.error("Error updating conversion stats:", error); // Don't show alert to user as this is not critical } // Update asset dropdowns in case there are new trackers updateAssetDropdowns(); alert(`Transferred $${dollarAmount.toFixed(2)} from ${fromName} to ${toName}`); }; // Update asset dropdowns to show available coin trackers const updateAssetDropdowns = () => { console.log("Updating asset dropdowns"); const fromSelect = document.getElementById('fromAsset'); const toSelect = document.getElementById('toAsset'); if (!fromSelect || !toSelect) { console.warn("Asset dropdown elements not found, make sure the HTML is correctly set up"); return; } // Clear and initialize dropdowns with default option fromSelect.innerHTML = ''; toSelect.innerHTML = ''; // Get all active coin trackers const container = document.getElementById('coinTrackerContainer'); if (!container) { console.warn("Coin tracker container not found"); return; } const trackerDivs = container.getElementsByClassName("coin-tracker"); console.log(`Found ${trackerDivs.length} coin trackers for dropdown population`); Array.from(trackerDivs).forEach(div => { if (div.dataset.active === "false") return; const trackerId = div.id; const coinNameInput = div.querySelector(".coin-tracker-header input.coin-name-input"); const coinName = coinNameInput ? coinNameInput.value : "Unnamed"; console.log(`Adding ${coinName} (${trackerId}) to asset dropdowns`); // Add to source dropdown const sourceOption = document.createElement("option"); sourceOption.value = trackerId; sourceOption.textContent = coinName; fromSelect.appendChild(sourceOption); // Add to destination dropdown const destOption = document.createElement("option"); destOption.value = trackerId; destOption.textContent = coinName; toSelect.appendChild(destOption); }); }; // Function to update the income rows dropdown with available income entries from the selected tracker const updateIncomeRowsDropdown = (trackerId) => { console.log("Updating income rows dropdown for tracker:", trackerId); const incomeSelect = document.getElementById('incomeRowSelection'); if (!incomeSelect) { console.warn("Income rows dropdown not found"); return; } // Clear existing options incomeSelect.innerHTML = ''; // If no tracker is selected, exit if (!trackerId) return; const tracker = $(trackerId); if (!tracker) return; const table = tracker.querySelector("table.coin-table"); if (!table) return; const tbody = table.querySelector("tbody"); if (!tbody) return; // Find rows with positive income values const rows = Array.from(tbody.rows); // Track if we found any income rows let foundIncomeRows = false; rows.forEach((row, index) => { const dateInput = row.cells[0].querySelector("input"); const timeInput = row.cells[1].querySelector("input"); const incomeInput = row.cells[3].querySelector("input"); const income = parseFloat(incomeInput.value) || 0; // Only add entries with positive income if (income > 0) { foundIncomeRows = true; const date = dateInput.value; const time = timeInput.value; const option = document.createElement("option"); // Store index and income value in the option value as JSON option.value = JSON.stringify({ rowIndex: index, income: income, date: date, time: time }); // Display date and income value option.textContent = `${date} ${time} - $${income.toFixed(2)}`; incomeSelect.appendChild(option); } }); // If no income rows found, add an option indicating this if (!foundIncomeRows) { const option = document.createElement("option"); option.value = ""; option.textContent = "No income entries found"; option.disabled = true; incomeSelect.appendChild(option); } }; // Add event listeners for the conversion form controls const conversionTypeSelect = $('conversionType'); const fromAssetSelect = $('fromAsset'); // Show/hide income row selection based on conversion type if (conversionTypeSelect) { conversionTypeSelect.addEventListener('change', () => { const incomeSelectionDiv = $('incomeRowSelectionDiv'); if (incomeSelectionDiv) { incomeSelectionDiv.style.display = conversionTypeSelect.value === 'income' ? 'block' : 'none'; // If switching to income and a source is already selected, update the income rows dropdown if (conversionTypeSelect.value === 'income' && fromAssetSelect.value) { updateIncomeRowsDropdown(fromAssetSelect.value); } } }); // Trigger change event to set initial state conversionTypeSelect.dispatchEvent(new Event('change')); } // Update income rows dropdown when source asset changes if (fromAssetSelect) { fromAssetSelect.addEventListener('change', () => { const conversionTypeSelect = $('conversionType'); if (conversionTypeSelect && conversionTypeSelect.value === 'income') { updateIncomeRowsDropdown(fromAssetSelect.value); } }); } // Immediate debug logging console.log("πŸ”₯ Script loaded, document ready state:", document.readyState); console.log("πŸ”₯ Setting up DOMContentLoaded listener..."); document.addEventListener("DOMContentLoaded", () => { console.log("πŸ”₯ DOM Content Loaded - Setting up event handlers"); console.log("πŸ”₯ Starting analysis event listener setup..."); console.log("πŸ”₯ Location:", window.location.href); console.log("πŸ”₯ Document ready state:", document.readyState); // Make sure to update asset dropdowns when page loads updateAssetDropdowns(); document.querySelectorAll(".number-input button").forEach(button => { const handleInteraction = e => { e.preventDefault(); const targetId = button.getAttribute("data-target"); const delta = parseFloat(button.getAttribute("data-delta")); adjustValue(targetId, delta); }; if (window.PointerEvent) { button.addEventListener("pointerup", handleInteraction, false); } else { button.addEventListener("click", handleInteraction, false); button.addEventListener("touchend", handleInteraction, false); } }); ['initialPrincipal', 'dailyRate', 'termDays', 'reinvestRate'].forEach(id => { $(id).addEventListener("input", calculate); }); $('totalAmountPercent').addEventListener("input", calculatePercentage); $('givenAmount').addEventListener("input", calculatePercentage); calculatePercentage(); $('totalAmountPercent2').addEventListener("input", calculatePercentage2); $('percentInput2').addEventListener("input", calculatePercentage2); calculatePercentage2(); setupCollapsibles(); $('transferSettingsBtn').addEventListener("click", transferSettings); $('addCoinTrackerBtn').addEventListener("click", () => { addCoinTracker(); // No need to call saveCoinTrackers() here as it's now called inside addCoinTracker }); // Add event listener for the Export Coin Trackers button $('exportTrackersBtn').addEventListener("click", exportCoinTrackers); calculate(); loadCoinTrackers(); updateTransferDropdown(); // Simplified chart display code const showInvestedBtn = $('showInvestedChartBtn'); const showIncomeBtn = $('showIncomeChartBtn'); // Default display - only show invested chart $('investedChart').style.display = "block"; $('incomeChart').style.display = "none"; // Button click handlers showInvestedBtn.addEventListener("click", () => { $('investedChart').style.display = "block"; $('incomeChart').style.display = "none"; }); showIncomeBtn.addEventListener("click", () => { $('investedChart').style.display = "none"; $('incomeChart').style.display = "block"; }); // Event listeners for the new reset and delete graph value buttons $('resetGraphValueBtn').addEventListener("click", resetGraphValue); $('deleteGraphDataPointBtn').addEventListener("click", deleteGraphDataPoint); ['investedChart','incomeChart','dailyYieldChart'].forEach(chartId => { const chartEl = document.getElementById(chartId); chartEl.addEventListener('click', () => { chartEl.classList.toggle('chart-fullscreen'); // Resize chart if needed if (window[chartId]) { window[chartId].resize(); } }); }); // Add event listener for reinvest rate changes $('reinvestRate').addEventListener("input", updateAnalysisTable); // Add event listeners for analysis controls - live updates console.log("πŸ”₯ Setting up analysis event listeners..."); const reinvestRateEl = $('analysisReinvestRate'); const analysisDaysEl = $('analysisDays'); console.log("πŸ”₯ Analysis elements found:", !!reinvestRateEl, !!analysisDaysEl); console.log("πŸ”₯ Reinvest element:", reinvestRateEl); console.log("πŸ”₯ Days element:", analysisDaysEl); console.log("πŸ”₯ Current values:", reinvestRateEl?.value, analysisDaysEl?.value); if (reinvestRateEl) { console.log("πŸ”₯ Adding event listeners to reinvest rate element"); const inputHandler = () => { console.log("πŸ”₯ Reinvest rate INPUT event - new value:", reinvestRateEl.value); updateAnalysisTable(); }; const keyupHandler = () => { console.log("πŸ”₯ Reinvest rate KEYUP event - new value:", reinvestRateEl.value); updateAnalysisTable(); }; const changeHandler = () => { console.log("πŸ”₯ Reinvest rate CHANGE event - new value:", reinvestRateEl.value); updateAnalysisTable(); }; reinvestRateEl.addEventListener("input", inputHandler); reinvestRateEl.addEventListener("keyup", keyupHandler); reinvestRateEl.addEventListener("change", changeHandler); console.log("πŸ”₯ Event listeners attached to reinvest rate element"); } else { console.log("πŸ”₯ analysisReinvestRate element not found"); } if (analysisDaysEl) { console.log("πŸ”₯ Adding event listeners to analysis days element"); const inputHandler = () => { console.log("πŸ”₯ Analysis days INPUT event - new value:", analysisDaysEl.value); updateAnalysisTable(); }; const keyupHandler = () => { console.log("πŸ”₯ Analysis days KEYUP event - new value:", analysisDaysEl.value); updateAnalysisTable(); }; const changeHandler = () => { console.log("πŸ”₯ Analysis days CHANGE event - new value:", analysisDaysEl.value); updateAnalysisTable(); }; analysisDaysEl.addEventListener("input", inputHandler); analysisDaysEl.addEventListener("keyup", keyupHandler); analysisDaysEl.addEventListener("change", changeHandler); console.log("πŸ”₯ Event listeners attached to analysis days element"); } else { console.log("πŸ”₯ analysisDays element not found"); } // Backup and restore functionality $('createBackupBtn').addEventListener('click', createManualBackup); $('restoreBackupBtn').addEventListener('click', () => { const file = $('restoreBackupBtn').dataset.file; restoreFromBackup(file); }); // Set up conversion button - MOVED HERE to ensure proper initialization const executeBtn = $('executeConversion'); console.log("Finding executeConversion button:", executeBtn); if (executeBtn) { executeBtn.addEventListener('click', () => { console.log("Execute conversion button clicked"); // Check if element exists const amountInput = document.getElementById('conversionAmount'); console.log("Amount input element:", amountInput); console.log("Amount input value:", amountInput ? amountInput.value : "ELEMENT NOT FOUND"); const fromAsset = $('fromAsset').value; const toAsset = $('toAsset').value; const dollarAmount = parseFloat(amountInput ? amountInput.value : "0") || 0; const transferType = $('conversionType').value; // Get selected income row data if applicable let selectedIncomeData = null; if (transferType === 'income') { const incomeSelect = $('incomeRowSelection'); if (incomeSelect && incomeSelect.value) { try { selectedIncomeData = JSON.parse(incomeSelect.value); console.log("Selected income data:", selectedIncomeData); } catch (e) { console.error("Error parsing selected income data:", e); } } // If income transfer is selected but no income row is selected, show an error if (!selectedIncomeData && document.getElementById('incomeRowSelectionDiv').style.display !== 'none') { alert("Please select an income row to use for the transfer."); return; } } console.log("Transfer values:", {fromAsset, toAsset, dollarAmount, transferType, selectedIncomeData}); console.log("Dollar amount is a number?", !isNaN(dollarAmount)); console.log("Dollar amount > 0?", dollarAmount > 0); if (fromAsset && toAsset && dollarAmount > 0) { console.log("All validation passed, calling recordAssetTransfer"); try { recordAssetTransfer(fromAsset, toAsset, dollarAmount, transferType, selectedIncomeData); // Reset the amount field after successful conversion $('conversionAmount').value = ''; // Update dropdowns after conversion in case names have changed updateAssetDropdowns(); } catch (error) { console.error("Error during asset transfer:", error); alert("Error during transfer: " + error.message); } } else { alert("Please fill in all fields with valid values."); console.warn("Invalid form values:", {fromAsset, toAsset, dollarAmount}); } }); } else { console.error("Could not find executeConversion button"); } // Load backup list when page loads loadBackupList(); // Enable fullscreen toggle for each chart canvas ["investedChart", "incomeChart"].forEach(id => { // Removed "dailyYieldChart" from this array const chartCanvas = document.getElementById(id); if (!chartCanvas) return; chartCanvas.style.cursor = "pointer"; chartCanvas.addEventListener("click", () => { chartCanvas.classList.toggle("chart-fullscreen"); chartCanvas.style.cursor = chartCanvas.classList.contains("chart-fullscreen") ? "zoom-out" : "pointer"; }); }); updateAnalysisTable(); // Populate asset dropdown menus updateAssetDropdowns(); // Set up conversion button - this was moved to the DOMContentLoaded event handler // to ensure proper initialization and avoid duplicate event listeners // Also add these calls to update dropdowns when trackers change const addCoinTrackerOriginal = addCoinTracker; addCoinTracker = function(data) { const result = addCoinTrackerOriginal(data); if (typeof updateAssetDropdowns === 'function') { updateAssetDropdowns(); } return result; }; const saveCoinTrackersOriginal = saveCoinTrackers; saveCoinTrackers = function() { const result = saveCoinTrackersOriginal(); if (typeof updateAssetDropdowns === 'function') { updateAssetDropdowns(); } return result; }; }); // Expose the updateCoinTrackerSummary function globally window.updateCoinTrackerSummary = updateCoinTrackerSummary; // Expose updateConversionStats globally for use by direct-converter.js window.updateConversionStats = updateConversionStats; // Expose recordAssetTransfer globally for use by direct-converter.js window.recordAssetTransfer = recordAssetTransfer; // Helper function to update all trackers const updateAllTrackers = () => { const container = $('coinTrackerContainer'); if (!container) return; const trackerDivs = container.getElementsByClassName("coin-tracker"); Array.from(trackerDivs).forEach(div => { updateCoinTrackerSummary(div.id); }); }; // Expose the updateAllTrackers function globally window.updateAllTrackers = updateAllTrackers; // Export coin trackers to CSV file with notes included const exportCoinTrackers = () => { console.log("Exporting coin trackers to CSV..."); const container = $('coinTrackerContainer'); const trackerDivs = container.getElementsByClassName("coin-tracker"); // CSV header let csvContent = "Coin,Platform,Date,Time,Current Value ($),Income ($),Compound ($),Daily Yield (%),Notes\n"; // Collect data from all active trackers Array.from(trackerDivs).forEach(div => { // Skip inactive trackers if (div.dataset.active === "false") return; // Get coin name and platform const headerDiv = div.querySelector("div.coin-tracker-header"); const coinNameInput = headerDiv.querySelector("input.coin-name-input"); const coinName = coinNameInput ? coinNameInput.value.replace(/,/g, " ") : "Unnamed"; const platformInput = headerDiv.querySelector("input[list='platformListDatalist']"); const platform = platformInput ? platformInput.value.replace(/,/g, " ") : ""; // Get table data const table = div.querySelector("table.coin-table"); if (!table) return; const tbody = table.querySelector("tbody"); Array.from(tbody.rows).forEach(tr => { const date = tr.cells[0].querySelector("input").value; const time = tr.cells[1].querySelector("input").value; const currentValue = parseFloat(tr.cells[2].querySelector("input").value.replace("$", "")) || 0; const income = parseFloat(tr.cells[3].querySelector("input").value) || 0; const compound = parseFloat(tr.cells[4].querySelector("input").value) || 0; const dailyYield = parseFloat(tr.cells[5].querySelector("input").value) || 0; // Get notes (added to fix issue with notes not being included) const notesCell = tr.cells[7]; // Notes is the 8th cell (index 7) const notesInput = notesCell ? notesCell.querySelector("input") : null; const notes = notesInput ? notesInput.value.replace(/,/g, " ").replace(/\n/g, " ") : ""; // Add row to CSV csvContent += `"${coinName}","${platform}","${date}","${time}",${currentValue.toFixed(2)},${income.toFixed(2)},${compound.toFixed(2)},${dailyYield.toFixed(2)},"${notes}"\n`; }); }); // Create a download link for the CSV const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement("a"); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); link.setAttribute("href", URL.createObjectURL(blob)); link.setAttribute("download", `coin-trackers-export-${timestamp}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); console.log("Export completed"); }; // Expose the exportCoinTrackers function globally window.exportCoinTrackers = exportCoinTrackers; // Fallback event listener setup for analysis inputs console.log("πŸ”₯ Setting up fallback event listeners..."); setTimeout(() => { console.log("πŸ”₯ Fallback: Setting up analysis event listeners..."); const reinvestRateEl = document.getElementById('analysisReinvestRate'); const analysisDaysEl = document.getElementById('analysisDays'); console.log("πŸ”₯ Fallback: Analysis elements found:", !!reinvestRateEl, !!analysisDaysEl); if (reinvestRateEl) { console.log("πŸ”₯ Fallback: Adding event listeners to reinvest rate element"); const inputHandler = () => { console.log("πŸ”₯ Reinvest rate INPUT event (fallback) - new value:", reinvestRateEl.value); updateAnalysisTable(); }; reinvestRateEl.addEventListener("input", inputHandler); reinvestRateEl.addEventListener("change", inputHandler); console.log("πŸ”₯ Fallback: Event listeners attached to reinvest rate element"); } if (analysisDaysEl) { console.log("πŸ”₯ Fallback: Adding event listeners to analysis days element"); const inputHandler = () => { console.log("πŸ”₯ Analysis days INPUT event (fallback) - new value:", analysisDaysEl.value); updateAnalysisTable(); }; analysisDaysEl.addEventListener("input", inputHandler); analysisDaysEl.addEventListener("change", inputHandler); console.log("πŸ”₯ Fallback: Event listeners attached to analysis days element"); } }, 1000); // Wait 1 second to ensure DOM is ready })();