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