";
}
}
function AddRow(LineArr) {
LineArr = LineArr || Array(8).fill("");
LineArr.unshift("☰");
const IOTable = document.getElementById("InOutTableBody");
const NewRow = IOTable.insertRow(IOTable.rows.length);
for (let x = 0; x < 9; x++) {
MakeCell(NewRow, x, LineArr[x]);
}
}
function SaveTable() {
const buildData = BuildDataToSave();
const now = new Date();
const day = ("0" + now.getDate()).slice(-2);
const month = ("0" + (now.getMonth() + 1)).slice(-2);
const today = `${now.getFullYear()}-${month}-${day}`;
const FileName = `finances-${today}.csv`;
const a = document.createElement('a');
a.href = 'data:application/csv;charset=utf-8,' + encodeURIComponent(buildData);
a.target = '_blank';
a.download = FileName;
a.click();
}
function ClearTable(WarnText) {
const IOTable = document.getElementById("InOutTableBody");
if (WarnText && IOTable.hasChildNodes() && !document.getElementById('nosafe').checked) {
if (!confirm(WarnText)) return;
}
while (IOTable.hasChildNodes()) {
IOTable.removeChild(IOTable.firstChild);
}
ClearForecastTable();
document.getElementById("LoadInfo").innerHTML = "";
document.getElementById("StartDate").value = "";
document.getElementById("EndDate").value = "";
document.getElementById("StartBal").value = "";
}
function LoadTable(DataIn) {
ClearTable();
const DataLinesArr = DataIn.split(/\r\n|\n/);
for (const line of DataLinesArr) {
if (line !== "") {
const DataLineArr = line.split(',');
if (DataLineArr[2] == CSV_METADATA_MARKER) {
document.getElementById("StartDate").value = DataLineArr[0];
document.getElementById("EndDate").value = DataLineArr[1];
document.getElementById("StartBal").value = DataLineArr[3];
} else {
AddRow(DataLineArr);
}
}
}
}
function DeleteRow(DivObj) {
if (!document.getElementById('nosafe').checked) {
if (!confirm("Delete row: Are you sure?")) return true;
}
const RowIndex = DivObj.parentNode.parentNode.rowIndex;
DivObj.closest('tbody').deleteRow(RowIndex - 1);
}
function AlertRow(InR, InRowDesc, InText) {
alert(`Row ${InR + 1} "${InRowDesc}":\n${InText}`);
}
function BuildForecastArr() {
ForecastArr = [];
const table = document.getElementById("InOutTableBody");
for (let r = 0; r < table.rows.length; r++) {
if (table.rows[r].cells[FREQ_COL].innerHTML === "" &&
table.rows[r].cells[START_DATE_COL].innerHTML != table.rows[r].cells[END_DATE_COL].innerHTML) {
AlertRow(r, table.rows[r].cells[NAME_COL].innerHTML, "must have Start and End dates the same or Freq specified.");
return false;
}
}
const TStartEl = document.getElementById('StartDate');
const TEndEl = document.getElementById('EndDate');
const TBalEl = document.getElementById('StartBal');
if (TStartEl.value === "") TStartEl.value = Date.today().toString('yyyy-MM-dd');
if (TEndEl.value === "" || TEndEl.value < TStartEl.value) {
TEndEl.value = Date.parse(TStartEl.value).addYears(1).toString('yyyy-MM-dd');
}
if (TBalEl.value === "" || isNaN(TBalEl.value)) TBalEl.value = 0;
const TStart = new Date.parse(TStartEl.value);
const TEnd = new Date.parse(TEndEl.value);
for (let r = 0; r < table.rows.length; r++) {
const RowStart = new Date.parse(table.rows[r].cells[START_DATE_COL].innerHTML);
let RowEnd = new Date.parse(table.rows[r].cells[END_DATE_COL].innerHTML);
const RowDesc = table.rows[r].cells[NAME_COL].innerHTML;
let RowFreqN = table.rows[r].cells[FREQ_COL].innerHTML.match(/[0-9]*/);
let RowFreqP = table.rows[r].cells[FREQ_COL].innerHTML.match(/[dDwWmMyY]/);
const RowDebit = table.rows[r].cells[DEBIT_COL].innerHTML;
const RowCredit = table.rows[r].cells[CREDIT_COL].innerHTML;
const RowColor = table.rows[r].cells[COLOR_COL].getElementsByClassName('colorpicker')[0].value;
if (!isFinite(RowStart)) {
AlertRow(r, RowDesc, "Does not have a valid start date.");
return false;
}
if (RowStart > RowEnd) {
RowEnd = RowStart;
table.rows[r].cells[END_DATE_COL].innerHTML = RowEnd.toString('yyyy-MM-dd');
}
if (RowStart > TEnd) continue;
const StartReal = (RowStart < TStart) ? TStart : RowStart;
const EndReal = (RowEnd && isFinite(RowEnd) && RowEnd < TEnd) ? RowEnd : TEnd;
let RowFreqPVal = "none";
if (RowStart.valueOf() !== RowEnd.valueOf()) {
if (!RowFreqN || RowFreqN < 1) {
AlertRow(r, RowDesc, "Freq amount less than 1.\nSetting to 1.");
RowFreqN = 1;
}
if (RowFreqN % 1 !== 0) {
RowFreqN = Math.round(RowFreqN);
AlertRow(r, RowDesc, `Freq amount non-integer.\nRounded to: ${RowFreqN}`);
}
if (!RowFreqP || "dwmy".indexOf(RowFreqP[0].toLowerCase()) === -1) {
AlertRow(r, RowDesc, "Freq type invalid or missing.\nSetting to m (month).");
RowFreqP = ["m"];
}
RowFreqPVal = RowFreqP[0].toLowerCase();
table.rows[r].cells[FREQ_COL].innerHTML = `${RowFreqN}${RowFreqPVal}`;
}
if (RowFreqPVal === "none") {
if (RowStart.between(StartReal, EndReal)) {
ForecastArr.push([RowStart.toString('yyyy-MM-dd'), RowDesc, RowDebit, RowCredit, 0, RowColor]);
}
} else {
for (let i = 0; ; i++) {
const nextDate = new Date(RowStart);
const timewarp = i * (RowFreqN || 1);
if (RowFreqPVal == "d") nextDate.addDays(timewarp);
else if (RowFreqPVal == "w") nextDate.addWeeks(timewarp);
else if (RowFreqPVal == "m") nextDate.addMonths(timewarp);
else if (RowFreqPVal == "y") nextDate.addYears(timewarp);
if (nextDate > EndReal) break;
if (nextDate.between(StartReal, EndReal)) {
ForecastArr.push([nextDate.toString('yyyy-MM-dd'), RowDesc, RowDebit, RowCredit, 0, RowColor]);
}
}
}
}
ForecastArr.sort((a, b) => (a[0] + a[1]).toLowerCase().localeCompare((b[0] + b[1]).toLowerCase()));
SaveBuildData();
return true;
}
function ClearForecastTable() {
const ForeCastClear = document.getElementById("forecastTrack");
while (ForeCastClear.hasChildNodes()) {
ForeCastClear.removeChild(ForeCastClear.lastChild);
}
const pointers = ["lowpointer", "highpointer", "warnpointer"];
pointers.forEach(p => {
const pointerEl = document.getElementById(p);
if (pointerEl) {
pointerEl.style.left = 0;
pointerEl.style.top = 0;
pointerEl.style.visibility = 'hidden';
}
});
}
function calculateForecastMetrics(forecastArr, startBalance) {
let balTrack = startBalance;
let lowestBal = startBalance;
let highestBal = startBalance;
let lowestRow = 1;
let highestRow = 1;
let firstNegRow = 0;
for (let r = 2; r < forecastArr.length; r++) {
const debit = parseFloat(forecastArr[r][2]) || 0;
const credit = parseFloat(forecastArr[r][3]) || 0;
balTrack += credit - debit;
forecastArr[r][6] = balTrack.toFixed(2);
if (balTrack < lowestBal) {
lowestRow = r;
lowestBal = balTrack;
}
if (balTrack > highestBal) {
highestRow = r;
highestBal = balTrack;
}
if (balTrack < 0 && firstNegRow === 0) {
firstNegRow = r;
}
}
return { lowestRow, highestRow, firstNegRow };
}
function renderForecastTable(forecastArr, metrics) {
const StatusF = document.getElementById("forecastTrack");
const OutTable = document.createElement('table');
const OTBody = document.createElement('tbody');
OutTable.appendChild(OTBody);
let lastMonth = "start things off";
for (let r = 0; r < forecastArr.length; r++) {
const NewRow = document.createElement('tr');
NewRow.style.backgroundColor = forecastArr[r][5];
for (let c = 0; c < 5; c++) {
const NewCell = document.createElement('td');
let myText = forecastArr[r][c];
if (r > 0) {
if (myText !== "" && (c === 2 || c === 3)) {
myText = Number(parseFloat(myText)).toFixed(2);
}
if (c === 4) {
myText = (r === 1) ? forecastArr[r][4] : forecastArr[r][6];
}
}
if (r > 0 && parseFloat(forecastArr[r][6] || forecastArr[r][4]) < 0) {
NewCell.style.fontStyle = "italic";
}
NewCell.appendChild(document.createTextNode(myText));
NewCell.className = (c < 2) ? "left" : "right";
NewRow.appendChild(NewCell);
}
const curMonth = forecastArr[r][0].split("-")[1];
if (r > 1 && curMonth !== lastMonth) {
NewRow.className = "outbordersep";
lastMonth = curMonth;
} else {
NewRow.className = "outborder";
}
OTBody.appendChild(NewRow);
}
StatusF.appendChild(OutTable);
function applyHighlight(row, className, pointerId) {
row.className += ` ${className}`;
for (let c = 0; c < 5; c++) {
row.cells[c].style.fontWeight = "bold";
}
const pointer = document.getElementById(pointerId);
const posX = OutTable.offsetLeft + row.offsetLeft + row.offsetWidth + 7;
const posY = OutTable.offsetTop + row.offsetTop + Math.round(row.offsetHeight / 2) - 7;
pointer.style.visibility = 'visible';
pointer.style.left = `${posX}px`;
pointer.style.top = `${posY}px`;
}
const highlightedRows = new Set();
const { lowestRow, firstNegRow, highestRow } = metrics;
if (lowestRow > 1) {
applyHighlight(OTBody.rows[lowestRow], "outborderlow", "lowpointer");
highlightedRows.add(lowestRow);
}
if (firstNegRow > 1 && !highlightedRows.has(firstNegRow)) {
applyHighlight(OTBody.rows[firstNegRow], "outborderwarn", "warnpointer");
highlightedRows.add(firstNegRow);
}
if (highestRow > 1 && !highlightedRows.has(highestRow)) {
applyHighlight(OTBody.rows[highestRow], "outborderhigh", "highpointer");
}
const ClearButton = document.createElement('button');
ClearButton.className = "warnbutton";
ClearButton.innerHTML = "Clear Forecast";
ClearButton.onclick = ClearForecastTable;
ClearButton.style.width = '160px';
ClearButton.style.height = '44px';
StatusF.appendChild(ClearButton);
}
function DrawForecastTable() {
ClearForecastTable();
const startBalance = parseFloat(document.getElementById("StartBal").value) || 0;
ForecastArr.unshift(["", "Start Balance", "", "", startBalance.toFixed(2), HEADER_COLOR]);
ForecastArr.unshift(["Date", "Description", "Out", "In", "Bal", HEADER_COLOR]);
const metrics = calculateForecastMetrics(ForecastArr, startBalance);
renderForecastTable(ForecastArr, metrics);
}
function Forecast() {
if (BuildForecastArr()) {
DrawForecastTable();
}
}
window.onload = function() {
const filesInput = document.getElementById("files");
const LoadInfo = document.getElementById("LoadInfo");
filesInput.addEventListener("change", event => {
const file = event.target.files[0];
const reader = new FileReader();
reader.addEventListener("load", event => {
const fileIn = event.target.result;
if (fileIn) {
LoadTable(fileIn);
const nameFile = `Data Source: ${filesInput.value.replace(/.*\\/, "")}`;
const loadTime = new Date().toString('yyyy-MM-dd HH:mm:ss');
LoadInfo.innerHTML = `${nameFile} @ ${loadTime}`;
}
});
reader.readAsText(file);
});
if (localStorage["ForeCastArr"]) {
LoadBuildData();
}
}
Start Date:
End Date:
Balance:
☰
Start
End
Name
Freq
Debit
Credit
Do
← Minimum ←
← Maximum ←
← Warning ←
Help / Formatting Guide
Date Columns (Start / End)
Dates in the main table must be entered in YYYY-MM-DD format.
The 'End' date column can also accept the value 'none'.
Note: The main "Start Date" and "End Date" fields at the top of the page may display in a different format (e.g., MM/DD/YYYY) based on your browser or system settings. This is normal.
Freq (Frequency) Column
Frequency is specified as a number followed by a time period character. Examples: