Files
kidsai/html/rechner/script.js
2025-06-24 15:43:32 +02:00

2891 lines
113 KiB
JavaScript

"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 += `<tr>
<td>${row.day}</td>
<td>${row.date}</td>
<td>${fmt(row.earnings)}</td>
<td>${row.reinvestRate}%</td>
<td>${principalCashCell}</td>
<td>${fmt(row.totalPrincipal)}</td>
<td>${fmt(row.totalCash)}</td>
</tr>`;
});
$('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 = `<tr>
<th>Date</th>
<th>Time</th>
<th>Current Value</th>
<th>Income</th>
<th>Compound</th>
<th>Daily Yield (%)</th>
<th>Actions</th>
<th>Notes</th>
</tr>`;
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 = `<small>24h: $${projection24h.toFixed(2)} | 7d: $${projection7d.toFixed(2)} | 30d: $${projection30d.toFixed(2)}</small>`;
} 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 = '<option value="">-- Select --</option>';
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 += `<tr>
<td>${i + 1}</td>
<td>${dateStr}</td>
<td>$${earnings.toFixed(2)}</td>
<td>${reinvestRate}%</td>
<td>$${reinvestment.toFixed(2)} / $${cashout.toFixed(2)}</td>
<td>$${runningPrincipal.toFixed(2)}</td>
<td>$${cumulativeCash.toFixed(2)}</td>
</tr>`;
}
// 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 = `
<div class="backup-time">${time}</div>
<div class="backup-name">${backupName}</div>
<div class="backup-size">${size}</div>
`;
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 = `
<p><strong>Date:</strong> ${new Date(backup.date).toLocaleString()}</p>
<p><strong>Name:</strong> ${backupName}</p>
<p><strong>Size:</strong> ${size}</p>
<p><strong>Filename:</strong> ${backup.file}</p>
`;
// Store the selected backup filename
$('restoreBackupBtn').dataset.file = backup.file;
});
backupCalendar.appendChild(backupCard);
});
});
// Show the backup calendar
$('backupCalendarContainer').style.display = 'block';
} else {
backupCalendar.innerHTML = '<p>No backups available.</p>';
$('backupCalendarContainer').style.display = 'block';
}
})
.catch(error => {
console.error('Error loading backups:', error);
$('backupCalendar').innerHTML = '<p>Failed to load backups. See console for details.</p>';
$('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 = '<option value="">-- Select Source --</option>';
toSelect.innerHTML = '<option value="">-- Select Destination --</option>';
// 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 = '<option value="">-- Select Income Row --</option>';
// 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
})();