1097 lines
41 KiB
JavaScript
1097 lines
41 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
// Helper functions
|
|
const $ = id => document.getElementById(id);
|
|
const adjustValue = (id, delta) => {
|
|
const input = $(id);
|
|
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: [] };
|
|
|
|
// Update coin trackers on the backend with the given data
|
|
const updateCoinTrackersToBackend = async (trackers) => {
|
|
try {
|
|
const response = await fetch("/api/coinTrackers", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ data: trackers })
|
|
});
|
|
if (!response.ok) {
|
|
console.error("Failed to update backend:", response.statusText);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating backend:", 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";
|
|
}
|
|
};
|
|
|
|
// Revert setupCollapsibles to original behavior:
|
|
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";
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
// 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";
|
|
}
|
|
|
|
// 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 from Chart";
|
|
}
|
|
updateInvestedChart();
|
|
saveCoinTrackers(); // persist the new state
|
|
});
|
|
headerDiv.appendChild(toggleChartBtn);
|
|
|
|
const toggleActiveBtn = document.createElement("button");
|
|
toggleActiveBtn.style.marginLeft = "10px";
|
|
toggleActiveBtn.textContent = trackerDiv.dataset.active === "false" ? "Activate" : "Deactivate";
|
|
toggleActiveBtn.addEventListener("click", () => {
|
|
if (trackerDiv.dataset.active === "false") {
|
|
trackerDiv.dataset.active = "true";
|
|
toggleActiveBtn.textContent = "Deactivate";
|
|
} else {
|
|
trackerDiv.dataset.active = "false";
|
|
toggleActiveBtn.textContent = "Activate";
|
|
}
|
|
updateTotalInvested();
|
|
updateIncomeChart();
|
|
saveCoinTrackers();
|
|
});
|
|
headerDiv.appendChild(toggleActiveBtn);
|
|
|
|
headerDiv.addEventListener("click", e => {
|
|
if (!["INPUT", "BUTTON", "SPAN"].includes(e.target.tagName)) {
|
|
const contentDiv = trackerDiv.querySelector(".content");
|
|
contentDiv.style.display = (contentDiv.style.display === "none") ? "block" : "none";
|
|
}
|
|
});
|
|
trackerDiv.appendChild(headerDiv);
|
|
|
|
const contentDiv = document.createElement("div");
|
|
contentDiv.className = "content";
|
|
contentDiv.style.display = "none";
|
|
|
|
// Create initialCapitalDiv
|
|
const initialCapitalDiv = document.createElement("div");
|
|
initialCapitalDiv.style.marginBottom = "10px";
|
|
const initialCapitalLabel = document.createElement("label");
|
|
initialCapitalLabel.textContent = 'Initial Capital: ';
|
|
const initialCapitalInput = document.createElement("input");
|
|
initialCapitalInput.type = "number";
|
|
initialCapitalInput.step = "0.01";
|
|
initialCapitalInput.value = data && data.initialCapital ? data.initialCapital : "0.00";
|
|
initialCapitalDiv.appendChild(initialCapitalLabel);
|
|
initialCapitalDiv.appendChild(initialCapitalInput);
|
|
contentDiv.appendChild(initialCapitalDiv);
|
|
|
|
// Create a container with positioning context for the table
|
|
const tableWrapper = document.createElement("div");
|
|
tableWrapper.className = "table-wrapper";
|
|
tableWrapper.style.position = "relative";
|
|
|
|
const table = document.createElement("table");
|
|
table.className = "income-table coin-table";
|
|
const thead = document.createElement("thead");
|
|
thead.innerHTML = `<tr>
|
|
<th>Date</th>
|
|
<th>Time</th>
|
|
<th>Current Value</th>
|
|
<th>Income</th>
|
|
<th>Compound</th>
|
|
<th>Daily Yield (%)</th>
|
|
<th>Actions</th>
|
|
</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();
|
|
};
|
|
|
|
// 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();
|
|
|
|
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);
|
|
|
|
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 = () => {
|
|
let persistedHistory = JSON.parse(localStorage.getItem("persistedInvestedHistory") || "{}");
|
|
|
|
const container = $('coinTrackerContainer');
|
|
const trackerDivs = container.getElementsByClassName("coin-tracker");
|
|
let currentTotals = {};
|
|
|
|
Array.from(trackerDivs).forEach(div => {
|
|
if (div.dataset.showInChart === "false") return;
|
|
const table = div.querySelector("table.coin-table");
|
|
if (table) {
|
|
const tbody = table.querySelector("tbody");
|
|
let trackerDaily = {}; // { date -> { time, current } }
|
|
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;
|
|
const time = (timeInput && timeInput.value) ? timeInput.value : "00:00";
|
|
const currentInput = tr.cells[2].querySelector("input");
|
|
const currentVal = parseFloat(currentInput.value.replace("$", "")) || 0;
|
|
if (!trackerDaily[date] || time > trackerDaily[date].time) {
|
|
trackerDaily[date] = { time, current: currentVal };
|
|
}
|
|
}
|
|
});
|
|
for (const date in trackerDaily) {
|
|
currentTotals[date] = (currentTotals[date] || 0) + trackerDaily[date].current;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (Object.keys(currentTotals).length === 0) {
|
|
persistedHistory = {};
|
|
} else {
|
|
for (const date in currentTotals) {
|
|
persistedHistory[date] = currentTotals[date];
|
|
}
|
|
}
|
|
localStorage.setItem("persistedInvestedHistory", JSON.stringify(persistedHistory));
|
|
|
|
const dates = Object.keys(persistedHistory).sort();
|
|
let labels = dates.map(date => new Date(date).toLocaleDateString());
|
|
let data = dates.map(date => persistedHistory[date]);
|
|
|
|
if (labels.length > 20) {
|
|
labels = labels.slice(-20);
|
|
data = data.slice(-20);
|
|
}
|
|
|
|
investedHistory.labels = labels;
|
|
investedHistory.data = data;
|
|
|
|
if (investedChart) {
|
|
investedChart.data.labels = labels;
|
|
investedChart.data.datasets[0].data = data;
|
|
investedChart.update();
|
|
} else {
|
|
const ctx = $('investedChart').getContext('2d');
|
|
investedChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Total Invested ($)',
|
|
data: data,
|
|
fill: true,
|
|
backgroundColor: function(context) {
|
|
const ctx = context.chart.ctx;
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, 300);
|
|
gradient.addColorStop(0, 'rgba(0,123,255,0.4)');
|
|
gradient.addColorStop(1, 'rgba(0,123,255,0.1)');
|
|
return gradient;
|
|
},
|
|
borderColor: '#007bff',
|
|
borderWidth: 2,
|
|
tension: 0.3
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
animation: { duration: 500, easing: 'easeOutQuart' },
|
|
scales: {
|
|
y: { beginAtZero: true, ticks: { callback: value => '$' + value } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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 totalInvested = 0;
|
|
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");
|
|
if (tbody.rows.length > 0) {
|
|
const lastRow = tbody.rows[tbody.rows.length - 1];
|
|
const inputCurrent = lastRow.cells[2].querySelector("input");
|
|
const value = parseFloat(inputCurrent.value.replace("$", "")) || 0;
|
|
totalInvested += value;
|
|
}
|
|
}
|
|
});
|
|
let displaySpan = $('totalInvestedDisplay');
|
|
if (!displaySpan) {
|
|
displaySpan = document.createElement("span");
|
|
displaySpan.id = "totalInvestedDisplay";
|
|
const addBtn = $('addCoinTrackerBtn');
|
|
addBtn.insertAdjacentElement('afterend', displaySpan);
|
|
}
|
|
// Updated label text:
|
|
displaySpan.innerText = `Total Investment Value: $${totalInvested.toFixed(2)}`;
|
|
updateInvestedChart();
|
|
updateIncomeChart();
|
|
};
|
|
|
|
// 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
|
|
|
|
// Get initialCapital value
|
|
const initialCapitalInput = div.querySelector(".content input[type='number']");
|
|
const initialCapital = initialCapitalInput ? initialCapitalInput.value : "0.00";
|
|
|
|
const table = div.querySelector("table.coin-table");
|
|
const tbody = table.querySelector("tbody");
|
|
const rows = [];
|
|
Array.from(tbody.rows).forEach(tr => {
|
|
const date = tr.cells[0].querySelector("input").value;
|
|
const time = tr.cells[1].querySelector("input").value;
|
|
const currentValue = parseFloat(tr.cells[2].querySelector("input").value.replace("$", "")) || 0;
|
|
const income = parseFloat(tr.cells[3].querySelector("input").value) || 0;
|
|
const compound = parseFloat(tr.cells[4].querySelector("input").value) || 0;
|
|
const dailyYield = parseFloat(tr.cells[5].querySelector("input").value) || 0;
|
|
const created = tr.dataset.created;
|
|
rows.push({ date, time, currentValue, income, compound, dailyYield, created });
|
|
});
|
|
const hasData = rows.some(row => row.currentValue !== 0 || row.income !== 0 || row.dailyYield !== 0);
|
|
if (hasData) {
|
|
trackers.push({ id: trackerId, coinName, platform, initialCapital, rows, showInChart });
|
|
}
|
|
});
|
|
updateCoinTrackersToBackend(trackers);
|
|
};
|
|
|
|
// Load coin trackers exclusively from the backend.
|
|
const loadCoinTrackersFromBackend = async () => {
|
|
try {
|
|
const response = await fetch("/api/coinTrackers");
|
|
if (!response.ok) {
|
|
console.error("Error loading data from backend:", response.statusText);
|
|
return;
|
|
}
|
|
const result = await response.json();
|
|
const trackers = result.data || [];
|
|
const container = $("coinTrackerContainer");
|
|
container.innerHTML = "";
|
|
if (trackers.length === 0) {
|
|
console.log("No coin trackers found.");
|
|
} else {
|
|
trackers.forEach(tracker => addCoinTracker(tracker));
|
|
}
|
|
} catch (error) {
|
|
console.error("Fetch error while loading from backend:", 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);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
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());
|
|
calculate();
|
|
loadCoinTrackersFromBackend();
|
|
updateTransferDropdown();
|
|
|
|
// Chart toggle buttons
|
|
const showInvestedBtn = $('showInvestedChartBtn');
|
|
const showIncomeBtn = $('showIncomeChartBtn');
|
|
showInvestedBtn.addEventListener("click", () => {
|
|
$('investedChart').style.display = "block";
|
|
$('incomeChart').style.display = "none";
|
|
if (investedChart) investedChart.resize();
|
|
});
|
|
showIncomeBtn.addEventListener("click", () => {
|
|
$('investedChart').style.display = "none";
|
|
$('incomeChart').style.display = "block";
|
|
updateIncomeChart(); // This calls our updateIncomeChart function.
|
|
});
|
|
|
|
// Event listeners for the new reset and delete graph value buttons
|
|
$('resetGraphValueBtn').addEventListener("click", resetGraphValue);
|
|
$('deleteGraphDataPointBtn').addEventListener("click", deleteGraphDataPoint);
|
|
});
|
|
})();
|
|
|