Files
kidsai/html/rechner/script.js
root 7ec360c5d1 Implement proper deposit tracking for coin trackers
- Added deposit/withdrawal history section to each coin tracker
- Created addDepositEntry function for managing deposits
- Fixed updateInvestedChart to use actual deposit history instead of flawed logic
- Total Deposited Value now reflects actual money invested, not market performance
- Portfolio Value and Total Deposited Value are now properly separated
- Supports migration of existing Initial Capital as first deposit
2025-09-19 15:01:23 +02:00

3107 lines
121 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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('/rechner/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 deposit tracking section
const depositSectionDiv = document.createElement("div");
depositSectionDiv.style.marginBottom = "10px";
depositSectionDiv.style.padding = "10px";
depositSectionDiv.style.border = "1px solid #444";
depositSectionDiv.style.borderRadius = "5px";
depositSectionDiv.style.backgroundColor = "#2a2a2a";
const depositSectionLabel = document.createElement("h4");
depositSectionLabel.textContent = "Deposit/Withdrawal History";
depositSectionLabel.style.marginTop = "0";
depositSectionLabel.style.marginBottom = "10px";
depositSectionDiv.appendChild(depositSectionLabel);
// Deposit list container
const depositListDiv = document.createElement("div");
depositListDiv.className = "deposit-list";
depositSectionDiv.appendChild(depositListDiv);
// Add deposit controls
const addDepositDiv = document.createElement("div");
addDepositDiv.style.marginTop = "10px";
const depositDateInput = document.createElement("input");
depositDateInput.type = "date";
depositDateInput.value = getToday();
depositDateInput.style.marginRight = "5px";
const depositAmountInput = document.createElement("input");
depositAmountInput.type = "number";
depositAmountInput.step = "0.01";
depositAmountInput.placeholder = "Amount (+/-)";
depositAmountInput.style.marginRight = "5px";
depositAmountInput.style.width = "120px";
const depositNotesInput = document.createElement("input");
depositNotesInput.type = "text";
depositNotesInput.placeholder = "Notes (optional)";
depositNotesInput.style.marginRight = "5px";
depositNotesInput.style.width = "150px";
const addDepositBtn = document.createElement("button");
addDepositBtn.textContent = "Add Deposit/Withdrawal";
addDepositBtn.style.fontSize = "12px";
addDepositBtn.addEventListener("click", () => {
if (depositAmountInput.value && depositDateInput.value) {
addDepositEntry(depositListDiv, {
date: depositDateInput.value,
amount: parseFloat(depositAmountInput.value),
notes: depositNotesInput.value || ""
}, trackerId);
depositAmountInput.value = "";
depositNotesInput.value = "";
saveCoinTrackers();
updateInvestedChart();
}
});
addDepositDiv.appendChild(depositDateInput);
addDepositDiv.appendChild(depositAmountInput);
addDepositDiv.appendChild(depositNotesInput);
addDepositDiv.appendChild(addDepositBtn);
depositSectionDiv.appendChild(addDepositDiv);
contentDiv.appendChild(depositSectionDiv);
// Load existing deposits if available
if (data && data.deposits && data.deposits.length > 0) {
data.deposits.forEach(deposit => {
addDepositEntry(depositListDiv, deposit, trackerId);
});
} else if (data && data.initialCapital && parseFloat(data.initialCapital) > 0) {
// Migrate existing initial capital as first deposit
addDepositEntry(depositListDiv, {
date: "2025-01-01", // Default date for migrated data
amount: parseFloat(data.initialCapital),
notes: "Migrated from Initial Capital"
}, trackerId);
}
// 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);
};
// Function to add deposit/withdrawal entries to the deposit history
const addDepositEntry = (depositListDiv, depositData, trackerId) => {
const depositDiv = document.createElement("div");
depositDiv.style.display = "flex";
depositDiv.style.alignItems = "center";
depositDiv.style.marginBottom = "5px";
depositDiv.style.padding = "5px";
depositDiv.style.backgroundColor = "#3a3a3a";
depositDiv.style.borderRadius = "3px";
const dateSpan = document.createElement("span");
dateSpan.textContent = depositData.date;
dateSpan.style.minWidth = "100px";
dateSpan.style.marginRight = "10px";
const amountSpan = document.createElement("span");
const amount = parseFloat(depositData.amount);
amountSpan.textContent = amount >= 0 ? `+$${amount.toFixed(2)}` : `-$${Math.abs(amount).toFixed(2)}`;
amountSpan.style.minWidth = "80px";
amountSpan.style.marginRight = "10px";
amountSpan.style.color = amount >= 0 ? "#4CAF50" : "#f44336";
amountSpan.style.fontWeight = "bold";
const notesSpan = document.createElement("span");
notesSpan.textContent = depositData.notes || "";
notesSpan.style.flex = "1";
notesSpan.style.marginRight = "10px";
notesSpan.style.fontStyle = "italic";
notesSpan.style.color = "#ccc";
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "×";
deleteBtn.style.background = "#f44336";
deleteBtn.style.color = "white";
deleteBtn.style.border = "none";
deleteBtn.style.borderRadius = "50%";
deleteBtn.style.width = "20px";
deleteBtn.style.height = "20px";
deleteBtn.style.fontSize = "12px";
deleteBtn.style.cursor = "pointer";
deleteBtn.title = "Delete deposit";
deleteBtn.addEventListener("click", () => {
if (confirm("Are you sure you want to delete this deposit record?")) {
depositDiv.remove();
saveCoinTrackers();
updateInvestedChart();
}
});
depositDiv.appendChild(dateSpan);
depositDiv.appendChild(amountSpan);
depositDiv.appendChild(notesSpan);
depositDiv.appendChild(deleteBtn);
// Insert deposits in chronological order
let inserted = false;
const existingDeposits = depositListDiv.children;
for (let i = 0; i < existingDeposits.length; i++) {
const existingDate = existingDeposits[i].querySelector('span').textContent;
if (depositData.date < existingDate) {
depositListDiv.insertBefore(depositDiv, existingDeposits[i]);
inserted = true;
break;
}
}
if (!inserted) {
depositListDiv.appendChild(depositDiv);
}
};
// 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
// Collect all deposit data from all trackers
let allDeposits = {};
Array.from(trackerDivs).forEach(div => {
if (div.dataset.showInChart === "false") return;
// Collect deposits for this tracker
const depositListDiv = div.querySelector(".deposit-list");
if (depositListDiv) {
Array.from(depositListDiv.children).forEach(depositDiv => {
const dateSpan = depositDiv.querySelector('span:nth-child(1)');
const amountSpan = depositDiv.querySelector('span:nth-child(2)');
if (dateSpan && amountSpan) {
const date = dateSpan.textContent;
const amountText = amountSpan.textContent;
const amount = parseFloat(amountText.replace(/[+\-$]/g, '')) * (amountText.startsWith('-') ? -1 : 1);
if (!allDeposits[date]) {
allDeposits[date] = 0;
}
allDeposits[date] += amount;
}
});
}
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;
}
});
for (const date in trackerDaily) {
currentTotals[date] = (currentTotals[date] || 0) + trackerDaily[date].current;
}
}
});
// Calculate cumulative deposits over time
const sortedDepositDates = Object.keys(allDeposits).sort();
let cumulativeDeposits = 0;
const allDates = new Set([...Object.keys(portfolioValues), ...sortedDepositDates]);
Array.from(allDates).sort().forEach(date => {
// Add any deposits that occurred on this date
if (allDeposits[date]) {
cumulativeDeposits += allDeposits[date];
}
// Set total deposited for this date to the cumulative amount
totalDeposited[date] = cumulativeDeposits;
});
// 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] || 0);
// 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";
// Get deposit history
const depositListDiv = div.querySelector(".deposit-list");
const deposits = [];
if (depositListDiv) {
Array.from(depositListDiv.children).forEach(depositDiv => {
const dateSpan = depositDiv.querySelector('span:nth-child(1)');
const amountSpan = depositDiv.querySelector('span:nth-child(2)');
const notesSpan = depositDiv.querySelector('span:nth-child(3)');
if (dateSpan && amountSpan) {
const date = dateSpan.textContent;
const amountText = amountSpan.textContent;
// Parse amount from "+$123.45" or "-$123.45" format
const amount = parseFloat(amountText.replace(/[+\-$]/g, '')) * (amountText.startsWith('-') ? -1 : 1);
const notes = notesSpan ? notesSpan.textContent : "";
deposits.push({ date, amount, notes });
}
});
}
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 || deposits.length > 0) {
trackers.push({ id: trackerId, coinName, platform, initialCapital, deposits, rows, showInChart, active });
}
});
updateCoinTrackersToBackend(trackers);
};
// Load coin trackers exclusively from the backend.
const loadCoinTrackers = () => {
console.log("Loading trackers from backend...");
fetch('/rechner/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");
// FIXED: Use main calculator inputs first, fallback to analysis settings if needed
const mainInitialPrincipal = parseFloat($('initialPrincipal')?.value) || 0;
const mainDailyRate = parseFloat($('dailyRate')?.value) || 0;
const mainTermDays = parseInt($('termDays')?.value) || 0;
const mainReinvestRate = parseFloat($('reinvestRate')?.value) || 0;
// Get analysis settings as fallback/override
const reinvestRateElement = document.getElementById('analysisReinvestRate');
const analysisDaysElement = document.getElementById('analysisDays');
console.log("Elements found:", !!reinvestRateElement, !!analysisDaysElement);
console.log("Main calculator values:", {mainInitialPrincipal, mainDailyRate, mainTermDays, mainReinvestRate});
// Use main calculator values, but allow analysis settings to override if they have non-zero values
let reinvestRate = mainReinvestRate;
let days = mainTermDays;
let initialPrincipal = mainInitialPrincipal;
let dailyRate = mainDailyRate;
// Override with analysis settings ONLY if they have meaningful values different from defaults
if (reinvestRateElement && reinvestRateElement.value !== "" && reinvestRateElement.value !== "0") {
const analysisReinvestRate = parseFloat(reinvestRateElement.value);
if (analysisReinvestRate > 0) {
reinvestRate = analysisReinvestRate;
}
}
if (analysisDaysElement && analysisDaysElement.value !== "" && analysisDaysElement.value !== "30") {
const analysisDays = parseInt(analysisDaysElement.value);
if (analysisDays > 0 && analysisDays !== 30) {
days = analysisDays;
}
}
console.log("Parsed values - Reinvest Rate:", reinvestRate, "Days:", days);
console.log("Using values - Initial Principal:", initialPrincipal, "Daily Rate:", dailyRate);
// If we have main calculator values, use those for initial investment and yield
// Otherwise fall back to coin tracker data
let totalInvestment = initialPrincipal;
let weightedAverageDailyYield = dailyRate;
// Only use coin tracker data if main calculator has no values
if (totalInvestment === 0 || weightedAverageDailyYield === 0) {
// Get data from active coin trackers
const container = $('coinTrackerContainer');
const trackerDivs = container.getElementsByClassName("coin-tracker");
// Initialize total investment and weighted yield
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
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(`/rechner/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('/rechner/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(`/rechner/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
})();