519 lines
17 KiB
JavaScript
519 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
|
|
});
|
|
|
|
let indexOfNewAccount = -1;
|
|
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: []
|
|
}
|
|
)
|
|
indexOfNewAccount = data.findIndex( p => p.name == newAccountName);
|
|
console.log(indexOfNewAccount);
|
|
gui.closeWindow(win);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return indexOfNewAccount;
|
|
}
|
|
|
|
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) => {
|
|
if (accountIndex!=null) {
|
|
gui.set(win, cycleId, getAccounts(data));
|
|
gui.set(win, cycleId, accountIndex);
|
|
}else {
|
|
accountIndex = gui.get(win, cycleId);
|
|
}
|
|
gui.set(win, 2, toListViewEntries(data[accountIndex].entries, cols));
|
|
gui.set(win, 3, getSummation(data[accountIndex].entries).toFixed(2));
|
|
}
|
|
|
|
let doNew = () => {
|
|
return [
|
|
{
|
|
name: "Current",
|
|
entries: []
|
|
}
|
|
];
|
|
}
|
|
|
|
let doOpen = () => {
|
|
var r = gui.fileRequest({ title: 'Open File', pattern: '#?.json' });
|
|
if (r) {
|
|
return JSON.parse(fs.readFileSync(r.file));
|
|
}
|
|
}
|
|
|
|
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();
|
|
updateView(0, internalProjectData);
|
|
}
|
|
else if (evt.id === 102) {
|
|
internalProjectData = doOpen();
|
|
updateView(0, internalProjectData);
|
|
}
|
|
else if (evt.id === 103) {
|
|
doSave();
|
|
}
|
|
else if (evt.id === 104) {
|
|
let indexOfNewAccount = addAccount(internalProjectData);
|
|
if (indexOfNewAccount > -1) {
|
|
currentAccountIndex = indexOfNewAccount;
|
|
}
|
|
updateView(currentAccountIndex, internalProjectData);
|
|
}
|
|
else if (evt.id === 105) {
|
|
if (internalProjectData.length > 1) {
|
|
const rValue = closeAccount(internalProjectData, internalProjectData[currentAccountIndex]);
|
|
if (rValue === -1) {
|
|
internalProjectData.splice(currentAccountIndex, 1);
|
|
updateView(0, internalProjectData);
|
|
}
|
|
} 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(null, 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);
|
|
}
|
|
|
|
|
|
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
|
|
}
|
|
],
|
|
}
|
|
|
|
];
|
|
|
|
|
|
mainWindow(projectData); |