2891 lines
113 KiB
JavaScript
2891 lines
113 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
// Helper functions
|
|
const $ = id => {
|
|
const element = document.getElementById(id);
|
|
if (!element && console && console.warn) {
|
|
console.warn(`Element with ID '${id}' not found`);
|
|
}
|
|
return element;
|
|
};
|
|
|
|
const adjustValue = (id, delta) => {
|
|
const input = $(id);
|
|
if (!input) return;
|
|
const currentValue = parseFloat(input.value) || 0;
|
|
const step = parseFloat(input.getAttribute('data-step')) || 1;
|
|
input.value = currentValue + delta * step;
|
|
calculate();
|
|
};
|
|
|
|
// Register Chart.js plugins
|
|
Chart.register(ChartDataLabels);
|
|
|
|
let chart;
|
|
let investedChart;
|
|
let incomeChart; // Global variable for income chart
|
|
let coinTrackerCounter = 0;
|
|
let investedHistory = { labels: [], data: [] };
|
|
let dailyYieldChart; // global reference
|
|
|
|
// Update coin trackers on the backend with the given data
|
|
const updateCoinTrackersToBackend = (trackers) => {
|
|
console.log("Saving trackers to backend:", trackers);
|
|
|
|
fetch('/api/coinTrackers', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ data: trackers })
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log("Trackers saved successfully:", data);
|
|
})
|
|
.catch(error => {
|
|
console.error("Error saving trackers:", error);
|
|
});
|
|
};
|
|
|
|
// Main calculation for the compounding interest chart, left panel summary, and analysis table
|
|
const calculate = () => {
|
|
const initialPrincipal = parseFloat($('initialPrincipal').value) || 0;
|
|
const dailyRate = parseFloat($('dailyRate').value) || 0;
|
|
const termDays = parseInt($('termDays').value, 10) || 0;
|
|
const reinvestRate = parseFloat($('reinvestRate').value) || 0;
|
|
|
|
const labels = [];
|
|
const reinvestArray = [];
|
|
const cashArray = [];
|
|
const earningsDaily = [];
|
|
const tableRows = [];
|
|
let runningPrincipal = initialPrincipal;
|
|
let cumulativeCash = 0;
|
|
let currentDate = new Date();
|
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
|
|
for (let i = 0; i < termDays; i++) {
|
|
const dateStr = `${currentDate.getMonth()+1}/${currentDate.getDate()}/${currentDate.getFullYear()}`;
|
|
labels.push(`Day ${i + 1}`);
|
|
const earnings = runningPrincipal * (dailyRate / 100);
|
|
const reinvestment = earnings * (reinvestRate / 100);
|
|
const cashFlow = earnings - reinvestment;
|
|
reinvestArray.push(Number(reinvestment.toFixed(2)));
|
|
cashArray.push(Number(cashFlow.toFixed(2)));
|
|
earningsDaily.push(Number(earnings.toFixed(2)));
|
|
runningPrincipal += reinvestment;
|
|
cumulativeCash += cashFlow;
|
|
tableRows.push({
|
|
day: i + 1,
|
|
date: dateStr,
|
|
earnings: earnings,
|
|
reinvestment: reinvestment,
|
|
cashFlow: cashFlow,
|
|
totalPrincipal: runningPrincipal,
|
|
totalCash: cumulativeCash,
|
|
reinvestRate: reinvestRate
|
|
});
|
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
}
|
|
|
|
// Update left panel summary
|
|
$('sumInvestment').innerText = `Initial Investment: $${initialPrincipal.toFixed(2)}`;
|
|
$('sumRate').innerText = `Interest Rate: ${dailyRate.toFixed(2)}%`;
|
|
$('sumDays').innerText = `Days: ${termDays}`;
|
|
$('sumInterest').innerText = `Interest Earned: $${((runningPrincipal - initialPrincipal) + cumulativeCash).toFixed(2)}`;
|
|
$('sumNewBalance').innerText = `New Balance: $${runningPrincipal.toFixed(2)}`;
|
|
$('sumCash').innerText = `Cash Withdrawals: $${cumulativeCash.toFixed(2)}`;
|
|
|
|
// Draw compounding interest chart
|
|
const ctx = $('interestChart').getContext('2d');
|
|
if (chart) { chart.destroy(); }
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
|
|
gradient.addColorStop(0, 'rgba(0,123,255,0.8)');
|
|
gradient.addColorStop(1, 'rgba(0,123,255,0.2)');
|
|
chart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: 'Re-investment',
|
|
data: reinvestArray,
|
|
backgroundColor: '#fd7e14',
|
|
stack: 'combined',
|
|
datalabels: {
|
|
anchor: 'center',
|
|
align: 'center',
|
|
formatter: (value, context) => reinvestArray[context.dataIndex].toFixed(2),
|
|
color: '#e0e0e0',
|
|
font: { weight: 'bold', size: 14 }
|
|
}
|
|
},
|
|
{
|
|
label: 'Cash Flow',
|
|
data: cashArray,
|
|
backgroundColor: '#28a745',
|
|
stack: 'combined',
|
|
datalabels: {
|
|
labels: {
|
|
cashFlowLabel: {
|
|
anchor: 'center',
|
|
align: 'center',
|
|
formatter: (value, context) => cashArray[context.dataIndex].toFixed(2),
|
|
color: '#e0e0e0',
|
|
font: { weight: 'bold', size: 14 }
|
|
},
|
|
totalEarningsLabel: {
|
|
anchor: 'end',
|
|
align: 'end',
|
|
offset: 10,
|
|
formatter: (value, context) => earningsDaily[context.dataIndex].toFixed(2),
|
|
color: '#e0e0e0',
|
|
font: { weight: 'bold', size: 14 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
animation: { duration: 0 },
|
|
scales: {
|
|
x: { stacked: true, grid: { display: false }, ticks: { color: '#e0e0e0' } },
|
|
y: { stacked: true, grid: { color: '#555' }, ticks: { color: '#e0e0e0' } }
|
|
},
|
|
plugins: {
|
|
legend: { display: true, labels: { color: '#e0e0e0' } },
|
|
tooltip: {
|
|
backgroundColor: '#333',
|
|
titleColor: '#e0e0e0',
|
|
bodyColor: '#e0e0e0',
|
|
borderColor: '#007bff',
|
|
borderWidth: 1,
|
|
cornerRadius: 4,
|
|
padding: 10
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Populate the Analysis Table for Compounding Interest
|
|
let tableHTML = '';
|
|
const fmt = num => '$' + num.toFixed(2);
|
|
tableRows.forEach(row => {
|
|
const principalCashCell = `${fmt(row.reinvestment)} / ${fmt(row.cashFlow)}`;
|
|
tableHTML += `<tr>
|
|
<td>${row.day}</td>
|
|
<td>${row.date}</td>
|
|
<td>${fmt(row.earnings)}</td>
|
|
<td>${row.reinvestRate}%</td>
|
|
<td>${principalCashCell}</td>
|
|
<td>${fmt(row.totalPrincipal)}</td>
|
|
<td>${fmt(row.totalCash)}</td>
|
|
</tr>`;
|
|
});
|
|
$('analysisTable').querySelector('tbody').innerHTML = tableHTML;
|
|
|
|
updateTotalInvested();
|
|
};
|
|
|
|
// Percentage Calculators
|
|
const calculatePercentage = () => {
|
|
const total = parseFloat($('totalAmountPercent').value) || 0;
|
|
const given = parseFloat($('givenAmount').value) || 0;
|
|
if (total > 0) {
|
|
const perc = (given / total) * 100;
|
|
$('percentageResult').innerText = `${given.toFixed(2)} is ${perc.toFixed(2)}% of ${total.toFixed(2)}`;
|
|
} else {
|
|
$('percentageResult').innerText = "Enter a valid total amount";
|
|
}
|
|
};
|
|
|
|
const calculatePercentage2 = () => {
|
|
const total2 = parseFloat($('totalAmountPercent2').value) || 0;
|
|
const perc2 = parseFloat($('percentInput2').value) || 0;
|
|
if (total2 > 0) {
|
|
const dollarValue = (perc2 / 100) * total2;
|
|
$('percentageResult2').innerText = `${perc2}% of $${total2.toFixed(2)} = $${dollarValue.toFixed(2)}`;
|
|
} else {
|
|
$('percentageResult2').innerText = "Enter a valid total amount";
|
|
}
|
|
};
|
|
|
|
// Enhanced setupCollapsibles with proper support for independent sections:
|
|
const setupCollapsibles = () => {
|
|
document.querySelectorAll('.collapsible-header').forEach(header => {
|
|
header.addEventListener("click", () => {
|
|
const body = header.nextElementSibling;
|
|
if (body) {
|
|
// Toggle display manually
|
|
body.style.display = (body.style.display === "none" || body.style.display === "") ? "block" : "none";
|
|
|
|
// Update aria-expanded attribute for accessibility
|
|
const expanded = body.style.display === "block";
|
|
header.setAttribute('aria-expanded', expanded.toString());
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
// Date/Time helpers
|
|
const getToday = () => new Date().toISOString().split('T')[0];
|
|
const getBerlinTime = () => {
|
|
const options = { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Europe/Berlin' };
|
|
return new Date().toLocaleTimeString('en-GB', options);
|
|
};
|
|
|
|
const sortCoinTrackerRows = trackerDiv => {
|
|
const table = trackerDiv.querySelector("table.coin-table");
|
|
const tbody = table.querySelector("tbody");
|
|
const rows = Array.from(tbody.rows);
|
|
rows.sort((a, b) => {
|
|
const dateA = new Date(a.cells[0].querySelector("input").value);
|
|
const dateB = new Date(b.cells[0].querySelector("input").value);
|
|
return dateA - dateB;
|
|
});
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerDiv.id);
|
|
};
|
|
|
|
// Create coin tracker element with a toggle for chart display.
|
|
// Persist the hide/show state by using a "showInChart" property.
|
|
const addCoinTracker = data => {
|
|
let trackerId;
|
|
if (data && data.id) {
|
|
trackerId = data.id;
|
|
const num = parseInt(trackerId.replace("coinTracker", ""));
|
|
if (!isNaN(num) && num > coinTrackerCounter) {
|
|
coinTrackerCounter = num;
|
|
}
|
|
} else {
|
|
coinTrackerCounter++;
|
|
trackerId = `coinTracker${coinTrackerCounter}`;
|
|
}
|
|
const trackerDiv = document.createElement("div");
|
|
trackerDiv.className = "coin-tracker";
|
|
trackerDiv.id = trackerId;
|
|
|
|
// Set persistent show/hide state if provided, otherwise default to "true"
|
|
if (data && data.showInChart !== undefined) {
|
|
trackerDiv.dataset.showInChart = data.showInChart;
|
|
} else {
|
|
trackerDiv.dataset.showInChart = "true";
|
|
}
|
|
|
|
// Set active state from loaded data or default to "true
|
|
if (data && data.active !== undefined) {
|
|
trackerDiv.dataset.active = data.active;
|
|
} else {
|
|
trackerDiv.dataset.active = "true";
|
|
}
|
|
|
|
// Enable manual sorting by default
|
|
trackerDiv.dataset.manualSort = "true";
|
|
|
|
const headerDiv = document.createElement("div");
|
|
headerDiv.classList.add("coin-tracker-header");
|
|
|
|
const coinNameInput = document.createElement("input");
|
|
coinNameInput.type = "text";
|
|
coinNameInput.value = data && data.coinName ? data.coinName : "Unnamed Coin";
|
|
coinNameInput.classList.add("coin-name-input");
|
|
coinNameInput.placeholder = "Coin/Coin pair";
|
|
coinNameInput.addEventListener("click", e => e.stopPropagation());
|
|
coinNameInput.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
updateTransferDropdown();
|
|
});
|
|
headerDiv.appendChild(coinNameInput);
|
|
|
|
const platformInput = document.createElement("input");
|
|
platformInput.type = "text";
|
|
platformInput.setAttribute("list", "platformListDatalist"); // <-- add this
|
|
platformInput.placeholder = "Platform";
|
|
platformInput.value = data && data.platform ? data.platform : "";
|
|
platformInput.addEventListener("change", () => {
|
|
// Could save the platform if desired
|
|
saveCoinTrackers();
|
|
updatePlatformDatalist(platformInput.value);
|
|
});
|
|
headerDiv.appendChild(platformInput);
|
|
|
|
const lastValueSpan = document.createElement("span");
|
|
lastValueSpan.className = "last-value";
|
|
lastValueSpan.textContent = "Current Value: $0.00"; // Add label "Current Value: "
|
|
lastValueSpan.style.marginLeft = "10px"; // adjust spacing as needed
|
|
headerDiv.appendChild(lastValueSpan);
|
|
|
|
// Add new span for total income
|
|
const totalIncomeSpan = document.createElement("span");
|
|
totalIncomeSpan.className = "total-income";
|
|
totalIncomeSpan.textContent = "Total Income: $0.00";
|
|
totalIncomeSpan.style.marginLeft = "10px";
|
|
headerDiv.appendChild(totalIncomeSpan);
|
|
|
|
const pnlDiv = document.createElement("div");
|
|
pnlDiv.className = "pnl-div";
|
|
pnlDiv.style.marginLeft = "10px";
|
|
|
|
const pnlCurrentValueSpan = document.createElement("span");
|
|
pnlCurrentValueSpan.className = "pnl-current-value";
|
|
pnlCurrentValueSpan.textContent = "PnL (Current Value): $0.00";
|
|
pnlDiv.appendChild(pnlCurrentValueSpan);
|
|
|
|
const pnlTotalValueSpan = document.createElement("span");
|
|
pnlTotalValueSpan.className = "pnl-total-value";
|
|
pnlTotalValueSpan.textContent = "PnL (Total Value): $0.00";
|
|
pnlDiv.appendChild(pnlTotalValueSpan);
|
|
|
|
headerDiv.appendChild(pnlDiv);
|
|
|
|
const interestSpan = document.createElement("span");
|
|
interestSpan.className = "tracker-interest";
|
|
headerDiv.appendChild(interestSpan);
|
|
|
|
const deleteTrackerBtn = document.createElement("button");
|
|
deleteTrackerBtn.textContent = "Delete Tracker";
|
|
deleteTrackerBtn.className = "deleteTracker";
|
|
deleteTrackerBtn.addEventListener("click", e => {
|
|
e.stopPropagation();
|
|
if (confirm("Are you sure you want to delete this coin tracker?")) {
|
|
trackerDiv.remove();
|
|
saveCoinTrackers();
|
|
updateTransferDropdown();
|
|
}
|
|
});
|
|
headerDiv.appendChild(deleteTrackerBtn);
|
|
|
|
// Toggle button for chart display with persistent state
|
|
const toggleChartBtn = document.createElement("button");
|
|
toggleChartBtn.style.marginLeft = "10px";
|
|
toggleChartBtn.textContent = trackerDiv.dataset.showInChart === "true" ? "Hide from Chart" : "Show in Chart";
|
|
toggleChartBtn.addEventListener("click", () => {
|
|
if (trackerDiv.dataset.showInChart === "false") {
|
|
trackerDiv.dataset.showInChart = "true";
|
|
toggleChartBtn.textContent = "Hide from Chart";
|
|
} else {
|
|
trackerDiv.dataset.showInChart = "false";
|
|
toggleChartBtn.textContent = "Show in Chart";
|
|
}
|
|
updateInvestedChart();
|
|
saveCoinTrackers(); // persist the new state
|
|
});
|
|
headerDiv.appendChild(toggleChartBtn);
|
|
|
|
const toggleActiveBtn = document.createElement("button");
|
|
toggleActiveBtn.style.marginLeft = "10px";
|
|
toggleActiveBtn.textContent = trackerDiv.dataset.active === "false" ? "Activate" : "Deactivate";
|
|
toggleActiveBtn.addEventListener("click", () => {
|
|
if (trackerDiv.dataset.active === "false") {
|
|
trackerDiv.dataset.active = "true";
|
|
toggleActiveBtn.textContent = "Deactivate";
|
|
} else {
|
|
trackerDiv.dataset.active = "false";
|
|
toggleActiveBtn.textContent = "Activate";
|
|
}
|
|
updateTotalInvested();
|
|
updateIncomeChart();
|
|
saveCoinTrackers();
|
|
});
|
|
headerDiv.appendChild(toggleActiveBtn);
|
|
|
|
headerDiv.addEventListener("click", e => {
|
|
if (!["INPUT", "BUTTON", "SPAN"].includes(e.target.tagName)) {
|
|
const contentDiv = trackerDiv.querySelector(".content");
|
|
contentDiv.style.display = (contentDiv.style.display === "none") ? "block" : "none";
|
|
}
|
|
});
|
|
trackerDiv.appendChild(headerDiv);
|
|
|
|
const contentDiv = document.createElement("div");
|
|
contentDiv.className = "content";
|
|
contentDiv.style.display = "none";
|
|
|
|
// Create initialCapitalDiv
|
|
const initialCapitalDiv = document.createElement("div");
|
|
initialCapitalDiv.style.marginBottom = "10px";
|
|
const initialCapitalLabel = document.createElement("label");
|
|
initialCapitalLabel.textContent = 'Initial Capital: ';
|
|
const initialCapitalInput = document.createElement("input");
|
|
initialCapitalInput.type = "number";
|
|
initialCapitalInput.step = "0.01";
|
|
initialCapitalInput.value = data && data.initialCapital ? data.initialCapital : "0.00";
|
|
initialCapitalDiv.appendChild(initialCapitalLabel);
|
|
initialCapitalDiv.appendChild(initialCapitalInput);
|
|
contentDiv.appendChild(initialCapitalDiv);
|
|
|
|
// Create a container with positioning context for the table
|
|
const tableWrapper = document.createElement("div");
|
|
tableWrapper.className = "table-wrapper";
|
|
tableWrapper.style.position = "relative";
|
|
|
|
const table = document.createElement("table");
|
|
table.className = "income-table coin-table";
|
|
const thead = document.createElement("thead");
|
|
thead.innerHTML = `<tr>
|
|
<th>Date</th>
|
|
<th>Time</th>
|
|
<th>Current Value</th>
|
|
<th>Income</th>
|
|
<th>Compound</th>
|
|
<th>Daily Yield (%)</th>
|
|
<th>Actions</th>
|
|
<th>Notes</th>
|
|
</tr>`;
|
|
table.appendChild(thead);
|
|
const tbody = document.createElement("tbody");
|
|
table.appendChild(tbody);
|
|
|
|
if (data && data.rows && data.rows.length > 0) {
|
|
data.rows.forEach(row => { addDynamicEntry(tbody, row, trackerId); });
|
|
}
|
|
|
|
// Add the table to the wrapper
|
|
tableWrapper.appendChild(table);
|
|
|
|
// No need for a separate yield projection box anymore
|
|
// We're removing the summaryDiv creation and positioning code
|
|
|
|
tableWrapper.style.marginBottom = "20px";
|
|
|
|
// Add the wrapper to the content div
|
|
contentDiv.appendChild(tableWrapper);
|
|
|
|
const addRowBtn = document.createElement("button");
|
|
addRowBtn.textContent = "Add Row";
|
|
addRowBtn.className = "add-row-btn"; // Add a class for styling
|
|
addRowBtn.style.display = "block"; // Ensure it's a block element
|
|
addRowBtn.style.marginTop = "5px"; // Position right at the edge of the box
|
|
addRowBtn.style.marginBottom = "0"; // Remove any bottom margin
|
|
|
|
addRowBtn.addEventListener("click", () => {
|
|
addDynamicEntry(tbody, { date: getToday(), time: getBerlinTime(), income: "", dailyYield: "", currentValue: 0 }, trackerId);
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
contentDiv.appendChild(addRowBtn);
|
|
|
|
trackerDiv.appendChild(contentDiv);
|
|
$('coinTrackerContainer').appendChild(trackerDiv);
|
|
|
|
// Initialize Sortable on this table by default
|
|
if (window.Sortable) {
|
|
const tbody = table.querySelector("tbody");
|
|
Sortable.create(tbody, {
|
|
animation: 150,
|
|
onEnd: () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateCoinTrackerSummary(trackerId);
|
|
updateTransferDropdown();
|
|
|
|
// Add this line to save the new tracker immediately after adding it
|
|
saveCoinTrackers();
|
|
};
|
|
|
|
// Modified addDynamicEntry: assign a creation timestamp so we can track insertion order
|
|
const addDynamicEntry = (tbody, rowData, trackerId) => {
|
|
const tr = document.createElement("tr");
|
|
// Use existing created value if provided, otherwise use current timestamp
|
|
tr.dataset.created = rowData.created || Date.now();
|
|
|
|
// Store transaction ID if provided (for linked entries)
|
|
if (rowData.transactionId) {
|
|
tr.dataset.transactionId = rowData.transactionId;
|
|
}
|
|
|
|
// Store transaction type if provided
|
|
if (rowData.transactionType) {
|
|
tr.dataset.transactionType = rowData.transactionType;
|
|
}
|
|
|
|
const tdDate = document.createElement("td");
|
|
const inputDate = document.createElement("input");
|
|
inputDate.type = "date";
|
|
inputDate.value = rowData.date || getToday();
|
|
inputDate.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
const trackerDiv = $(trackerId);
|
|
if (!trackerDiv.dataset.manualSort || trackerDiv.dataset.manualSort === "false") {
|
|
sortCoinTrackerRows(trackerDiv);
|
|
}
|
|
});
|
|
tdDate.appendChild(inputDate);
|
|
tr.appendChild(tdDate);
|
|
|
|
const tdTime = document.createElement("td");
|
|
const inputTime = document.createElement("input");
|
|
inputTime.type = "time";
|
|
inputTime.value = rowData.time || getBerlinTime();
|
|
inputTime.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
tdTime.appendChild(inputTime);
|
|
tr.appendChild(tdTime);
|
|
|
|
const tdCurrent = document.createElement("td");
|
|
tdCurrent.innerHTML = "$";
|
|
const inputCurrent = document.createElement("input");
|
|
inputCurrent.type = "number";
|
|
inputCurrent.step = "0.01";
|
|
inputCurrent.value = rowData.currentValue ? Number(rowData.currentValue).toFixed(2) : "0.00";
|
|
inputCurrent.addEventListener("input", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
inputCurrent.addEventListener("keyup", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
inputCurrent.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
tdCurrent.appendChild(inputCurrent);
|
|
tr.appendChild(tdCurrent);
|
|
|
|
const tdIncome = document.createElement("td");
|
|
const inputIncome = document.createElement("input");
|
|
inputIncome.type = "number";
|
|
inputIncome.step = "0.01";
|
|
inputIncome.value = rowData.income || "";
|
|
inputIncome.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
tdIncome.appendChild(inputIncome);
|
|
tr.appendChild(tdIncome);
|
|
|
|
const tdCompound = document.createElement("td");
|
|
const inputCompound = document.createElement("input");
|
|
inputCompound.type = "number";
|
|
inputCompound.step = "0.01";
|
|
inputCompound.value = rowData.compound || "";
|
|
inputCompound.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
tdCompound.appendChild(inputCompound);
|
|
tr.appendChild(tdCompound);
|
|
|
|
const tdYield = document.createElement("td");
|
|
const inputYield = document.createElement("input");
|
|
inputYield.type = "number";
|
|
inputYield.step = "0.01";
|
|
inputYield.value = rowData.dailyYield || "";
|
|
inputYield.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
updateRowYieldProjections(tr);
|
|
});
|
|
tdYield.appendChild(inputYield);
|
|
|
|
// Add a div to hold the yield projections within the cell
|
|
const projectionDiv = document.createElement("div");
|
|
projectionDiv.className = "inline-yield-projection";
|
|
tdYield.appendChild(projectionDiv);
|
|
|
|
tr.appendChild(tdYield);
|
|
|
|
const tdActions = document.createElement("td");
|
|
const delBtn = document.createElement("button");
|
|
delBtn.textContent = "Delete";
|
|
delBtn.className = "deleteRowBtn";
|
|
delBtn.addEventListener("click", () => {
|
|
tr.remove();
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
tdActions.appendChild(delBtn);
|
|
tr.appendChild(tdActions);
|
|
|
|
// Add Notes cell
|
|
const tdNotes = document.createElement("td");
|
|
const notesInput = document.createElement("input");
|
|
notesInput.type = "text";
|
|
notesInput.value = rowData.notes || "";
|
|
notesInput.placeholder = "Transaction notes";
|
|
notesInput.style.width = "100%";
|
|
notesInput.addEventListener("change", () => {
|
|
rowData.notes = notesInput.value;
|
|
saveCoinTrackers();
|
|
});
|
|
tdNotes.appendChild(notesInput);
|
|
tr.appendChild(tdNotes);
|
|
|
|
tbody.appendChild(tr);
|
|
updateRowYieldProjections(tr);
|
|
};
|
|
|
|
// Enhanced version of addDynamicEntry that supports transaction types
|
|
const addDynamicEntryWithData = (tbody, rowData, trackerId) => {
|
|
const tr = document.createElement("tr");
|
|
tr.dataset.created = rowData.created || Date.now();
|
|
|
|
// Store transaction metadata
|
|
if (rowData.transactionType) {
|
|
tr.dataset.transactionType = rowData.transactionType;
|
|
}
|
|
if (rowData.linkedTrackerId) {
|
|
tr.dataset.linkedTrackerId = rowData.linkedTrackerId;
|
|
}
|
|
if (rowData.conversionAmount) {
|
|
tr.dataset.conversionAmount = rowData.conversionAmount;
|
|
}
|
|
|
|
// Create date cell
|
|
const tdDate = document.createElement("td");
|
|
const inputDate = document.createElement("input");
|
|
inputDate.type = "date";
|
|
inputDate.value = rowData.date || getToday();
|
|
// Add event listeners as in your existing code
|
|
inputDate.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
const trackerDiv = $(trackerId);
|
|
if (!trackerDiv.dataset.manualSort || trackerDiv.dataset.manualSort === "false") {
|
|
sortCoinTrackerRows(trackerDiv);
|
|
}
|
|
});
|
|
tdDate.appendChild(inputDate);
|
|
tr.appendChild(tdDate);
|
|
|
|
const tdTime = document.createElement("td");
|
|
const inputTime = document.createElement("input");
|
|
inputTime.type = "time";
|
|
inputTime.value = rowData.time || getBerlinTime();
|
|
inputTime.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
tdTime.appendChild(inputTime);
|
|
tr.appendChild(tdTime);
|
|
|
|
const tdCurrent = document.createElement("td");
|
|
tdCurrent.innerHTML = "$";
|
|
const inputCurrent = document.createElement("input");
|
|
inputCurrent.type = "number";
|
|
inputCurrent.step = "0.01";
|
|
inputCurrent.value = rowData.currentValue ? Number(rowData.currentValue).toFixed(2) : "0.00";
|
|
inputCurrent.addEventListener("input", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
inputCurrent.addEventListener("keyup", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
inputCurrent.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
tdCurrent.appendChild(inputCurrent);
|
|
tr.appendChild(tdCurrent);
|
|
|
|
const tdIncome = document.createElement("td");
|
|
const inputIncome = document.createElement("input");
|
|
inputIncome.type = "number";
|
|
inputIncome.step = "0.01";
|
|
inputIncome.value = rowData.income || "";
|
|
inputIncome.addEventListener("change", () => { saveCoinTrackers(); updateCoinTrackerSummary(trackerId); });
|
|
tdIncome.appendChild(inputIncome);
|
|
tr.appendChild(tdIncome);
|
|
|
|
const tdCompound = document.createElement("td");
|
|
const inputCompound = document.createElement("input");
|
|
inputCompound.type = "number";
|
|
inputCompound.step = "0.01";
|
|
inputCompound.value = rowData.compound || "";
|
|
inputCompound.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
tdCompound.appendChild(inputCompound);
|
|
tr.appendChild(tdCompound);
|
|
|
|
const tdYield = document.createElement("td");
|
|
const inputYield = document.createElement("input");
|
|
inputYield.type = "number";
|
|
inputYield.step = "0.01";
|
|
inputYield.value = rowData.dailyYield || "";
|
|
inputYield.addEventListener("change", () => {
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
updateRowYieldProjections(tr);
|
|
});
|
|
tdYield.appendChild(inputYield);
|
|
|
|
// Add a div to hold the yield projections within the cell
|
|
const projectionDiv = document.createElement("div");
|
|
projectionDiv.className = "inline-yield-projection";
|
|
tdYield.appendChild(projectionDiv);
|
|
|
|
tr.appendChild(tdYield);
|
|
|
|
const tdActions = document.createElement("td");
|
|
const delBtn = document.createElement("button");
|
|
delBtn.textContent = "Delete";
|
|
delBtn.className = "deleteRowBtn";
|
|
delBtn.addEventListener("click", () => {
|
|
tr.remove();
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
tdActions.appendChild(delBtn);
|
|
tr.appendChild(tdActions);
|
|
|
|
// Add a notes column
|
|
const tdNotes = document.createElement("td");
|
|
const notesInput = document.createElement("input");
|
|
notesInput.type = "text";
|
|
notesInput.value = rowData.notes || "";
|
|
notesInput.placeholder = "Transaction notes";
|
|
if (rowData.notes) {
|
|
console.log("Loading notes for row:", rowData.notes);
|
|
}
|
|
notesInput.style.width = "100%";
|
|
notesInput.addEventListener("change", () => {
|
|
rowData.notes = notesInput.value;
|
|
saveCoinTrackers();
|
|
});
|
|
tdNotes.appendChild(notesInput);
|
|
tr.appendChild(tdNotes);
|
|
|
|
// Color code rows based on transaction type
|
|
if (rowData.transactionType === "conversion-out") {
|
|
tr.style.backgroundColor = "rgba(255, 99, 71, 0.2)"; // Light red for outgoing
|
|
} else if (rowData.transactionType === "conversion-in") {
|
|
tr.style.backgroundColor = "rgba(144, 238, 144, 0.2)"; // Light green for incoming
|
|
}
|
|
|
|
tbody.appendChild(tr);
|
|
updateRowYieldProjections(tr);
|
|
};
|
|
|
|
// New function to update the projections for a single row
|
|
const updateRowYieldProjections = (row) => {
|
|
const currentVal = parseFloat(row.cells[2].querySelector("input").value) || 0;
|
|
const yieldVal = parseFloat(row.cells[5].querySelector("input").value) || 0;
|
|
const projectionDiv = row.cells[5].querySelector(".inline-yield-projection");
|
|
|
|
if (currentVal > 0 && yieldVal > 0) {
|
|
const dailyIncome = currentVal * (yieldVal / 100);
|
|
const projection24h = dailyIncome;
|
|
const projection7d = dailyIncome * 7;
|
|
const projection30d = dailyIncome * 30;
|
|
|
|
projectionDiv.innerHTML = `<small>24h: $${projection24h.toFixed(2)} | 7d: $${projection7d.toFixed(2)} | 30d: $${projection30d.toFixed(2)}</small>`;
|
|
} else {
|
|
projectionDiv.innerHTML = "";
|
|
}
|
|
};
|
|
|
|
// Update Invested Value Chart
|
|
const updateInvestedChart = () => {
|
|
// Load existing data from localStorage
|
|
let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}");
|
|
let dailyYieldHistory = JSON.parse(localStorage.getItem("persistedDailyYieldHistory") || "{}");
|
|
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
|
|
// Collect new totals from active trackers
|
|
let currentTotals = {};
|
|
let portfolioValues = {};
|
|
let totalDeposited = {};
|
|
let dailyYieldTotals = {}; // Add this to track daily yields
|
|
|
|
// Calculate total initial capital across all trackers
|
|
let totalInitialCapital = 0;
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.showInChart === "false") return;
|
|
// Get initialCapital value
|
|
const initialCapitalInput = div.querySelector(".content input[type='number']");
|
|
if (initialCapitalInput) {
|
|
totalInitialCapital += parseFloat(initialCapitalInput.value || "0") || 0;
|
|
}
|
|
});
|
|
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.showInChart === "false") return;
|
|
const table = div.querySelector("table.coin-table");
|
|
if (table) {
|
|
const tbody = table.querySelector("tbody");
|
|
let trackerDaily = {};
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
const dateInput = tr.cells[0].querySelector("input");
|
|
const timeInput = tr.cells[1].querySelector("input");
|
|
if (dateInput && dateInput.value) {
|
|
const date = dateInput.value; // "YYYY-MM-DD"
|
|
const time = (timeInput && timeInput.value) ? timeInput.value : "00:00";
|
|
const currentInput = tr.cells[2].querySelector("input");
|
|
const dailyYieldPercentInput = tr.cells[5].querySelector("input");
|
|
const currentVal = parseFloat(currentInput.value.replace("$", "")) || 0;
|
|
const dailyYieldPercent = parseFloat(dailyYieldPercentInput.value || "0") || 0;
|
|
|
|
// Store only the largest (latest) time entry for each date
|
|
if (!trackerDaily[date] || time > trackerDaily[date].time) {
|
|
trackerDaily[date] = { time, current: currentVal };
|
|
}
|
|
|
|
// Track portfolio value
|
|
portfolioValues[date] = (portfolioValues[date] || 0) + currentVal;
|
|
|
|
// Calculate daily yield in dollars and accumulate for each date
|
|
const yieldInDollars = currentVal * (dailyYieldPercent / 100);
|
|
dailyYieldTotals[date] = (dailyYieldTotals[date] || 0) + yieldInDollars;
|
|
|
|
// Set totalDeposited to the initial capital for all dates
|
|
totalDeposited[date] = totalInitialCapital;
|
|
}
|
|
});
|
|
for (const date in trackerDaily) {
|
|
currentTotals[date] = (currentTotals[date] || 0) + trackerDaily[date].current;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update daily yield history with new calculations
|
|
for (const date in dailyYieldTotals) {
|
|
dailyYieldHistory[date] = dailyYieldTotals[date];
|
|
}
|
|
|
|
// Save updated daily yield history to localStorage
|
|
localStorage.setItem("persistedDailyYieldHistory", JSON.stringify(dailyYieldHistory));
|
|
|
|
// Merge the new totals into the existing history
|
|
for (const date in currentTotals) {
|
|
persistedHistory[date] = currentTotals[date];
|
|
}
|
|
|
|
// Save merged data back to localStorage
|
|
localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory));
|
|
|
|
// Build sorted arrays
|
|
const dates = Object.keys(persistedHistory).sort();
|
|
let labels = dates.map(date => new Date(date).toLocaleDateString());
|
|
let dailyFeesData = dates.map(date => dailyYieldHistory[date] || 0);
|
|
let portfolioData = dates.map(date => portfolioValues[date] || persistedHistory[date]);
|
|
let depositedData = dates.map(date => totalDeposited[date] || persistedHistory[date] * 0.9);
|
|
|
|
// Update or create the chart
|
|
if (investedChart) {
|
|
investedChart.destroy();
|
|
}
|
|
|
|
const ctx = $('investedChart').getContext('2d');
|
|
investedChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: 'Daily Fees',
|
|
data: dailyFeesData,
|
|
fill: false,
|
|
backgroundColor: '#00FF00',
|
|
borderColor: '#00FF00',
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
yAxisID: 'y1'
|
|
},
|
|
{
|
|
label: 'Portfolio Value',
|
|
data: portfolioData,
|
|
fill: false,
|
|
backgroundColor: '#1E90FF',
|
|
borderColor: '#1E90FF',
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
yAxisID: 'y'
|
|
},
|
|
{
|
|
label: 'Total Deposited Value',
|
|
data: depositedData,
|
|
fill: false,
|
|
backgroundColor: '#FFD700',
|
|
borderColor: '#FFD700',
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
pointRadius: 3,
|
|
yAxisID: 'y'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
color: '#333333'
|
|
},
|
|
ticks: {
|
|
color: '#CCCCCC'
|
|
}
|
|
},
|
|
y: {
|
|
position: 'left',
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: '#333333'
|
|
},
|
|
ticks: {
|
|
color: '#CCCCCC',
|
|
callback: value => '$' + value
|
|
}
|
|
},
|
|
y1: {
|
|
position: 'right',
|
|
beginAtZero: true,
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
color: '#CCCCCC',
|
|
callback: value => '$' + value
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
color: '#CCCCCC',
|
|
boxWidth: 15,
|
|
padding: 10
|
|
}
|
|
},
|
|
datalabels: {
|
|
color: '#FFFFFF',
|
|
font: {
|
|
weight: 'bold',
|
|
size: 10
|
|
},
|
|
formatter: function(value) {
|
|
return value.toFixed(2);
|
|
},
|
|
align: 'top',
|
|
anchor: 'end',
|
|
offset: 0
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += '$' + context.parsed.y.toFixed(2);
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
backgroundColor: '#1a1a1a'
|
|
}
|
|
});
|
|
};
|
|
|
|
// Update Income Chart (sums total income for each coin tracker)
|
|
const updateIncomeChart = () => {
|
|
console.log("updateIncomeChart called");
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
const labels = [];
|
|
const incomeData = [];
|
|
const compoundData = [];
|
|
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.active === "false") return;
|
|
const coinNameInput = div.querySelector(".coin-tracker-header input.coin-name-input");
|
|
const coinName = coinNameInput ? coinNameInput.value : "Unnamed Coin";
|
|
let totalIncome = 0;
|
|
let totalCompound = 0;
|
|
const table = div.querySelector("table.coin-table");
|
|
if (table) {
|
|
const tbody = table.querySelector("tbody");
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
if (tr.cells.length > 3) {
|
|
const incomeInput = tr.cells[3].querySelector("input");
|
|
const incomeVal = parseFloat(incomeInput.value || "0");
|
|
totalIncome += isNaN(incomeVal) ? 0 : incomeVal;
|
|
const compoundInput = tr.cells[4].querySelector("input");
|
|
const compoundVal = parseFloat(compoundInput.value || "0");
|
|
totalCompound += isNaN(compoundVal) ? 0 : compoundVal;
|
|
}
|
|
});
|
|
}
|
|
labels.push(coinName);
|
|
incomeData.push(totalIncome);
|
|
compoundData.push(totalCompound);
|
|
});
|
|
|
|
console.log("Income Chart labels:", labels);
|
|
console.log("Income Chart data:", incomeData);
|
|
|
|
const ctx = $('incomeChart').getContext('2d');
|
|
if (incomeChart) {
|
|
incomeChart.data.labels = labels;
|
|
incomeChart.data.datasets[0].data = incomeData;
|
|
incomeChart.data.datasets[1].data = compoundData;
|
|
incomeChart.update();
|
|
} else {
|
|
incomeChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: 'Income',
|
|
data: incomeData,
|
|
backgroundColor: 'rgba(255, 99, 132, 0.5)',
|
|
stack: 'combined',
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
borderWidth: 1
|
|
},
|
|
{
|
|
label: 'Compound',
|
|
data: compoundData,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
|
stack: 'combined',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
x: { stacked: true },
|
|
y: {
|
|
stacked: true,
|
|
beginAtZero: true,
|
|
ticks: { callback: value => '$' + value }
|
|
}
|
|
}
|
|
}
|
|
}); // FIXED: Added missing closing brace here
|
|
}
|
|
};
|
|
|
|
// Update Total Invested display (using last row's current value from each tracker)
|
|
const updateTotalInvested = () => {
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
let totalInitialCapital = 0; // To track sum of all initial capital
|
|
let currentTotalInvestment = 0; // Current value of investments
|
|
let combinedYields = 0;
|
|
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.active === "false") return;
|
|
|
|
// Sum up initial capital for this tracker
|
|
const initialCapitalInput = div.querySelector(".content input[type='number']");
|
|
if (initialCapitalInput) {
|
|
totalInitialCapital += parseFloat(initialCapitalInput.value || "0") || 0;
|
|
}
|
|
|
|
const table = div.querySelector("table.coin-table");
|
|
if (!table) return;
|
|
|
|
const rows = table.querySelector("tbody").rows;
|
|
if (rows.length > 0) {
|
|
// Take current value from the last row
|
|
const lastRow = rows[rows.length - 1];
|
|
const inputCurrent = lastRow.cells[2].querySelector("input");
|
|
currentTotalInvestment += parseFloat(inputCurrent.value || "0") || 0;
|
|
|
|
// Only use the last row's daily yield for this tracker
|
|
const yieldInput = lastRow.cells[5].querySelector("input");
|
|
const dailyYieldPercent = parseFloat(yieldInput.value || "0") || 0;
|
|
const currentVal = parseFloat(inputCurrent.value || "0") || 0;
|
|
combinedYields += (currentVal * dailyYieldPercent / 100);
|
|
}
|
|
});
|
|
|
|
// Determine if we're in profit or loss
|
|
const isProfit = currentTotalInvestment > totalInitialCapital;
|
|
const difference = currentTotalInvestment - totalInitialCapital;
|
|
const differencePrefix = difference > 0 ? '+' : '';
|
|
|
|
// Update the displays
|
|
$('totalInvestmentValueDisplay').textContent = `Initial Capital: $${totalInitialCapital.toFixed(2)}`;
|
|
|
|
// Create or update the current investment value element
|
|
let currentValueDisplay = $('currentInvestmentValueDisplay');
|
|
if (!currentValueDisplay) {
|
|
currentValueDisplay = document.createElement('span');
|
|
currentValueDisplay.id = 'currentInvestmentValueDisplay';
|
|
currentValueDisplay.style.marginLeft = '15px';
|
|
$('totalInvestmentValueDisplay').parentNode.insertBefore(
|
|
currentValueDisplay,
|
|
$('combinedDailyYieldsDisplay')
|
|
);
|
|
}
|
|
|
|
// Set the current value with appropriate color and show difference in brackets
|
|
currentValueDisplay.textContent = `Current Value: $${currentTotalInvestment.toFixed(2)} [${differencePrefix}$${difference.toFixed(2)}]`;
|
|
currentValueDisplay.style.color = isProfit ? 'green' : (currentTotalInvestment < totalInitialCapital ? 'red' : 'inherit');
|
|
|
|
$('combinedDailyYieldsDisplay').textContent = `Combined Daily Yields: $${combinedYields.toFixed(2)}`;
|
|
|
|
updateInvestedChart();
|
|
updateIncomeChart();
|
|
updateAnalysisTable();
|
|
};
|
|
|
|
// Save coin trackers by reading the DOM and updating the backend.
|
|
const saveCoinTrackers = () => {
|
|
const trackers = [];
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
Array.from(trackerDivs).forEach(div => {
|
|
const trackerId = div.id;
|
|
const headerDiv = div.querySelector("div");
|
|
const coinNameInput = headerDiv.querySelector("input.coin-name-input");
|
|
const coinName = coinNameInput.value;
|
|
|
|
// Get the platform value
|
|
const platformInput = headerDiv.querySelector("input[list='platformListDatalist']");
|
|
const platform = platformInput ? platformInput.value : "";
|
|
|
|
const showInChart = div.dataset.showInChart; // persist the state here
|
|
const active = div.dataset.active; // save active state
|
|
|
|
// Get initialCapital value
|
|
const initialCapitalInput = div.querySelector(".content input[type='number']");
|
|
const initialCapital = initialCapitalInput ? initialCapitalInput.value : "0.00";
|
|
|
|
const table = div.querySelector("table.coin-table");
|
|
const tbody = table.querySelector("tbody");
|
|
const rows = [];
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
const date = tr.cells[0].querySelector("input").value;
|
|
const time = tr.cells[1].querySelector("input").value;
|
|
const currentValue = parseFloat(tr.cells[2].querySelector("input").value.replace("$", "")) || 0;
|
|
const income = parseFloat(tr.cells[3].querySelector("input").value) || 0;
|
|
const compound = parseFloat(tr.cells[4].querySelector("input").value) || 0;
|
|
const dailyYield = parseFloat(tr.cells[5].querySelector("input").value) || 0;
|
|
const notes = tr.cells[7] ? (tr.cells[7].querySelector("input") ? tr.cells[7].querySelector("input").value : "") : "";
|
|
const created = tr.dataset.created;
|
|
console.log("Saving row with notes:", notes, "for date:", date);
|
|
rows.push({ date, time, currentValue, income, compound, dailyYield, notes, created });
|
|
});
|
|
const hasData = rows.some(row => row.currentValue !== 0 || row.income !== 0 || row.dailyYield !== 0);
|
|
if (hasData) {
|
|
trackers.push({ id: trackerId, coinName, platform, initialCapital, rows, showInChart, active });
|
|
}
|
|
});
|
|
updateCoinTrackersToBackend(trackers);
|
|
};
|
|
|
|
// Load coin trackers exclusively from the backend.
|
|
const loadCoinTrackers = () => {
|
|
console.log("Loading trackers from backend...");
|
|
|
|
fetch('/api/coinTrackers')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(responseData => {
|
|
console.log("Received tracker data:", responseData);
|
|
|
|
if (responseData && responseData.data && Array.isArray(responseData.data)) {
|
|
// Clear existing trackers from the DOM
|
|
$('coinTrackerContainer').innerHTML = '';
|
|
|
|
// Add each tracker from the backend data
|
|
responseData.data.forEach(tracker => {
|
|
console.log("Loading tracker:", tracker);
|
|
if (tracker.rows) {
|
|
tracker.rows.forEach(row => {
|
|
console.log("Loading row:", row);
|
|
});
|
|
}
|
|
addCoinTracker(tracker);
|
|
});
|
|
|
|
// Update UI after loading all trackers
|
|
updateTotalInvested();
|
|
|
|
// Update the asset conversion dropdowns
|
|
updateAssetDropdowns();
|
|
} else {
|
|
console.error("Invalid data structure received from backend");
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error("Error loading trackers:", error);
|
|
});
|
|
};
|
|
|
|
// Update coin tracker summary.
|
|
// Now, to determine the “last entered” daily yield, we use the row with the highest creation timestamp.
|
|
const updateCoinTrackerSummary = trackerId => {
|
|
const trackerDiv = $(trackerId);
|
|
if (!trackerDiv) return;
|
|
const table = trackerDiv.querySelector("table.coin-table");
|
|
const tbody = table.querySelector("tbody");
|
|
|
|
// Use the last row (in reverse order) that has a nonzero current value.
|
|
let trackerValue = 0;
|
|
let lastDailyYield = 0;
|
|
const rowsReversed = Array.from(tbody.rows).reverse();
|
|
for (const row of rowsReversed) {
|
|
const currentVal = parseFloat(row.cells[2].querySelector("input").value) || 0;
|
|
if (currentVal > 0) {
|
|
lastDailyYield = parseFloat(row.cells[5].querySelector("input").value) || 0;
|
|
trackerValue = currentVal;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update header "Current Value: $" display.
|
|
const lastValueSpan = trackerDiv.querySelector('.last-value');
|
|
if (lastValueSpan) {
|
|
lastValueSpan.textContent = "Current Value: $" + trackerValue.toFixed(2);
|
|
}
|
|
|
|
// Calculate and update total income
|
|
let totalIncome = 0;
|
|
let totalCompound = 0;
|
|
Array.from(tbody.rows).forEach(row => {
|
|
const incomeVal = parseFloat(row.cells[3].querySelector("input").value) || 0;
|
|
totalIncome += incomeVal;
|
|
const compoundVal = parseFloat(row.cells[4].querySelector("input").value) || 0;
|
|
totalCompound += compoundVal;
|
|
});
|
|
|
|
// Update total income display
|
|
const totalIncomeSpan = trackerDiv.querySelector('.total-income');
|
|
if (totalIncomeSpan) {
|
|
totalIncomeSpan.textContent = `Total Income: $${totalIncome.toFixed(2)}`;
|
|
}
|
|
|
|
// Update all rows' yield projections
|
|
Array.from(tbody.rows).forEach(row => {
|
|
updateRowYieldProjections(row);
|
|
});
|
|
|
|
// Calculate and update PnL
|
|
const initialCapitalInput = trackerDiv.querySelector(".content input[type='number']");
|
|
const initialCapital = parseFloat(initialCapitalInput.value) || 0;
|
|
|
|
const pnlCurrentValue = trackerValue - initialCapital;
|
|
const pnlTotalValue = (trackerValue + totalIncome + totalCompound) - initialCapital;
|
|
|
|
const pnlCurrentValueSpan = trackerDiv.querySelector('.pnl-current-value');
|
|
if (pnlCurrentValueSpan) {
|
|
pnlCurrentValueSpan.textContent = `PnL (Current Value): $${pnlCurrentValue.toFixed(2)}`;
|
|
pnlCurrentValueSpan.style.color = pnlCurrentValue >= 0 ? 'green' : 'red';
|
|
}
|
|
|
|
const pnlTotalValueSpan = trackerDiv.querySelector('.pnl-total-value');
|
|
if (pnlTotalValueSpan) {
|
|
pnlTotalValueSpan.textContent = `PnL (Total Value): $${pnlTotalValue.toFixed(2)}`;
|
|
pnlTotalValueSpan.style.color = pnlTotalValue >= 0 ? 'green' : 'red';
|
|
}
|
|
|
|
updateTotalInvested();
|
|
};
|
|
|
|
const updateTransferDropdown = () => {
|
|
const select = $('transferSelect');
|
|
// If element is not found, log a warning and exit.
|
|
if (!select) {
|
|
console.warn("transferSelect element is not found in the DOM.");
|
|
return;
|
|
}
|
|
select.innerHTML = '<option value="">-- Select --</option>';
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
Array.from(trackerDivs).forEach(div => {
|
|
const id = div.id;
|
|
// Use the specific header element (using a more specific selector)
|
|
const headerDiv = div.querySelector("div.coin-tracker-header");
|
|
if (!headerDiv) return;
|
|
const coinNameInput = headerDiv.querySelector("input.coin-name-input");
|
|
if (!coinNameInput) return;
|
|
const option = document.createElement("option");
|
|
option.value = id;
|
|
option.text = coinNameInput.value;
|
|
select.appendChild(option);
|
|
});
|
|
const createOption = document.createElement("option");
|
|
createOption.value = "create";
|
|
createOption.text = "Create New Coin Tracker";
|
|
select.appendChild(createOption);
|
|
};
|
|
|
|
const transferSettings = () => {
|
|
const select = $('transferSelect');
|
|
const selection = select.value;
|
|
const currentValue = $('initialPrincipal').value;
|
|
const rateValue = $('dailyRate').value;
|
|
if (!selection) {
|
|
alert("Please select a coin tracker or create a new one.");
|
|
return;
|
|
}
|
|
let trackerDiv;
|
|
if (selection === "create") {
|
|
addCoinTracker();
|
|
const container = $('coinTrackerContainer');
|
|
trackerDiv = container.lastElementChild;
|
|
} else {
|
|
trackerDiv = $(selection);
|
|
}
|
|
if (trackerDiv) {
|
|
const table = trackerDiv.querySelector("table.coin-table");
|
|
const tbody = table.querySelector("tbody");
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
const currentCellInput = tr.cells[2].querySelector("input");
|
|
const currentVal = parseFloat(currentCellInput.value.replace("$", "")) || 0;
|
|
if (currentVal === 0) {
|
|
currentCellInput.value = parseFloat(currentValue).toFixed(2);
|
|
}
|
|
});
|
|
const headerDiv = trackerDiv.querySelector("div");
|
|
const interestSpan = headerDiv.querySelector(".tracker-interest");
|
|
interestSpan.innerText = `(Interest Rate: ${rateValue}%)`;
|
|
const earnedValue = (parseFloat(currentValue) * parseFloat(rateValue) / 100).toFixed(2);
|
|
addDynamicEntry(tbody, {
|
|
date: getToday(),
|
|
time: getBerlinTime(),
|
|
income: earnedValue,
|
|
dailyYield: rateValue,
|
|
currentValue: parseFloat(currentValue)
|
|
}, trackerDiv.id);
|
|
alert("Settings transferred!");
|
|
saveCoinTrackers();
|
|
updateCoinTrackerSummary(trackerDiv.id);
|
|
updateTransferDropdown();
|
|
}
|
|
};
|
|
|
|
// New function: reset (subtract) a specified dollar amount from a specific day in the graph.
|
|
const resetGraphValue = () => {
|
|
const resetDate = $('resetDate').value;
|
|
const resetAmount = parseFloat($('resetAmount').value) || 0;
|
|
console.log('Reset clicked:', resetDate, resetAmount);
|
|
if (!resetDate || resetAmount <= 0) {
|
|
alert("Please enter a valid date and amount.");
|
|
return;
|
|
}
|
|
let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}");
|
|
console.log('Persisted history before update:', persistedHistory);
|
|
if (persistedHistory.hasOwnProperty(resetDate)) {
|
|
persistedHistory[resetDate] = Math.max(0, persistedHistory[resetDate] - resetAmount);
|
|
localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory));
|
|
updateInvestedChart();
|
|
alert(`Updated value for ${resetDate}.`);
|
|
} else {
|
|
alert("No value exists for that date.");
|
|
}
|
|
};
|
|
|
|
// New function: delete all datapoints for a specific date from the graph.
|
|
const deleteGraphDataPoint = () => {
|
|
const resetDate = $('resetDate').value;
|
|
if (!resetDate) {
|
|
alert("Please select a valid date.");
|
|
return;
|
|
}
|
|
let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}");
|
|
if (persistedHistory.hasOwnProperty(resetDate)) {
|
|
delete persistedHistory[resetDate];
|
|
localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory));
|
|
updateInvestedChart();
|
|
alert(`Deleted data for ${resetDate}.`);
|
|
} else {
|
|
alert("No data exists for that date.");
|
|
}
|
|
};
|
|
|
|
function updatePlatformDatalist(platform) {
|
|
const list = document.getElementById("platformListDatalist");
|
|
if (!list) return;
|
|
const existingOption = Array.from(list.options).find(opt => opt.value === platform);
|
|
if (!existingOption && platform.trim().length > 0) {
|
|
const newOption = document.createElement("option");
|
|
newOption.value = platform;
|
|
list.appendChild(newOption);
|
|
}
|
|
}
|
|
|
|
const updateDailyYieldChart = () => {
|
|
// Load existing data from localStorage
|
|
let persistedDailyYieldHistory = JSON.parse(localStorage.getItem("persistedDailyYieldHistory") || "{}");
|
|
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
|
|
// Collect new yields from active trackers
|
|
let dailyTotals = {};
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.active === "false") return;
|
|
const table = div.querySelector("table.coin-table");
|
|
if (table) {
|
|
const tbody = table.querySelector("tbody");
|
|
|
|
// CHANGED: Process ALL rows instead of just the last one
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
const dateInput = tr.cells[0].querySelector("input");
|
|
if (dateInput && dateInput.value) {
|
|
const date = dateInput.value; // "YYYY-MM-DD"
|
|
const currentInput = tr.cells[2].querySelector("input");
|
|
const dailyYieldPercentInput = tr.cells[5].querySelector("input");
|
|
const currentVal = parseFloat(currentInput.value.replace("$", "")) || 0;
|
|
const dailyYieldPercent = parseFloat(dailyYieldPercentInput.value || "0") || 0;
|
|
// Convert percentage to dollar amount
|
|
const yieldInDollars = currentVal * (dailyYieldPercent / 100);
|
|
|
|
// Sum yields for each date across trackers
|
|
dailyTotals[date] = (dailyTotals[date] || 0) + yieldInDollars;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Merge new yields into persistedDailyYieldHistory
|
|
for (const date in dailyTotals) {
|
|
persistedDailyYieldHistory[date] = dailyTotals[date] // Just assign, don't add
|
|
}
|
|
|
|
// Save merged data back to localStorage
|
|
localStorage.setItem("persistedDailyYieldHistory", JSON.stringify(persistedDailyYieldHistory));
|
|
|
|
// Build sorted arrays
|
|
const dates = Object.keys(persistedDailyYieldHistory).sort();
|
|
const labels = dates.map(date => new Date(date).toLocaleDateString());
|
|
const data = dates.map(date => persistedDailyYieldHistory[date]);
|
|
|
|
// Create or update the chart
|
|
if (dailyYieldChart) {
|
|
dailyYieldChart.data.labels = labels;
|
|
dailyYieldChart.data.datasets[0].data = data;
|
|
dailyYieldChart.update();
|
|
} else {
|
|
const ctx = $('dailyYieldChart').getContext('2d');
|
|
dailyYieldChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: 'Daily Yield ($)',
|
|
data,
|
|
fill: true,
|
|
backgroundColor: context => {
|
|
const gradient = context.chart
|
|
gradient.addColorStop(0, 'rgba(255, 193, 7, 0.4)');
|
|
gradient.addColorStop(1, 'rgba(255, 193, 7, 0.1)');
|
|
return gradient;
|
|
},
|
|
borderColor: '#ffc107',
|
|
borderWidth: 2,
|
|
tension: 0.3
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: { callback: value => '$' + value }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Function to update the analysis table based on coin tracker data
|
|
const updateAnalysisTable = () => {
|
|
console.log("updateAnalysisTable called");
|
|
console.trace("Call stack for updateAnalysisTable");
|
|
|
|
// Get reinvestment rate from the dedicated analysis input - NOT the main calculator
|
|
const reinvestRateElement = document.getElementById('analysisReinvestRate');
|
|
const analysisDaysElement = document.getElementById('analysisDays');
|
|
|
|
console.log("Elements found:", !!reinvestRateElement, !!analysisDaysElement);
|
|
console.log("Elements:", reinvestRateElement, analysisDaysElement);
|
|
if (reinvestRateElement) {
|
|
console.log("Reinvest rate value:", reinvestRateElement.value);
|
|
console.log("Reinvest rate attribute:", reinvestRateElement.getAttribute('value'));
|
|
console.log("Reinvest rate type:", typeof reinvestRateElement.value);
|
|
}
|
|
if (analysisDaysElement) {
|
|
console.log("Days value:", analysisDaysElement.value);
|
|
console.log("Days attribute:", analysisDaysElement.getAttribute('value'));
|
|
console.log("Days type:", typeof analysisDaysElement.value);
|
|
}
|
|
|
|
const reinvestRate = reinvestRateElement ? parseFloat(reinvestRateElement.value) || 0 : 0;
|
|
const days = analysisDaysElement ? parseInt(analysisDaysElement.value) || 30 : 30;
|
|
|
|
console.log("Parsed values - Reinvest Rate:", reinvestRate, "Days:", days);
|
|
|
|
// Get data from active coin trackers
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
|
|
// Initialize total investment and weighted yield
|
|
let totalInvestment = 0;
|
|
let weightedYieldSum = 0;
|
|
|
|
// Calculate total investment and weighted yield
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.active === "false") return;
|
|
const table = div.querySelector("table.coin-table");
|
|
if (!table) return;
|
|
|
|
const rows = table.querySelector("tbody").rows;
|
|
if (rows.length > 0) {
|
|
const lastRow = rows[rows.length - 1];
|
|
const inputCurrent = lastRow.cells[2].querySelector("input");
|
|
const currentVal = parseFloat(inputCurrent.value || "0") || 0;
|
|
const yieldInput = lastRow.cells[5].querySelector("input");
|
|
const dailyYieldPercent = parseFloat(yieldInput.value || "0") || 0;
|
|
|
|
totalInvestment += currentVal;
|
|
weightedYieldSum += (currentVal * dailyYieldPercent);
|
|
}
|
|
});
|
|
|
|
// Calculate weighted average daily yield
|
|
const weightedAverageDailyYield = totalInvestment > 0 ? (weightedYieldSum / totalInvestment) : 0;
|
|
|
|
console.log("Analysis values - Reinvest Rate:", reinvestRate, "Days:", days);
|
|
console.log("Total Investment:", totalInvestment, "Weighted Yield:", weightedAverageDailyYield);
|
|
|
|
// Calculate compounding for the next 30 days (or whatever period you want)
|
|
let runningPrincipal = totalInvestment;
|
|
let cumulativeCash = 0;
|
|
let tableHTML = '';
|
|
let currentDate = new Date();
|
|
|
|
for (let i = 0; i < days; i++) {
|
|
// Move to next day
|
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
const dateStr = currentDate.toLocaleDateString();
|
|
|
|
// Calculate daily earnings based on the current principal
|
|
const earnings = runningPrincipal * (weightedAverageDailyYield / 100);
|
|
|
|
// Calculate reinvestment and cashout based on reinvest rate
|
|
const reinvestment = earnings * (reinvestRate / 100);
|
|
const cashout = earnings - reinvestment;
|
|
|
|
// Add reinvestment to principal
|
|
runningPrincipal += reinvestment;
|
|
|
|
// Add cashout to cumulative cash
|
|
cumulativeCash += cashout;
|
|
|
|
// Create table row
|
|
tableHTML += `<tr>
|
|
<td>${i + 1}</td>
|
|
<td>${dateStr}</td>
|
|
<td>$${earnings.toFixed(2)}</td>
|
|
<td>${reinvestRate}%</td>
|
|
<td>$${reinvestment.toFixed(2)} / $${cashout.toFixed(2)}</td>
|
|
<td>$${runningPrincipal.toFixed(2)}</td>
|
|
<td>$${cumulativeCash.toFixed(2)}</td>
|
|
</tr>`;
|
|
}
|
|
|
|
// Update the table
|
|
$('analysisTable').querySelector('tbody').innerHTML = tableHTML;
|
|
};
|
|
|
|
// Function to create a manual backup
|
|
const createManualBackup = () => {
|
|
const backupNameInput = document.querySelector('input[placeholder="Backup name (optional)"]');
|
|
const backupName = backupNameInput ? backupNameInput.value : '';
|
|
|
|
console.log("Creating manual backup with name:", backupName || "(unnamed)");
|
|
|
|
// Show loading indicator
|
|
if (backupNameInput) {
|
|
backupNameInput.disabled = true;
|
|
}
|
|
document.getElementById('createBackupBtn').disabled = true;
|
|
document.getElementById('createBackupBtn').textContent = 'Creating backup...';
|
|
|
|
// Make request to API
|
|
fetch(`/api/backup/create${backupName ? `?name=${encodeURIComponent(backupName)}` : ''}`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Network error: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Backup created successfully:', data);
|
|
alert('Backup created successfully!');
|
|
|
|
// Refresh backup list
|
|
loadBackupList();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error creating backup:', error);
|
|
alert(`Failed to create backup: ${error.message}`);
|
|
})
|
|
.finally(() => {
|
|
// Reset UI
|
|
if (backupNameInput) {
|
|
backupNameInput.disabled = false;
|
|
backupNameInput.value = '';
|
|
}
|
|
document.getElementById('createBackupBtn').disabled = false;
|
|
document.getElementById('createBackupBtn').textContent = 'Create Manual Backup';
|
|
});
|
|
};
|
|
|
|
// Function to load the list of available backups
|
|
const loadBackupList = () => {
|
|
fetch('/api/backup/list')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const backupCalendar = $('backupCalendar');
|
|
backupCalendar.innerHTML = '';
|
|
|
|
if (data.backups && data.backups.length > 0) {
|
|
// Group backups by date
|
|
const backupsByDate = {};
|
|
data.backups.forEach(backup => {
|
|
const date = new Date(backup.date);
|
|
const dateKey = date.toISOString().split('T')[0];
|
|
|
|
if (!backupsByDate[dateKey]) {
|
|
backupsByDate[dateKey] = [];
|
|
}
|
|
backupsByDate[dateKey].push(backup);
|
|
});
|
|
|
|
// Sort dates in reverse order (newest first)
|
|
const sortedDates = Object.keys(backupsByDate).sort().reverse();
|
|
|
|
sortedDates.forEach(dateKey => {
|
|
const backupsForDate = backupsByDate[dateKey];
|
|
|
|
// Create date header
|
|
const dateHeader = document.createElement('div');
|
|
dateHeader.className = 'backup-date-header';
|
|
dateHeader.style.gridColumn = '1 / -1';
|
|
dateHeader.style.background = '#333';
|
|
dateHeader.style.padding = '5px';
|
|
dateHeader.style.fontWeight = 'bold';
|
|
dateHeader.style.borderRadius = '4px';
|
|
dateHeader.style.marginTop = '10px';
|
|
dateHeader.textContent = new Date(dateKey).toLocaleDateString(undefined, {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
backupCalendar.appendChild(dateHeader);
|
|
|
|
// Add the backups for this date
|
|
backupsForDate.forEach(backup => {
|
|
const backupCard = document.createElement('div');
|
|
backupCard.className = 'backup-card';
|
|
backupCard.style.padding = '10px';
|
|
backupCard.style.background = '#555';
|
|
backupCard.style.borderRadius = '4px';
|
|
backupCard.style.cursor = 'pointer';
|
|
backupCard.style.transition = 'background-color 0.3s';
|
|
|
|
// Format timestamp nicely
|
|
const time = new Date(backup.date).toLocaleTimeString();
|
|
const size = (backup.size / 1024).toFixed(1) + ' KB';
|
|
|
|
// Get backup name if available
|
|
const fileName = backup.file;
|
|
let backupName = fileName;
|
|
if (fileName.includes('_name_')) {
|
|
backupName = fileName.split('_name_')[1].split('.json')[0];
|
|
}
|
|
|
|
backupCard.innerHTML = `
|
|
<div class="backup-time">${time}</div>
|
|
<div class="backup-name">${backupName}</div>
|
|
<div class="backup-size">${size}</div>
|
|
`;
|
|
|
|
backupCard.addEventListener('click', () => {
|
|
// Remove selected class from all cards
|
|
document.querySelectorAll('.backup-card').forEach(card => {
|
|
card.style.background = '#555';
|
|
});
|
|
|
|
// Add selected class to this card
|
|
backupCard.style.background = '#007bff';
|
|
|
|
// Show backup details
|
|
$('selectedBackupInfo').style.display = 'block';
|
|
$('backupDetails').innerHTML = `
|
|
<p><strong>Date:</strong> ${new Date(backup.date).toLocaleString()}</p>
|
|
<p><strong>Name:</strong> ${backupName}</p>
|
|
<p><strong>Size:</strong> ${size}</p>
|
|
<p><strong>Filename:</strong> ${backup.file}</p>
|
|
`;
|
|
|
|
// Store the selected backup filename
|
|
$('restoreBackupBtn').dataset.file = backup.file;
|
|
});
|
|
|
|
backupCalendar.appendChild(backupCard);
|
|
});
|
|
});
|
|
|
|
// Show the backup calendar
|
|
$('backupCalendarContainer').style.display = 'block';
|
|
} else {
|
|
backupCalendar.innerHTML = '<p>No backups available.</p>';
|
|
$('backupCalendarContainer').style.display = 'block';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading backups:', error);
|
|
$('backupCalendar').innerHTML = '<p>Failed to load backups. See console for details.</p>';
|
|
$('backupCalendarContainer').style.display = 'block';
|
|
});
|
|
};
|
|
|
|
// Function to restore from a backup
|
|
const restoreFromBackup = (filename) => {
|
|
if (!filename) {
|
|
alert('No backup selected');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Are you sure you want to restore from backup "${filename}"? This will replace all current data.`)) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/backup/restore/${filename}`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
alert('Backup restored successfully! The page will reload.');
|
|
window.location.reload();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error restoring backup:', error);
|
|
alert('Failed to restore backup. See console for details.');
|
|
});
|
|
};
|
|
|
|
// Function to log conversions to history
|
|
const logConversion = (conversionData) => {
|
|
// Get existing history
|
|
let conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]");
|
|
|
|
// Add new conversion with timestamp
|
|
conversionData.timestamp = Date.now();
|
|
conversionHistory.push(conversionData);
|
|
|
|
// Save back to localStorage
|
|
localStorage.setItem("conversionHistory", JSON.stringify(conversionHistory));
|
|
|
|
// Update the conversion history table - with error handling
|
|
try {
|
|
updateConversionHistoryTable();
|
|
} catch (error) {
|
|
console.error("Error updating conversion history table:", error);
|
|
// Don't show alert to user as this is not critical
|
|
}
|
|
};
|
|
|
|
// Function to update the conversion history table
|
|
const updateConversionHistoryTable = () => {
|
|
const tableBody = document.querySelector("#conversionHistoryTable tbody");
|
|
if (!tableBody) {
|
|
console.warn("Conversion history table tbody not found in the DOM");
|
|
return;
|
|
}
|
|
|
|
// Get conversion history
|
|
const conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]");
|
|
|
|
// Sort by date, newest first
|
|
conversionHistory.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
|
|
// Clear existing rows
|
|
tableBody.innerHTML = "";
|
|
|
|
// Add rows for each conversion
|
|
conversionHistory.forEach((conversion, index) => {
|
|
// Create row elements only if conversion data exists
|
|
if (!conversion) return;
|
|
|
|
const row = document.createElement("tr");
|
|
row.dataset.index = index; // Store the index for easy deletion
|
|
|
|
const dateCell = document.createElement("td");
|
|
dateCell.textContent = `${conversion.date} ${conversion.time}`;
|
|
row.appendChild(dateCell);
|
|
|
|
const fromCell = document.createElement("td");
|
|
fromCell.textContent = conversion.fromName;
|
|
row.appendChild(fromCell);
|
|
|
|
const amountCell = document.createElement("td");
|
|
// For backward compatibility with existing data
|
|
const dollarAmount = conversion.dollarAmount || conversion.sentAmount || 0;
|
|
|
|
// If this is an income transfer, show that in the cell
|
|
if (conversion.transferType === 'income') {
|
|
amountCell.textContent = `$${dollarAmount.toFixed(2)} (Income)`;
|
|
amountCell.style.color = '#4CAF50'; // Green for income transfers
|
|
} else {
|
|
amountCell.textContent = `$${dollarAmount.toFixed(2)}`;
|
|
}
|
|
row.appendChild(amountCell);
|
|
|
|
const toCell = document.createElement("td");
|
|
toCell.textContent = conversion.toName;
|
|
row.appendChild(toCell);
|
|
|
|
const receivedCell = document.createElement("td");
|
|
// Same dollar amount, preserves consistency with existing data
|
|
receivedCell.textContent = `$${dollarAmount.toFixed(2)}`;
|
|
row.appendChild(receivedCell);
|
|
|
|
// Add delete button
|
|
const actionsCell = document.createElement("td");
|
|
const deleteBtn = document.createElement("button");
|
|
deleteBtn.textContent = "Delete";
|
|
deleteBtn.style.padding = "3px 8px";
|
|
deleteBtn.style.background = "#ff4444";
|
|
deleteBtn.style.color = "white";
|
|
deleteBtn.style.border = "none";
|
|
deleteBtn.style.borderRadius = "3px";
|
|
deleteBtn.style.cursor = "pointer";
|
|
|
|
deleteBtn.addEventListener("click", function() {
|
|
if (confirm("CONFIRM DELETION: This will:\n\n1. REMOVE the destination entry (the target coin purchase)\n2. KEEP the source entry but clear its transfer note\n\nSource rows will NEVER be deleted, only their transfer notes will be cleared.")) {
|
|
// Always re-read localStorage to avoid index bugs
|
|
let conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]");
|
|
|
|
// CRITICAL FIX: The table displays sorted history, but we need to find the record by its actual properties, not index
|
|
// because the displayed index might not match the storage array index due to sorting
|
|
const displayedConversions = [...conversionHistory].sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0));
|
|
const conversionRecord = displayedConversions[index];
|
|
|
|
// Find the actual index in the original unsorted array
|
|
const actualIndex = conversionHistory.findIndex(record =>
|
|
record.transactionId === conversionRecord.transactionId ||
|
|
(record.date === conversionRecord.date &&
|
|
record.time === conversionRecord.time &&
|
|
record.fromName === conversionRecord.fromName &&
|
|
record.toName === conversionRecord.toName &&
|
|
record.dollarAmount === conversionRecord.dollarAmount)
|
|
);
|
|
|
|
console.log("=== SCRIPT.JS DELETION DEBUG START ===");
|
|
console.log("Displayed index (from sorted array):", index);
|
|
console.log("Actual index (in storage array):", actualIndex);
|
|
console.log("Conversion record being deleted:", conversionRecord);
|
|
console.log("Total conversion history length:", conversionHistory.length);
|
|
console.log("=== SCRIPT.JS DELETION DEBUG END ===");
|
|
|
|
// If the conversion has a transaction ID, find and handle related tracker entries with protection
|
|
if (conversionRecord && conversionRecord.transactionId) {
|
|
const transactionId = conversionRecord.transactionId;
|
|
const fromName = conversionRecord.fromName;
|
|
const toName = conversionRecord.toName;
|
|
|
|
console.log(`Processing deletion for transaction ${transactionId}: ${fromName} → ${toName}`);
|
|
|
|
// Get all coin trackers
|
|
const trackers = document.querySelectorAll('.coin-tracker');
|
|
|
|
// STEP 1: First find and process ONLY the destination entries across all trackers
|
|
trackers.forEach(tracker => {
|
|
const tbody = tracker.querySelector('table.coin-table tbody');
|
|
if (!tbody) return;
|
|
|
|
// Find destination entries first - we ONLY delete these
|
|
const destRows = tbody.querySelectorAll(`tr[data-transaction-id="${transactionId}"][data-transaction-type="conversion-in"], tr[data-transaction-id="${transactionId}"][data-transaction-type="income-transfer-in"]`);
|
|
|
|
console.log(`Found ${destRows.length} destination rows in tracker ${tracker.id}`);
|
|
|
|
// Delete ONLY destination rows with extreme caution
|
|
destRows.forEach(row => {
|
|
// TRIPLE-CHECK that this is NOT a source row under any circumstances
|
|
const isSafeToDelete =
|
|
// 1. Check it does not have the source marker
|
|
row.dataset.sourceRow !== 'true' &&
|
|
// 2. Check it is explicitly a destination type
|
|
(row.dataset.transactionType === 'conversion-in' || row.dataset.transactionType === 'income-transfer-in');
|
|
|
|
// Make sure this is a tracker for the destination asset before deleting
|
|
const trackerNameInput = tracker.querySelector(".coin-tracker-header .coin-name-input");
|
|
const trackerName = trackerNameInput ? trackerNameInput.value : "";
|
|
|
|
if (!isSafeToDelete) {
|
|
console.log(`SAFETY BLOCK: Prevented deletion of row that might be a source row`);
|
|
} else if (trackerName === toName) {
|
|
console.log(`Removing destination row in ${toName}`);
|
|
row.remove(); // ONLY destination rows are deleted
|
|
} else {
|
|
console.log(`NOT removing destination row in ${trackerName || tracker.id} because it's not the target asset ${toName}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// STEP 2: Now find and modify ONLY source entries across all trackers
|
|
console.log("STEP 2: Processing source entries - These should NEVER be deleted, only notes cleared");
|
|
trackers.forEach(tracker => {
|
|
const tbody = tracker.querySelector('table.coin-table tbody');
|
|
if (!tbody) return;
|
|
|
|
// Get all rows that might be source entries
|
|
const allRows = tbody.querySelectorAll('tr');
|
|
|
|
allRows.forEach(row => {
|
|
// Method 1: Check by explicit source row marker (highest priority)
|
|
const isSourceByMarker = row.dataset && row.dataset.sourceRow === 'true';
|
|
|
|
// Method 2: Check by transaction ID and transaction type
|
|
const isSourceByID = row.dataset &&
|
|
row.dataset.transactionId === transactionId &&
|
|
row.dataset.transactionType !== 'conversion-in' &&
|
|
row.dataset.transactionType !== 'income-transfer-in';
|
|
|
|
// Method 3: Check by specific source transaction types
|
|
const isSourceByType = row.dataset &&
|
|
row.dataset.transactionType &&
|
|
(row.dataset.transactionType === 'conversion-out' ||
|
|
row.dataset.transactionType === 'income-source' ||
|
|
row.dataset.transactionType === 'income-transfer-note' ||
|
|
row.dataset.transactionType === 'source-recovered');
|
|
|
|
// Method 4: Check by notes content
|
|
let isSourceByNotes = false;
|
|
const notesCell = row.cells && row.cells[7] ? row.cells[7] : null;
|
|
const notesInput = notesCell ? notesCell.querySelector("input") : null;
|
|
const notes = notesInput ? notesInput.value : "";
|
|
|
|
if (notes.includes(`TRANSFERRED: $`) && notes.includes(`to buy ${toName}`)) {
|
|
isSourceByNotes = true;
|
|
}
|
|
if (notes.includes(`Moved $`) && notes.includes(`to ${toName}`)) {
|
|
isSourceByNotes = true;
|
|
}
|
|
|
|
// Combine all source detection methods
|
|
const isSourceRowFound = isSourceByMarker || isSourceByID || isSourceByType || isSourceByNotes;
|
|
|
|
// CRITICAL PROTECTION: Immediately mark any potential source row to prevent deletion
|
|
if (isSourceRowFound) {
|
|
// Add the source marker BEFORE doing anything else to protect this row
|
|
row.dataset.sourceRow = "true";
|
|
|
|
console.log(`PROTECTED SOURCE ROW FOUND in ${tracker.id}: ${
|
|
isSourceByMarker ? "by explicit marker" :
|
|
isSourceByID ? "by transaction ID" :
|
|
isSourceByType ? "by transaction type" :
|
|
"by notes content"
|
|
}`);
|
|
|
|
// NEVER delete source rows, just clear the transfer note
|
|
if (notesInput) {
|
|
// Only modify notes if they contain transfer information relating to this transaction
|
|
const containsRelevantTransfer =
|
|
(notes.includes(`TRANSFERRED: $`) && notes.includes(`to buy ${toName}`)) ||
|
|
(notes.includes(`Moved $`) && notes.includes(`to ${toName}`));
|
|
|
|
if (containsRelevantTransfer) {
|
|
const transferTextPattern = /TRANSFERRED: \$[\d.]+( to buy [^;]+)?/;
|
|
const movedTextPattern = /Moved \$[\d.]+( to [^;]+)?/;
|
|
let newNotes = notes;
|
|
newNotes = newNotes.replace(transferTextPattern, "");
|
|
newNotes = newNotes.replace(movedTextPattern, "");
|
|
// Clean up any trailing or leading semicolons and double semicolons
|
|
newNotes = newNotes.replace(/^;\s*/, "").replace(/;\s*$/, "").replace(/;\s*;/g, ";");
|
|
|
|
notesInput.value = newNotes;
|
|
console.log(`Source row notes after: "${newNotes}"`);
|
|
console.log(`KEPT source row - only cleared transfer note`);
|
|
} else {
|
|
console.log(`Source row notes don't contain relevant transfer info for ${toName}, keeping unchanged`);
|
|
}
|
|
|
|
// Tag this row if it wasn't already tagged
|
|
if (!isSourceByID) {
|
|
row.dataset.transactionId = transactionId;
|
|
row.dataset.transactionType = "source-recovered";
|
|
console.log("Tagged source row for future operations");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Update all trackers after deletion
|
|
document.querySelectorAll('.coin-tracker').forEach(tracker => {
|
|
const trackerId = tracker.id;
|
|
updateCoinTrackerSummary(trackerId);
|
|
});
|
|
|
|
// Save all trackers
|
|
saveCoinTrackers();
|
|
}
|
|
|
|
// Remove from the array using the actual index, not the display index
|
|
if (actualIndex !== -1) {
|
|
conversionHistory.splice(actualIndex, 1);
|
|
console.log(`Removed conversion record at actual index ${actualIndex}`);
|
|
} else {
|
|
console.error("Could not find actual index for conversion record, deletion skipped");
|
|
}
|
|
|
|
// Save back to local storage
|
|
localStorage.setItem("conversionHistory", JSON.stringify(conversionHistory));
|
|
|
|
// Remove the row from the table
|
|
row.remove();
|
|
|
|
// Update conversion stats
|
|
updateConversionStats();
|
|
|
|
// Update total investment display
|
|
if (typeof updateTotalInvested === 'function') {
|
|
updateTotalInvested();
|
|
}
|
|
}
|
|
});
|
|
|
|
actionsCell.appendChild(deleteBtn);
|
|
row.appendChild(actionsCell);
|
|
|
|
// Append the row to the table body
|
|
tableBody.appendChild(row);
|
|
});
|
|
};
|
|
|
|
// Function to update conversion statistics
|
|
const updateConversionStats = () => {
|
|
try {
|
|
const statsTable = document.querySelector("#conversionStatsTable tbody");
|
|
if (!statsTable) {
|
|
console.warn("Conversion stats table tbody not found in the DOM");
|
|
return;
|
|
}
|
|
|
|
// Get conversion history
|
|
const conversionHistory = JSON.parse(localStorage.getItem("conversionHistory") || "[]");
|
|
|
|
// Track flows by asset
|
|
const assetFlows = {};
|
|
|
|
// Process all conversions
|
|
conversionHistory.forEach(conversion => {
|
|
// Skip invalid conversions
|
|
if (!conversion || !conversion.fromName || !conversion.toName) return;
|
|
|
|
// Initialize asset records if they don't exist
|
|
if (!assetFlows[conversion.fromName]) {
|
|
assetFlows[conversion.fromName] = { in: 0, out: 0 };
|
|
}
|
|
if (!assetFlows[conversion.toName]) {
|
|
assetFlows[conversion.toName] = { in: 0, out: 0 };
|
|
}
|
|
|
|
// Determine the dollar amount (support both new and old data format)
|
|
const dollarAmount = conversion.dollarAmount || conversion.sentAmount || 0;
|
|
|
|
// Record the flow (same dollar amount for both directions)
|
|
assetFlows[conversion.fromName].out += dollarAmount;
|
|
assetFlows[conversion.toName].in += dollarAmount;
|
|
});
|
|
|
|
// Clear existing rows
|
|
statsTable.innerHTML = "";
|
|
|
|
// Create rows for each asset
|
|
Object.keys(assetFlows).sort().forEach(asset => {
|
|
const flow = assetFlows[asset];
|
|
const netFlow = flow.in - flow.out;
|
|
|
|
const row = document.createElement("tr");
|
|
|
|
const assetCell = document.createElement("td");
|
|
assetCell.textContent = asset;
|
|
row.appendChild(assetCell);
|
|
|
|
const inCell = document.createElement("td");
|
|
inCell.textContent = `$${flow.in.toFixed(2)}`;
|
|
row.appendChild(inCell);
|
|
|
|
const outCell = document.createElement("td");
|
|
outCell.textContent = `$${flow.out.toFixed(2)}`;
|
|
row.appendChild(outCell);
|
|
|
|
const netCell = document.createElement("td");
|
|
netCell.textContent = `$${netFlow.toFixed(2)}`;
|
|
netCell.style.color = netFlow >= 0 ? 'green' : 'red';
|
|
row.appendChild(netCell);
|
|
|
|
statsTable.appendChild(row);
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating conversion stats:", error);
|
|
// Don't show alert to user as this is not critical
|
|
}
|
|
};
|
|
|
|
// Add these functions after your existing functions but before the DOMContentLoaded event
|
|
|
|
// Record asset transfer between trackers - dollar value based
|
|
const recordAssetTransfer = (fromId, toId, dollarAmount, transferType = 'principal', selectedIncomeData = null) => {
|
|
console.log("Recording asset transfer:", {fromId, toId, dollarAmount, transferType, selectedIncomeData});
|
|
console.log("fromId type:", typeof fromId, "toId type:", typeof toId);
|
|
|
|
// Make sure we can find the tracker elements
|
|
console.log("Looking for element with ID:", fromId);
|
|
const fromElement = document.getElementById(fromId);
|
|
console.log("Direct document.getElementById result:", fromElement);
|
|
|
|
const fromTracker = $(fromId);
|
|
const toTracker = $(toId);
|
|
|
|
console.log("Tracker elements:", {fromTracker, toTracker});
|
|
|
|
if (!fromTracker || !toTracker || dollarAmount <= 0) {
|
|
alert("Please select valid source and destination trackers and enter a positive dollar amount");
|
|
console.error("Invalid transfer parameters:", {fromTracker, toTracker, dollarAmount});
|
|
return;
|
|
}
|
|
|
|
// Get coin names for logging
|
|
const fromName = fromTracker.querySelector(".coin-tracker-header input.coin-name-input").value;
|
|
const toName = toTracker.querySelector(".coin-tracker-header input.coin-name-input").value;
|
|
|
|
const sourceTable = fromTracker.querySelector("table.coin-table");
|
|
const sourceTbody = sourceTable.querySelector("tbody");
|
|
const today = getToday();
|
|
const currentTime = getBerlinTime();
|
|
|
|
// Record the transaction ID for linked entries (used for deletion)
|
|
const transactionId = `transfer_${Date.now()}`;
|
|
|
|
if (transferType === 'income') {
|
|
// For income transfer - ALWAYS try to add note to a source row instead of creating a negative entry
|
|
|
|
if (selectedIncomeData) {
|
|
// User has selected a specific income row to use
|
|
const incomeRowIndex = selectedIncomeData.rowIndex;
|
|
const sourceRows = Array.from(sourceTbody.rows);
|
|
|
|
if (incomeRowIndex >= 0 && incomeRowIndex < sourceRows.length) {
|
|
const incomeRow = sourceRows[incomeRowIndex];
|
|
// Update the notes field in the income row
|
|
const notesCell = incomeRow.cells[7]; // Notes cell
|
|
const notesInput = notesCell ? notesCell.querySelector("input") : null;
|
|
|
|
if (notesInput) {
|
|
// Make very visible notes about the transfer
|
|
const currentNotes = notesInput.value;
|
|
const transferNote = `TRANSFERRED: $${dollarAmount.toFixed(2)} to buy ${toName}`;
|
|
notesInput.value = currentNotes ?
|
|
`${currentNotes}; ${transferNote}` :
|
|
transferNote;
|
|
|
|
// Set data attribute to track this transaction
|
|
incomeRow.dataset.transactionId = transactionId;
|
|
incomeRow.dataset.incomeUsed = dollarAmount;
|
|
incomeRow.dataset.usedForTracker = toId;
|
|
|
|
// Optionally update the row style to indicate it was used in a transfer
|
|
incomeRow.style.backgroundColor = "rgba(255, 165, 0, 0.1)"; // Light orange background
|
|
}
|
|
}
|
|
} else {
|
|
// No specific row selected - try to find any positive income row to add note to
|
|
// Get rows as an array and reverse to start with the newest rows first
|
|
const sourceRows = Array.from(sourceTbody.rows).reverse();
|
|
let foundRow = false;
|
|
|
|
console.log(`Checking ${sourceRows.length} source rows for income to transfer`);
|
|
|
|
// Try to find a row with positive income, starting with the newest rows
|
|
for (const row of sourceRows) {
|
|
const incomeInput = row.cells[3].querySelector("input");
|
|
const income = parseFloat(incomeInput?.value) || 0;
|
|
|
|
console.log(`Checking row with income: ${income}, need: ${dollarAmount}`);
|
|
|
|
if (income > 0 && income >= dollarAmount) {
|
|
// Found a suitable income row, update its notes
|
|
const notesCell = row.cells[7]; // Notes cell
|
|
const notesInput = notesCell ? notesCell.querySelector("input") : null;
|
|
|
|
if (notesInput) {
|
|
const currentNotes = notesInput.value;
|
|
const transferNote = `TRANSFERRED: $${dollarAmount.toFixed(2)} to buy ${toName}`;
|
|
notesInput.value = currentNotes ?
|
|
`${currentNotes}; ${transferNote}` :
|
|
transferNote;
|
|
|
|
console.log(`Added transfer note to row with income ${income}`);
|
|
|
|
// Mark this row as involved in the transaction
|
|
row.dataset.transactionId = transactionId;
|
|
row.dataset.incomeUsed = dollarAmount;
|
|
row.dataset.usedForTracker = toId;
|
|
|
|
// Make the row visually distinct
|
|
row.style.backgroundColor = "rgba(255, 165, 0, 0.1)"; // Light orange background
|
|
|
|
foundRow = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only if we couldn't find a suitable income row, create a new entry
|
|
if (!foundRow) {
|
|
console.log("No suitable income row found, creating a note row without negative income");
|
|
// Create a note entry rather than a negative income entry
|
|
addDynamicEntry(sourceTbody, {
|
|
date: today,
|
|
time: currentTime,
|
|
currentValue: 0, // Don't change the principal amount
|
|
income: 0, // No negative income - just a note
|
|
compound: 0,
|
|
dailyYield: 0,
|
|
notes: `TRANSFERRED: $${dollarAmount.toFixed(2)} to buy ${toName}`,
|
|
transactionType: "income-transfer-note",
|
|
transactionId: transactionId // Add transaction ID for linked entries
|
|
}, fromId);
|
|
}
|
|
}
|
|
} else {
|
|
// Regular principal transfer - subtract from current value of source
|
|
addDynamicEntry(sourceTbody, {
|
|
date: today,
|
|
time: currentTime,
|
|
currentValue: -dollarAmount, // Use negative value to represent money leaving this tracker
|
|
income: 0,
|
|
compound: 0,
|
|
dailyYield: 0,
|
|
notes: `Moved $${dollarAmount.toFixed(2)} to ${toName}`,
|
|
transactionType: "conversion-out",
|
|
transactionId: transactionId // Add transaction ID for linked entries
|
|
}, fromId);
|
|
}
|
|
|
|
// 2. Add a "conversion in" row to destination tracker
|
|
const destTable = toTracker.querySelector("table.coin-table");
|
|
const destTbody = destTable.querySelector("tbody");
|
|
|
|
// Get the last current value from the destination tracker
|
|
// This is key to the first improvement - add to the last known value instead of standalone value
|
|
let lastDestValue = 0;
|
|
const destRows = Array.from(destTbody.querySelectorAll("tr"));
|
|
if (destRows.length > 0) {
|
|
// Get the value from the last row in the destination tracker
|
|
const lastRow = destRows[destRows.length - 1];
|
|
const currentValueInput = lastRow.cells[2].querySelector("input");
|
|
if (currentValueInput) {
|
|
lastDestValue = parseFloat(currentValueInput.value) || 0;
|
|
}
|
|
}
|
|
|
|
// Calculate the new destination value
|
|
const newDestValue = lastDestValue + dollarAmount;
|
|
|
|
// For destination, add to the last current value
|
|
addDynamicEntry(destTbody, {
|
|
date: today,
|
|
time: currentTime,
|
|
currentValue: newDestValue, // Add to the last destination value
|
|
income: 0,
|
|
compound: 0,
|
|
dailyYield: 0,
|
|
notes: transferType === 'income'
|
|
? `BOUGHT: ${toName} using $${dollarAmount.toFixed(2)} income from ${fromName} (previous value: $${lastDestValue.toFixed(2)})`
|
|
: `BOUGHT: ${toName} using $${dollarAmount.toFixed(2)} from ${fromName} (previous value: $${lastDestValue.toFixed(2)})`,
|
|
transactionType: transferType === 'income' ? "income-transfer-in" : "conversion-in",
|
|
transactionId: transactionId // Add transaction ID for linked entries
|
|
}, toId);
|
|
|
|
// Log the conversion to history with additional details for better tracking
|
|
logConversion({
|
|
date: today,
|
|
time: currentTime,
|
|
fromName: fromName,
|
|
toName: toName,
|
|
dollarAmount: dollarAmount,
|
|
transferType: transferType,
|
|
destPreviousValue: lastDestValue, // Store the previous value for reference
|
|
destNewValue: newDestValue, // Store the new combined value
|
|
transactionId: transactionId // Store the transaction ID for linked deletion
|
|
});
|
|
|
|
// Update both trackers
|
|
updateCoinTrackerSummary(fromId);
|
|
updateCoinTrackerSummary(toId);
|
|
saveCoinTrackers();
|
|
|
|
// Update conversion stats - wrap in try/catch to prevent errors
|
|
try {
|
|
updateConversionStats();
|
|
} catch (error) {
|
|
console.error("Error updating conversion stats:", error);
|
|
// Don't show alert to user as this is not critical
|
|
}
|
|
|
|
// Update asset dropdowns in case there are new trackers
|
|
updateAssetDropdowns();
|
|
|
|
alert(`Transferred $${dollarAmount.toFixed(2)} from ${fromName} to ${toName}`);
|
|
};
|
|
|
|
// Update asset dropdowns to show available coin trackers
|
|
const updateAssetDropdowns = () => {
|
|
console.log("Updating asset dropdowns");
|
|
const fromSelect = document.getElementById('fromAsset');
|
|
const toSelect = document.getElementById('toAsset');
|
|
|
|
if (!fromSelect || !toSelect) {
|
|
console.warn("Asset dropdown elements not found, make sure the HTML is correctly set up");
|
|
return;
|
|
}
|
|
|
|
// Clear and initialize dropdowns with default option
|
|
fromSelect.innerHTML = '<option value="">-- Select Source --</option>';
|
|
toSelect.innerHTML = '<option value="">-- Select Destination --</option>';
|
|
|
|
// Get all active coin trackers
|
|
const container = document.getElementById('coinTrackerContainer');
|
|
if (!container) {
|
|
console.warn("Coin tracker container not found");
|
|
return;
|
|
}
|
|
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
console.log(`Found ${trackerDivs.length} coin trackers for dropdown population`);
|
|
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.active === "false") return;
|
|
|
|
const trackerId = div.id;
|
|
const coinNameInput = div.querySelector(".coin-tracker-header input.coin-name-input");
|
|
const coinName = coinNameInput ? coinNameInput.value : "Unnamed";
|
|
|
|
console.log(`Adding ${coinName} (${trackerId}) to asset dropdowns`);
|
|
|
|
// Add to source dropdown
|
|
const sourceOption = document.createElement("option");
|
|
sourceOption.value = trackerId;
|
|
sourceOption.textContent = coinName;
|
|
fromSelect.appendChild(sourceOption);
|
|
|
|
// Add to destination dropdown
|
|
const destOption = document.createElement("option");
|
|
destOption.value = trackerId;
|
|
destOption.textContent = coinName;
|
|
toSelect.appendChild(destOption);
|
|
});
|
|
};
|
|
|
|
// Function to update the income rows dropdown with available income entries from the selected tracker
|
|
const updateIncomeRowsDropdown = (trackerId) => {
|
|
console.log("Updating income rows dropdown for tracker:", trackerId);
|
|
const incomeSelect = document.getElementById('incomeRowSelection');
|
|
|
|
if (!incomeSelect) {
|
|
console.warn("Income rows dropdown not found");
|
|
return;
|
|
}
|
|
|
|
// Clear existing options
|
|
incomeSelect.innerHTML = '<option value="">-- Select Income Row --</option>';
|
|
|
|
// If no tracker is selected, exit
|
|
if (!trackerId) return;
|
|
|
|
const tracker = $(trackerId);
|
|
if (!tracker) return;
|
|
|
|
const table = tracker.querySelector("table.coin-table");
|
|
if (!table) return;
|
|
|
|
const tbody = table.querySelector("tbody");
|
|
if (!tbody) return;
|
|
|
|
// Find rows with positive income values
|
|
const rows = Array.from(tbody.rows);
|
|
|
|
// Track if we found any income rows
|
|
let foundIncomeRows = false;
|
|
|
|
rows.forEach((row, index) => {
|
|
const dateInput = row.cells[0].querySelector("input");
|
|
const timeInput = row.cells[1].querySelector("input");
|
|
const incomeInput = row.cells[3].querySelector("input");
|
|
|
|
const income = parseFloat(incomeInput.value) || 0;
|
|
|
|
// Only add entries with positive income
|
|
if (income > 0) {
|
|
foundIncomeRows = true;
|
|
const date = dateInput.value;
|
|
const time = timeInput.value;
|
|
const option = document.createElement("option");
|
|
// Store index and income value in the option value as JSON
|
|
option.value = JSON.stringify({
|
|
rowIndex: index,
|
|
income: income,
|
|
date: date,
|
|
time: time
|
|
});
|
|
|
|
// Display date and income value
|
|
option.textContent = `${date} ${time} - $${income.toFixed(2)}`;
|
|
incomeSelect.appendChild(option);
|
|
}
|
|
});
|
|
|
|
// If no income rows found, add an option indicating this
|
|
if (!foundIncomeRows) {
|
|
const option = document.createElement("option");
|
|
option.value = "";
|
|
option.textContent = "No income entries found";
|
|
option.disabled = true;
|
|
incomeSelect.appendChild(option);
|
|
}
|
|
};
|
|
|
|
// Add event listeners for the conversion form controls
|
|
const conversionTypeSelect = $('conversionType');
|
|
const fromAssetSelect = $('fromAsset');
|
|
|
|
// Show/hide income row selection based on conversion type
|
|
if (conversionTypeSelect) {
|
|
conversionTypeSelect.addEventListener('change', () => {
|
|
const incomeSelectionDiv = $('incomeRowSelectionDiv');
|
|
if (incomeSelectionDiv) {
|
|
incomeSelectionDiv.style.display = conversionTypeSelect.value === 'income' ? 'block' : 'none';
|
|
|
|
// If switching to income and a source is already selected, update the income rows dropdown
|
|
if (conversionTypeSelect.value === 'income' && fromAssetSelect.value) {
|
|
updateIncomeRowsDropdown(fromAssetSelect.value);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Trigger change event to set initial state
|
|
conversionTypeSelect.dispatchEvent(new Event('change'));
|
|
}
|
|
|
|
// Update income rows dropdown when source asset changes
|
|
if (fromAssetSelect) {
|
|
fromAssetSelect.addEventListener('change', () => {
|
|
const conversionTypeSelect = $('conversionType');
|
|
if (conversionTypeSelect && conversionTypeSelect.value === 'income') {
|
|
updateIncomeRowsDropdown(fromAssetSelect.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Immediate debug logging
|
|
console.log("🔥 Script loaded, document ready state:", document.readyState);
|
|
console.log("🔥 Setting up DOMContentLoaded listener...");
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
console.log("🔥 DOM Content Loaded - Setting up event handlers");
|
|
console.log("🔥 Starting analysis event listener setup...");
|
|
console.log("🔥 Location:", window.location.href);
|
|
console.log("🔥 Document ready state:", document.readyState);
|
|
|
|
// Make sure to update asset dropdowns when page loads
|
|
updateAssetDropdowns();
|
|
|
|
document.querySelectorAll(".number-input button").forEach(button => {
|
|
const handleInteraction = e => {
|
|
e.preventDefault();
|
|
const targetId = button.getAttribute("data-target");
|
|
const delta = parseFloat(button.getAttribute("data-delta"));
|
|
adjustValue(targetId, delta);
|
|
};
|
|
if (window.PointerEvent) {
|
|
button.addEventListener("pointerup", handleInteraction, false);
|
|
} else {
|
|
button.addEventListener("click", handleInteraction, false);
|
|
button.addEventListener("touchend", handleInteraction, false);
|
|
}
|
|
});
|
|
['initialPrincipal', 'dailyRate', 'termDays', 'reinvestRate'].forEach(id => {
|
|
$(id).addEventListener("input", calculate);
|
|
});
|
|
$('totalAmountPercent').addEventListener("input", calculatePercentage);
|
|
$('givenAmount').addEventListener("input", calculatePercentage);
|
|
calculatePercentage();
|
|
$('totalAmountPercent2').addEventListener("input", calculatePercentage2);
|
|
$('percentInput2').addEventListener("input", calculatePercentage2);
|
|
calculatePercentage2();
|
|
setupCollapsibles();
|
|
$('transferSettingsBtn').addEventListener("click", transferSettings);
|
|
$('addCoinTrackerBtn').addEventListener("click", () => {
|
|
addCoinTracker();
|
|
// No need to call saveCoinTrackers() here as it's now called inside addCoinTracker
|
|
});
|
|
|
|
// Add event listener for the Export Coin Trackers button
|
|
$('exportTrackersBtn').addEventListener("click", exportCoinTrackers);
|
|
|
|
calculate();
|
|
loadCoinTrackers();
|
|
updateTransferDropdown();
|
|
|
|
// Simplified chart display code
|
|
const showInvestedBtn = $('showInvestedChartBtn');
|
|
const showIncomeBtn = $('showIncomeChartBtn');
|
|
|
|
// Default display - only show invested chart
|
|
$('investedChart').style.display = "block";
|
|
$('incomeChart').style.display = "none";
|
|
|
|
// Button click handlers
|
|
showInvestedBtn.addEventListener("click", () => {
|
|
$('investedChart').style.display = "block";
|
|
$('incomeChart').style.display = "none";
|
|
});
|
|
|
|
showIncomeBtn.addEventListener("click", () => {
|
|
$('investedChart').style.display = "none";
|
|
$('incomeChart').style.display = "block";
|
|
});
|
|
|
|
// Event listeners for the new reset and delete graph value buttons
|
|
$('resetGraphValueBtn').addEventListener("click", resetGraphValue);
|
|
$('deleteGraphDataPointBtn').addEventListener("click", deleteGraphDataPoint);
|
|
|
|
['investedChart','incomeChart','dailyYieldChart'].forEach(chartId => {
|
|
const chartEl = document.getElementById(chartId);
|
|
chartEl.addEventListener('click', () => {
|
|
chartEl.classList.toggle('chart-fullscreen');
|
|
// Resize chart if needed
|
|
if (window[chartId]) {
|
|
window[chartId].resize();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add event listener for reinvest rate changes
|
|
$('reinvestRate').addEventListener("input", updateAnalysisTable);
|
|
|
|
// Add event listeners for analysis controls - live updates
|
|
console.log("🔥 Setting up analysis event listeners...");
|
|
const reinvestRateEl = $('analysisReinvestRate');
|
|
const analysisDaysEl = $('analysisDays');
|
|
|
|
console.log("🔥 Analysis elements found:", !!reinvestRateEl, !!analysisDaysEl);
|
|
console.log("🔥 Reinvest element:", reinvestRateEl);
|
|
console.log("🔥 Days element:", analysisDaysEl);
|
|
console.log("🔥 Current values:", reinvestRateEl?.value, analysisDaysEl?.value);
|
|
|
|
if (reinvestRateEl) {
|
|
console.log("🔥 Adding event listeners to reinvest rate element");
|
|
const inputHandler = () => {
|
|
console.log("🔥 Reinvest rate INPUT event - new value:", reinvestRateEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
const keyupHandler = () => {
|
|
console.log("🔥 Reinvest rate KEYUP event - new value:", reinvestRateEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
const changeHandler = () => {
|
|
console.log("🔥 Reinvest rate CHANGE event - new value:", reinvestRateEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
|
|
reinvestRateEl.addEventListener("input", inputHandler);
|
|
reinvestRateEl.addEventListener("keyup", keyupHandler);
|
|
reinvestRateEl.addEventListener("change", changeHandler);
|
|
|
|
console.log("🔥 Event listeners attached to reinvest rate element");
|
|
} else {
|
|
console.log("🔥 analysisReinvestRate element not found");
|
|
}
|
|
|
|
if (analysisDaysEl) {
|
|
console.log("🔥 Adding event listeners to analysis days element");
|
|
const inputHandler = () => {
|
|
console.log("🔥 Analysis days INPUT event - new value:", analysisDaysEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
const keyupHandler = () => {
|
|
console.log("🔥 Analysis days KEYUP event - new value:", analysisDaysEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
const changeHandler = () => {
|
|
console.log("🔥 Analysis days CHANGE event - new value:", analysisDaysEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
|
|
analysisDaysEl.addEventListener("input", inputHandler);
|
|
analysisDaysEl.addEventListener("keyup", keyupHandler);
|
|
analysisDaysEl.addEventListener("change", changeHandler);
|
|
|
|
console.log("🔥 Event listeners attached to analysis days element");
|
|
} else {
|
|
console.log("🔥 analysisDays element not found");
|
|
}
|
|
|
|
// Backup and restore functionality
|
|
$('createBackupBtn').addEventListener('click', createManualBackup);
|
|
$('restoreBackupBtn').addEventListener('click', () => {
|
|
const file = $('restoreBackupBtn').dataset.file;
|
|
restoreFromBackup(file);
|
|
});
|
|
|
|
// Set up conversion button - MOVED HERE to ensure proper initialization
|
|
const executeBtn = $('executeConversion');
|
|
console.log("Finding executeConversion button:", executeBtn);
|
|
if (executeBtn) {
|
|
executeBtn.addEventListener('click', () => {
|
|
console.log("Execute conversion button clicked");
|
|
|
|
// Check if element exists
|
|
const amountInput = document.getElementById('conversionAmount');
|
|
console.log("Amount input element:", amountInput);
|
|
console.log("Amount input value:", amountInput ? amountInput.value : "ELEMENT NOT FOUND");
|
|
|
|
const fromAsset = $('fromAsset').value;
|
|
const toAsset = $('toAsset').value;
|
|
const dollarAmount = parseFloat(amountInput ? amountInput.value : "0") || 0;
|
|
const transferType = $('conversionType').value;
|
|
|
|
// Get selected income row data if applicable
|
|
let selectedIncomeData = null;
|
|
if (transferType === 'income') {
|
|
const incomeSelect = $('incomeRowSelection');
|
|
if (incomeSelect && incomeSelect.value) {
|
|
try {
|
|
selectedIncomeData = JSON.parse(incomeSelect.value);
|
|
console.log("Selected income data:", selectedIncomeData);
|
|
} catch (e) {
|
|
console.error("Error parsing selected income data:", e);
|
|
}
|
|
}
|
|
|
|
// If income transfer is selected but no income row is selected, show an error
|
|
if (!selectedIncomeData && document.getElementById('incomeRowSelectionDiv').style.display !== 'none') {
|
|
alert("Please select an income row to use for the transfer.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.log("Transfer values:", {fromAsset, toAsset, dollarAmount, transferType, selectedIncomeData});
|
|
console.log("Dollar amount is a number?", !isNaN(dollarAmount));
|
|
console.log("Dollar amount > 0?", dollarAmount > 0);
|
|
|
|
if (fromAsset && toAsset && dollarAmount > 0) {
|
|
console.log("All validation passed, calling recordAssetTransfer");
|
|
try {
|
|
recordAssetTransfer(fromAsset, toAsset, dollarAmount, transferType, selectedIncomeData);
|
|
// Reset the amount field after successful conversion
|
|
$('conversionAmount').value = '';
|
|
// Update dropdowns after conversion in case names have changed
|
|
updateAssetDropdowns();
|
|
} catch (error) {
|
|
console.error("Error during asset transfer:", error);
|
|
alert("Error during transfer: " + error.message);
|
|
}
|
|
} else {
|
|
alert("Please fill in all fields with valid values.");
|
|
console.warn("Invalid form values:", {fromAsset, toAsset, dollarAmount});
|
|
}
|
|
});
|
|
} else {
|
|
console.error("Could not find executeConversion button");
|
|
}
|
|
|
|
// Load backup list when page loads
|
|
loadBackupList();
|
|
|
|
// Enable fullscreen toggle for each chart canvas
|
|
["investedChart", "incomeChart"].forEach(id => {
|
|
// Removed "dailyYieldChart" from this array
|
|
const chartCanvas = document.getElementById(id);
|
|
if (!chartCanvas) return;
|
|
chartCanvas.style.cursor = "pointer";
|
|
chartCanvas.addEventListener("click", () => {
|
|
chartCanvas.classList.toggle("chart-fullscreen");
|
|
chartCanvas.style.cursor = chartCanvas.classList.contains("chart-fullscreen")
|
|
? "zoom-out"
|
|
: "pointer";
|
|
});
|
|
});
|
|
|
|
updateAnalysisTable();
|
|
|
|
// Populate asset dropdown menus
|
|
updateAssetDropdowns();
|
|
|
|
// Set up conversion button - this was moved to the DOMContentLoaded event handler
|
|
// to ensure proper initialization and avoid duplicate event listeners
|
|
|
|
// Also add these calls to update dropdowns when trackers change
|
|
const addCoinTrackerOriginal = addCoinTracker;
|
|
addCoinTracker = function(data) {
|
|
const result = addCoinTrackerOriginal(data);
|
|
if (typeof updateAssetDropdowns === 'function') {
|
|
updateAssetDropdowns();
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const saveCoinTrackersOriginal = saveCoinTrackers;
|
|
saveCoinTrackers = function() {
|
|
const result = saveCoinTrackersOriginal();
|
|
if (typeof updateAssetDropdowns === 'function') {
|
|
updateAssetDropdowns();
|
|
}
|
|
return result;
|
|
};
|
|
});
|
|
|
|
// Expose the updateCoinTrackerSummary function globally
|
|
window.updateCoinTrackerSummary = updateCoinTrackerSummary;
|
|
|
|
// Expose updateConversionStats globally for use by direct-converter.js
|
|
window.updateConversionStats = updateConversionStats;
|
|
|
|
// Expose recordAssetTransfer globally for use by direct-converter.js
|
|
window.recordAssetTransfer = recordAssetTransfer;
|
|
|
|
// Helper function to update all trackers
|
|
const updateAllTrackers = () => {
|
|
const container = $('coinTrackerContainer');
|
|
if (!container) return;
|
|
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
Array.from(trackerDivs).forEach(div => {
|
|
updateCoinTrackerSummary(div.id);
|
|
});
|
|
};
|
|
|
|
// Expose the updateAllTrackers function globally
|
|
window.updateAllTrackers = updateAllTrackers;
|
|
|
|
// Export coin trackers to CSV file with notes included
|
|
const exportCoinTrackers = () => {
|
|
console.log("Exporting coin trackers to CSV...");
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
|
|
// CSV header
|
|
let csvContent = "Coin,Platform,Date,Time,Current Value ($),Income ($),Compound ($),Daily Yield (%),Notes\n";
|
|
|
|
// Collect data from all active trackers
|
|
Array.from(trackerDivs).forEach(div => {
|
|
// Skip inactive trackers
|
|
if (div.dataset.active === "false") return;
|
|
|
|
// Get coin name and platform
|
|
const headerDiv = div.querySelector("div.coin-tracker-header");
|
|
const coinNameInput = headerDiv.querySelector("input.coin-name-input");
|
|
const coinName = coinNameInput ? coinNameInput.value.replace(/,/g, " ") : "Unnamed";
|
|
|
|
const platformInput = headerDiv.querySelector("input[list='platformListDatalist']");
|
|
const platform = platformInput ? platformInput.value.replace(/,/g, " ") : "";
|
|
|
|
// Get table data
|
|
const table = div.querySelector("table.coin-table");
|
|
if (!table) return;
|
|
|
|
const tbody = table.querySelector("tbody");
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
const date = tr.cells[0].querySelector("input").value;
|
|
const time = tr.cells[1].querySelector("input").value;
|
|
const currentValue = parseFloat(tr.cells[2].querySelector("input").value.replace("$", "")) || 0;
|
|
const income = parseFloat(tr.cells[3].querySelector("input").value) || 0;
|
|
const compound = parseFloat(tr.cells[4].querySelector("input").value) || 0;
|
|
const dailyYield = parseFloat(tr.cells[5].querySelector("input").value) || 0;
|
|
|
|
// Get notes (added to fix issue with notes not being included)
|
|
const notesCell = tr.cells[7]; // Notes is the 8th cell (index 7)
|
|
const notesInput = notesCell ? notesCell.querySelector("input") : null;
|
|
const notes = notesInput ? notesInput.value.replace(/,/g, " ").replace(/\n/g, " ") : "";
|
|
|
|
// Add row to CSV
|
|
csvContent += `"${coinName}","${platform}","${date}","${time}",${currentValue.toFixed(2)},${income.toFixed(2)},${compound.toFixed(2)},${dailyYield.toFixed(2)},"${notes}"\n`;
|
|
});
|
|
});
|
|
|
|
// Create a download link for the CSV
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement("a");
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
link.setAttribute("href", URL.createObjectURL(blob));
|
|
link.setAttribute("download", `coin-trackers-export-${timestamp}.csv`);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
console.log("Export completed");
|
|
};
|
|
|
|
// Expose the exportCoinTrackers function globally
|
|
window.exportCoinTrackers = exportCoinTrackers;
|
|
|
|
// Fallback event listener setup for analysis inputs
|
|
console.log("🔥 Setting up fallback event listeners...");
|
|
setTimeout(() => {
|
|
console.log("🔥 Fallback: Setting up analysis event listeners...");
|
|
const reinvestRateEl = document.getElementById('analysisReinvestRate');
|
|
const analysisDaysEl = document.getElementById('analysisDays');
|
|
|
|
console.log("🔥 Fallback: Analysis elements found:", !!reinvestRateEl, !!analysisDaysEl);
|
|
|
|
if (reinvestRateEl) {
|
|
console.log("🔥 Fallback: Adding event listeners to reinvest rate element");
|
|
const inputHandler = () => {
|
|
console.log("🔥 Reinvest rate INPUT event (fallback) - new value:", reinvestRateEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
|
|
reinvestRateEl.addEventListener("input", inputHandler);
|
|
reinvestRateEl.addEventListener("change", inputHandler);
|
|
|
|
console.log("🔥 Fallback: Event listeners attached to reinvest rate element");
|
|
}
|
|
|
|
if (analysisDaysEl) {
|
|
console.log("🔥 Fallback: Adding event listeners to analysis days element");
|
|
const inputHandler = () => {
|
|
console.log("🔥 Analysis days INPUT event (fallback) - new value:", analysisDaysEl.value);
|
|
updateAnalysisTable();
|
|
};
|
|
|
|
analysisDaysEl.addEventListener("input", inputHandler);
|
|
analysisDaysEl.addEventListener("change", inputHandler);
|
|
|
|
console.log("🔥 Fallback: Event listeners attached to analysis days element");
|
|
}
|
|
}, 1000); // Wait 1 second to ensure DOM is ready
|
|
})();
|
|
|