Budget/index.js

512 lines
17 KiB
JavaScript

var gui = require('gui');
var fs = require('fs');
var rh = 14;
var sp = 4;
var y = sp;
function getAccounts(data) {
const resultData = [];
for (let i = 0; i < data.length; i++) {
resultData.push(data[i].name);
}
return resultData
}
function toListViewEntries(entries, visibleCols) {
let cols = visibleCols;
return entries.map(e => {
const amount = parseFloat(e.amount);
const subject = ("" + e.subject).substr(0, cols - 20).padEnd(cols - 20, ' ');
const sum = ((amount >= 0 ? "+" : "") + amount.toFixed(2)).substr(0, 10).padStart(10, ' ');
const d = new Date(e.date);
const date = d.getDate().toString().padStart(2, '0') + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getFullYear();
let s = (date.substr(0, 10)) + " " + subject + " " + sum;
return s;
});
}
function getSummation(entries) {
return entries.reduce((sum, e) => sum + parseFloat(e.amount), 0);
}
function toDateByComponents(s) {
const parts = s.split("-");
if (parts.length !== 3) return null;
const day = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const year = parseInt(parts[2]);
if (isNaN(day) || isNaN(month) || isNaN(year)) return null;
return new Date(year, month, day).getTime();
}
function closeAccount(data, selectedAccount) {
let gadgets = [];
const accountNameId = 301;
const remainingId = 302;
const transferCheckboxId = 303;
const targetAccountId = 304;
const executeButtonId = 305;
gadgets.push({ kind: 'text', id: accountNameId, label: "Account:", left: sp+128, top: y, width: 200, height: rh, value: selectedAccount.name});
const openStanding = getSummation(selectedAccount.entries);
gadgets.push({ kind: 'text', id: remainingId, label: "Remaining " + (openStanding>0? "funds": "debt"), left: sp+128, top: y+rh+sp, width: 100, height: rh})
gadgets.push({ kind: 'checkbox', id: transferCheckboxId, label: "Transfer to:", left: sp, top: y+rh+sp+rh+sp, height: rh, value: 0});
let accounts = [];
for (let i = 0; i < data.length; ++i) {
if (data[i].name != selectedAccount.name) {
accounts.push(data[i].name);
}
}
gadgets.push({ kind: 'cycle', id: targetAccountId, left: sp+128, top: y+rh+sp+rh+sp, width: 120, height: rh, items: accounts, value: 0 });
gadgets.push({ kind: 'button', id: executeButtonId, left: sp+128, top: y+rh+sp+rh+sp+rh+sp, height: rh, width: 120, label: "Close Account"})
let win = gui.createWindow({
title: "Close Account '" + selectedAccount.name + "'",
width: sp+128+240+sp,
height: y+rh+sp+rh+sp+rh+sp+rh+sp,
left: 30,
top: 30,
gadgets: gadgets
});
gui.setDisabled(win, targetAccountId, true);
gui.set(win, remainingId, openStanding.toFixed(2));
while (true) {
var evt = gui.waitEvent(win);
if (!evt) continue;
if (evt.type === 'close') {
gui.closeWindow(win);
return null;
}
if (evt.type === 'gadgetup') {
if (evt.id === transferCheckboxId) {
gui.setDisabled(win, targetAccountId, !gui.get(win, transferCheckboxId));
}
if (evt.id === executeButtonId) {
if (gui.get(win, transferCheckboxId)) {
let otherAccount;
let target = accounts[gui.get(win, targetAccountId)];
for (let i = 0; data.length;++i) {
if (data[i].name === target) {
otherAccount = data[i];
break;
}
}
if (otherAccount) {
otherAccount.entries.push({
date: Date.now(),
subject: '('+ selectedAccount.name + ') Closing Statement',
amount: -openStanding,
targetAccount: null
})
}
gui.closeWindow(win);
return -1;
}
}
}
}
}
function addAccount(data) {
let gadgets = [];
gadgets.push({ kind: 'string', id: 201, label: "Name:", left: sp + 80, top: y, width: 300, height: rh});
gadgets.push({ kind: 'button', id: 202, label: "Open Account", left: sp+80, top: y + rh + sp, width: 100, height: rh});
let win = gui.createWindow({
title: "New Account",
width: sp+80+300+sp,
height: y + rh+ sp+ rh + sp,
left: 30,
top: 30,
gadgets: gadgets
});
while (true) {
var evt = gui.waitEvent(win);
if (!evt) continue;
if (evt.type === 'close') {
gui.closeWindow(win);
return null;
}
if (evt.type === 'gadgetup') {
if (evt.id === 202) {
const newAccountName = gui.get(win, 201);
if (newAccountName.trim() != '') {
data.push(
{
name: newAccountName,
entries: []
}
)
gui.closeWindow(win);
break;
}
}
}
}
}
function subWindow(data) {
let gadgets = [];
let removable = false;
const subjectInputId = 401;
const amountInputId = 402;
const dateInputId = 403;
const transferCheckboxId = 404;
const transferAccountInputId = 405;
const executeButtonId = 406;
const deleteButtonId = 407;
const innerData = {
date: Date.now(),
subject: '',
amount: 0,
targetAccount: null
}
if (data) {
innerData.date = data.date;
innerData.subject = data.subject;
innerData.amount = data.amount;
innerData.targetAccount = data.targetAccount;
removable = true;
}
let d = new Date(innerData.date);
let formattedDate = (""+d.getDate()).padStart(2, "0") + "-" + (""+(d.getMonth() + 1)).padStart(2, "0") + "-" + d.getFullYear();
gadgets.push({
kind: 'string', id: subjectInputId, label: 'Subject:',
left: sp + 80, top: y, width: 400 - sp - 80 - sp, height: rh,
value: innerData.subject
});
gadgets.push({
kind: 'string', id: amountInputId, label: 'Amount:',
left: sp + 80, top: sp + rh + sp, width: 100, height: rh,
value: innerData.amount.toFixed(2)
});
gadgets.push({
kind: 'string', id: dateInputId, label: 'Date:',
left: sp + 80, top: sp + rh + sp + rh + sp, width: 150, height: rh,
value: formattedDate
});
gadgets.push({ kind: 'checkbox', id: transferCheckboxId, label: "Account:", left: sp + 80, top: sp + rh + sp + rh + sp + rh + sp + 2, width: rh, height: rh, value: 0 });
const accounts = getAccounts(projectData);
gadgets.push({ kind: 'cycle', id: transferAccountInputId, left: sp + 160 + rh + sp, top: sp + rh + sp + rh + sp + rh + sp, width: 120, height: rh, items: accounts, value: 0 });
gadgets.push({ kind: 'button', id: executeButtonId, left: sp + 80, top: sp + rh + sp + rh + sp + rh + sp + rh + sp, width: 80, label: "Save", height: rh });
gadgets.push({ kind: 'button', id: deleteButtonId, left: sp + 80 + sp + 80, top: sp + rh + sp + rh + sp + rh + sp + rh + sp, width: 80, label: "Remove", height: rh });
let win = gui.createWindow({
title: 'Entry Details',
width: 400,
height: sp + rh + sp + rh + sp + rh + sp + rh + sp + rh + sp,
left: 30,
top: 30,
gadgets: gadgets
});
if (innerData.targetAccount) {
gui.set(win, transferCheckboxId, 1);
let i = -1;
for (i = 0; i < accounts.length; i++) {
if (accounts[i] === innerData.targetAccount) {
break;
}
}
gui.set(win, 7, i);
gui.setDisabled(win, transferAccountInputId, false);
} else {
gui.setDisabled(win, transferAccountInputId, true);
}
gui.setDisabled(win, deleteButtonId, !removable);
while (true) {
var evt = gui.waitEvent(win);
if (!evt) continue;
if (evt.type === 'close') {
gui.closeWindow(win);
return null;
}
if (evt.type === 'gadgetup') {
if (evt.id === transferCheckboxId) {
gui.setDisabled(win, transferAccountInputId, !gui.get(win, transferAccountInputId));
}
if (evt.id === executeButtonId) {
innerData.subject = gui.get(win, subjectInputId);
innerData.amount = parseFloat(gui.get(win, amountInputId));
innerData.date = toDateByComponents(gui.get(win, dateInputId));
if (gui.get(win, transferCheckboxId)) {
const targetAccountIndex = gui.get(win, transferAccountInputId);
if (targetAccountIndex !== null && targetAccountIndex !== undefined) {
innerData.targetAccount = data[targetAccountIndex].name;
}
}
gui.closeWindow(win);
return innerData;
}
if (evt.id === deleteButtonId) {
gui.closeWindow(win);
return -1;
}
}
}
}
function mainWindow(projectData) {
let internalProjectData = projectData;
let invalidate = false;
let gadgets = [];
const cycleId = 1;
const listViewId = 2;
const balanceId = 3;
const addButtonId = 4;
cols = 56;
gadgets.push({
kind: 'cycle', id: cycleId,
left: sp, top: y, width: 120, height: rh, border: false,
items: getAccounts(internalProjectData), value: 0
});
gadgets.push({ kind: 'button', id: addButtonId, left: 500 - 80 - sp, top: y, width: 80, label: "+Transfer", height: rh });
gadgets.push({
kind: 'listview', id: listViewId, left: sp, top: y + rh + sp, width: 500 - sp - sp, height: 186,
flex: true,
items: toListViewEntries(internalProjectData[0].entries, cols), value: 0
});
gadgets.push({
kind: 'text', id: balanceId, label: 'Balance:',
left: 100, top: y + rh + sp + 186, width: 400 - sp, height: rh,
value: getSummation(internalProjectData[0].entries).toFixed(2)
});
let win = gui.createWindow({
title: 'Budget',
width: 500,
height: 14 + 4 + 4 + 200 + 4,
left: 20,
top: 15,
gadgets: gadgets
});
/* Set up menu bar */
gui.setMenu(win, [
{
title: 'Project',
items: [
{ label: 'New', id: 101, key: 'N' },
{ label: 'Open...', id: 102, key: 'O' },
{ label: 'Save...', id: 103, key: 'S' },
{ label: '---' },
{ label: 'Quit', id: 199, key: 'Q' }
]
},
{
title: 'Accounts',
items: [
{ label: "Add...", id:104, key: 'A'},
{ label: "Close...", id:105, key: 'C'}
]
}
]);
let currentAccountIndex = 0;
let updateView = (accountIndex, data) => {
gui.set(win, 2, toListViewEntries(data[accountIndex].entries, cols));
gui.set(win, 3, getSummation(data[accountIndex].entries).toFixed(2));
}
let doNew = () => {
invalidate = true;
return [
{
name: "Current",
entries: []
}
];
}
let doOpen = () => {
var r = gui.fileRequest({ title: 'Open File', pattern: '#?.json' });
if (r) {
return JSON.parse(fs.readFileSync(r.file));
}
invalidate = false;
}
let doSave = () => {
var r = gui.fileRequest({ title: 'Save File', pattern: '#?.json', save: true });
if (r) {
fs.writeFileSync(r.file, JSON.stringify(internalProjectData));
}
}
gui.setMenuItem(win, 105, {disabled: internalProjectData.length<=1});
while (!invalidate) {
var evt = gui.waitEvent(win);
if (!evt) continue;
if (evt.type === 'close') {
invalidate = true;
internalProjectData = null;
}
if (evt.type === 'menu') {
if (evt.id === 101) {
internalProjectData = doNew();
break;
}
else if (evt.id === 102) {
internalProjectData = doOpen();
break;
}
else if (evt.id === 103) {
doSave();
}
else if (evt.id === 104) {
addAccount(internalProjectData);
invalidate = true;
}
else if (evt.id === 105) {
if (internalProjectData.length > 1) {
const rValue = closeAccount(internalProjectData, internalProjectData[currentAccountIndex]);
if (rValue === -1) {
internalProjectData.splice(currentAccountIndex, 1);
invalidate = true;
}
} else {
gui.alert("Cannot close last remaining Account");
}
}
else if (evt.id === 199) {
invalidate = true;
internalProjectData = null;
}
}
if (evt.type === 'gadgetup') {
if (evt.id === cycleId) {
currentAccountIndex = evt.code;
updateView(currentAccountIndex, internalProjectData);
} else if (evt.id === listViewId) {
const entry = subWindow(internalProjectData[currentAccountIndex].entries[evt.code]);
if (entry != null) {
if (entry === -1) {
internalProjectData[currentAccountIndex].entries.splice(evt.code, 1);
} else {
internalProjectData[currentAccountIndex].entries[evt.code] = entry;
}
if (entry.targetAccount) {
const targetAccount = internalProjectData.find(d => d.name === entry.targetAccount);
if (targetAccount) {
targetAccount.entries.push({
date: entry.date,
subject: "(" + internalProjectData[currentAccountIndex].name + ")" + entry.subject,
amount: -entry.amount
});
}
}
updateView(currentAccountIndex, internalProjectData);
}
} else if (evt.id === addButtonId) {
const entry = subWindow();
if (entry != null) {
internalProjectData[currentAccountIndex].entries.push(entry);
if (entry.targetAccount) {
const targetAccount = internalProjectData.find(d => d.name === entry.targetAccount);
if (targetAccount) {
targetAccount.entries.push({
date: entry.date,
subject: "(" + internalProjectData[currentAccountIndex].name + ")" + entry.subject,
amount: -entry.amount
});
}
}
updateView(currentAccountIndex, internalProjectData);
}
}
}
};
gui.closeWindow(win);
return internalProjectData;
}
let projectData = [
{
name: "Current",
entries: [
{
date: 1704067200000,
subject: '(Savings) Initial Deposit',
amount: -500,
targetAccount: "Savings"
},
{
date: 1704067200000,
subject: 'Salary',
amount: 2182.15
},
{
date: 1704153600000,
subject: 'Rent',
amount: -800
},
{
date: 1704240000000,
subject: 'Groceries',
amount: -24.19
}
],
},
{
name: "Savings",
entries: [
{
date: 1704067200000,
subject: 'Initial Deposit',
amount: 500
}
],
}
];
do {
projectData = mainWindow(projectData);
} while(projectData != null);
console.log("bye!");