main
macniel 2026-05-10 21:00:45 +02:00
parent 4051e01463
commit 4f7ce9a1ba
11 changed files with 842 additions and 378 deletions

7
consts.js 100644
View File

@ -0,0 +1,7 @@
var rh = 14;
var sp = 4;
var y = sp;
exports.rh = rh;
exports.sp = sp;
exports.y = y;

88
data-utilities.js 100644
View File

@ -0,0 +1,88 @@
/**
* Utility functions that concern about retrieving and storing data in a project file.
* this file also includes a default project in case a user want to start anew.
*/
const VERSION = "0.1";
let projectData = {
version: VERSION,
expenses: [],
accounts: [
{
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
}
],
}
]
};
class Project {
#projectData;
addAccount(accountName) {
}
closeAccount(accountName, transferToOtherAccount) {
}
getSummation(accountName) {
const acc = this.#projectData.accounts.find(p => p.name === accountName);
return acc.entries.reduce((sum, e) => sum + parseFloat(e.amount), 0);
}
getAccounts() {
}
getAccountEntries(accountName) {
}
save(to) {
}
constructor(projectData) {
}
static open(from) {
return new Project()
}
}

402
index.js
View File

@ -1,279 +1,17 @@
var gui = require('gui'); var gui = require('gui');
var fs = require('fs'); var fs = require('fs');
const { waitForAddAccountWindow } = require('./waitForAddAccountWindow');
const { waitForDataEntryWindow } = require('./waitForDataEntryWindow');
const { waitForCloseAccountWindow } = require('./waitForCloseAccountWindow');
const { getAccounts, toListViewEntries, getSummation } = require('./utils');
const { waitForChartWindow } = require('./waitForChartWindow');
function mainWindow(projectData) {
var rh = 14; var rh = 14;
var sp = 4; var sp = 4;
var y = sp; 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 internalProjectData = projectData;
let invalidate = false; let invalidate = false;
@ -293,29 +31,42 @@ function mainWindow(projectData) {
items: getAccounts(internalProjectData), value: 0 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({ gadgets.push({
kind: 'listview', id: listViewId, left: sp, top: y + rh + sp, width: 500 - sp - sp, height: 186, kind: 'button', id: addButtonId,
flex: true, left: -(sp + 80),
items: toListViewEntries(internalProjectData[0].entries, cols), value: 0 top: y,
width: 80,
label: "+Transfer",
height: rh
}); });
gadgets.push({ gadgets.push({
kind: 'text', id: balanceId, label: 'Balance:', kind: 'listview', id: listViewId,
left: 100, top: y + rh + sp + 186, width: 400 - sp, height: rh, left: sp,
top: y + rh + sp,
width: 500 - sp - sp,
height: 186,
items: toListViewEntries(internalProjectData[0].entries, cols),
value: 0
});
gadgets.push({
kind: 'text', id: balanceId,
label: 'Balance:',
left: 100,
top: -(sp + rh),
width: 400 - sp,
height: rh,
value: getSummation(internalProjectData[0].entries).toFixed(2) value: getSummation(internalProjectData[0].entries).toFixed(2)
}); });
let win = gui.createWindow({ let win = gui.createWindow({
title: 'Budget', title: 'Budget',
width: 500, width: 500,
height: 14 + 4 + 4 + 200 + 4, height: 14 + 4 + 4 + 200 + 4,
left: 20, left: 20,
top: 15, top: 15,
gadgets: gadgets gadgets: gadgets,
}); });
/* Set up menu bar */ /* Set up menu bar */
@ -333,8 +84,15 @@ function mainWindow(projectData) {
{ {
title: 'Accounts', title: 'Accounts',
items: [ items: [
{ label: "Add...", id:104, key: 'A'}, { label: "Add...", id: 104, key: 'A' },
{ label: "Close...", id:105, key: 'C'} { label: "Close...", id: 105, key: 'C' }
]
},
{
title: 'Charts',
items: [
{ label: "---" },
{ label: "New", id: 106 }
] ]
} }
]); ]);
@ -342,14 +100,16 @@ function mainWindow(projectData) {
let currentAccountIndex = 0; let currentAccountIndex = 0;
let updateView = (accountIndex, data) => { let updateView = (accountIndex, data) => {
if (accountIndex!=null) { if (accountIndex != null) {
gui.set(win, cycleId, getAccounts(data)); gui.set(win, cycleId, getAccounts(data));
gui.set(win, cycleId, accountIndex); gui.set(win, cycleId, accountIndex);
}else { } else {
accountIndex = gui.get(win, cycleId); accountIndex = gui.get(win, cycleId);
} }
// sort
data[accountIndex].entries = data[accountIndex].entries.sort((a, b) => a.date - b.date);
gui.set(win, 2, toListViewEntries(data[accountIndex].entries, cols)); gui.set(win, 2, toListViewEntries(data[accountIndex].entries, cols));
gui.set(win, 3, getSummation(data[accountIndex].entries).toFixed(2)); gui.set(win, 3, getSummation(data[accountIndex].entries).toFixed(2));
} }
let doNew = () => { let doNew = () => {
@ -375,7 +135,7 @@ function mainWindow(projectData) {
} }
} }
gui.setMenuItem(win, 105, {disabled: internalProjectData.length<=1}); gui.setMenuItem(win, 105, { disabled: internalProjectData.length <= 1 });
while (!invalidate) { while (!invalidate) {
@ -400,7 +160,7 @@ function mainWindow(projectData) {
doSave(); doSave();
} }
else if (evt.id === 104) { else if (evt.id === 104) {
let indexOfNewAccount = addAccount(internalProjectData); let indexOfNewAccount = waitForAddAccountWindow(internalProjectData);
if (indexOfNewAccount > -1) { if (indexOfNewAccount > -1) {
currentAccountIndex = indexOfNewAccount; currentAccountIndex = indexOfNewAccount;
} }
@ -408,7 +168,7 @@ function mainWindow(projectData) {
} }
else if (evt.id === 105) { else if (evt.id === 105) {
if (internalProjectData.length > 1) { if (internalProjectData.length > 1) {
const rValue = closeAccount(internalProjectData, internalProjectData[currentAccountIndex]); const rValue = waitForCloseAccountWindow(internalProjectData, internalProjectData[currentAccountIndex]);
if (rValue === -1) { if (rValue === -1) {
internalProjectData.splice(currentAccountIndex, 1); internalProjectData.splice(currentAccountIndex, 1);
updateView(0, internalProjectData); updateView(0, internalProjectData);
@ -416,9 +176,11 @@ function mainWindow(projectData) {
} else { } else {
gui.alert("Cannot close last remaining Account"); gui.alert("Cannot close last remaining Account");
} }
} }
else if (evt.id === 199) { else if (evt.id === 106) {
waitForChartWindow(projectData[currentAccountIndex]);
}
else if (evt.id === 199) {
invalidate = true; invalidate = true;
internalProjectData = null; internalProjectData = null;
} }
@ -430,12 +192,15 @@ function mainWindow(projectData) {
currentAccountIndex = evt.code; currentAccountIndex = evt.code;
updateView(null, internalProjectData); updateView(null, internalProjectData);
} else if (evt.id === listViewId) { } else if (evt.id === listViewId) {
const entry = subWindow(internalProjectData[currentAccountIndex].entries[evt.code]); let selectedEntry = evt.code;
const entry = waitForDataEntryWindow(internalProjectData[currentAccountIndex].entries[evt.code], internalProjectData);
if (entry != null) { if (entry != null) {
console.log(entry);
if (entry === -1) { if (entry === -1) {
internalProjectData[currentAccountIndex].entries.splice(evt.code, 1); console.log(selectedEntry);
internalProjectData[currentAccountIndex].entries.splice(selectedEntry, 1);
} else { } else {
internalProjectData[currentAccountIndex].entries[evt.code] = entry; internalProjectData[currentAccountIndex].entries[selectedEntry] = entry;
} }
if (entry.targetAccount) { if (entry.targetAccount) {
const targetAccount = internalProjectData.find(d => d.name === entry.targetAccount); const targetAccount = internalProjectData.find(d => d.name === entry.targetAccount);
@ -447,10 +212,11 @@ function mainWindow(projectData) {
}); });
} }
} }
updateView(currentAccountIndex, internalProjectData); updateView(currentAccountIndex, internalProjectData);
} }
} else if (evt.id === addButtonId) { } else if (evt.id === addButtonId) {
const entry = subWindow(); const entry = waitForDataEntryWindow(null, internalProjectData);
if (entry != null) { if (entry != null) {
internalProjectData[currentAccountIndex].entries.push(entry); internalProjectData[currentAccountIndex].entries.push(entry);
@ -475,45 +241,19 @@ function mainWindow(projectData) {
} }
let projectData = [ let projectData = [
{ { "name": "Current", "entries": [
name: "Current", { "date": 1704067200000, "subject": "Salary", "amount": 2182.15000000000009 },
entries: [ { "date": 1704153600000, "subject": "(Savings) Initial Deposit", "targetAccount": "Current", "amount": -500 },
{ { "date": 1704240000000, "subject": "Rent", "amount": -800 },
date: 1704067200000, { "date": 1704326400000, "subject": "Groceries", "amount": -24.19 }]
subject: '(Savings) Initial Deposit', },
amount: -500, { "name": "Savings", "entries": [
targetAccount: "Savings" { "date": 1704153600000, "subject": "Initial Deposit", "amount": 500 }]
}, }
{ ];
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
}
],
}
]; exports.projectData = projectData;
mainWindow(projectData); mainWindow(projectData);

View File

@ -1,47 +1 @@
[ [{"name":"Current","entries":[{"date":1704067200000,"subject":"Salary","amount":2182.15000000000009},{"date":1704153600000,"subject":"(Savings) Initial Deposit","targetAccount":"Current","amount":-500},{"date":1704240000000,"subject":"Rent","amount":-800},{"date":1704326400000,"subject":"Groceries","amount":-24.19}]},{"name":"Savings","entries":[{"date":1704067200000,"subject":"Initial Deposit","amount":500}]}]
{
"name": "Current",
"entries": [
{
"date": 1704067200000,
"subject": "(Savings) Initial Deposit",
"targetAccount": "Savings",
"amount": -500
},
{
"date": 1704067200000,
"subject": "Salary",
"amount": 2182.15000000000009
},
{
"date": 1704153600000,
"subject": "Rent",
"amount": -800
},
{
"date": 1704240000000,
"subject": "Groceries",
"amount": -24.19
},
{
"date": 1704240000000,
"subject": "Toiletries",
"amount": -12.95
}
]
},
{
"name": "Savings",
"entries": [
{
"date": 1704067200000,
"subject": "Initial Deposit",
"amount": 500
}
]
},
{
"name": "Portfolio",
"entries": []
}
]

View File

@ -0,0 +1 @@
----rwed 2026-05-10 20:58:03.64

92
test.js 100644
View File

@ -0,0 +1,92 @@
var gui = require('gui');
var gfx = gui.gfx;
var sp = 8;
// data
const datapoints = [
{ x: 0, v: 0 },
{ x: 1, v: 100 },
{ x: 2, v: -39 },
{ x: 3, v: -24 },
{ x: 4, v: 24 },
{ x: 5, v: -12 },
{ x: 6, v: 100 }
];
// create window (initial size; drawing will use current window client area)
let gadgets = [];
let win = gui.createWindow({
title: "Summary Chart",
width: 400,
height: 200,
left: 30,
top: 30,
gadgets: gadgets
});
// Compute cumulative series and min/max (include 0 to ensure Y=0 is visible)
let cumulative = [];
let cur = 0;
cumulative.push({ x: datapoints[0].x, y: 0 }); // start at 0
let lowest = 0;
let highest = 0;
for (let i = 0; i < datapoints.length; ++i) {
cur += datapoints[i].v;
cumulative.push({ x: datapoints[i].x, y: cur });
if (cur < lowest) lowest = cur;
if (cur > highest) highest = cur;
}
if (0 < lowest) lowest = 0;
if (0 > highest) highest = 0;
// Draw function using current window size
function drawChart() {
const totalW = Math.max(10, 400 - sp * 3);
const totalH = Math.max(10, 200 - sp * 3);
const left = sp;
const top = sp;
const right = left + totalW;
const bottom = top + totalH;
// X and Y ranges
const minX = cumulative[0].x;
const maxX = cumulative[cumulative.length - 1].x;
const xRange = (maxX === minX) ? 1 : (maxX - minX);
let yRange = highest - lowest;
if (yRange === 0) yRange = 1;
// mapping functions
const mapX = x => left + ((x - minX) / xRange) * totalW;
const mapY = y => bottom - ((y - lowest) / yRange) * totalH; // invert Y
// draw Y=0 axis
const y0 = mapY(0);
gfx.setColor(win, 1);
gfx.drawLine(win, left, y0, right, y0);
gfx.setColor(win, 4);
// draw polyline
let last = cumulative[0];
for (let i = 1; i < cumulative.length; ++i) {
let p = cumulative[i];
gfx.drawLine(win, mapX(last.x), mapY(last.y), mapX(p.x), mapY(p.y));
last = p;
}
}
// initial draw
drawChart();
while (true) {
var evt = gui.waitEvent(win);
if (!evt) continue;
if (evt.type === 'close') {
gui.closeWindow(win);
break;
}
}
console.log("close");

73
utils.js 100644
View File

@ -0,0 +1,73 @@
/**
* Reduces the Data Structure to the account names.
* @param {any[]} data
* @returns
*/
function getAccounts(data) {
const resultData = [];
for (let i = 0; i < data.length; i++) {
resultData.push(data[i].name);
}
return resultData;
}
/**
* Sums up every entry and returns it.
* @param {any[]} entries
* @returns
*/
function getSummation(entries) {
return entries.reduce((sum, e) => sum + parseFloat(e.amount), 0);
}
/**
* Turns the given String in dd-MM-yyyy into a Javascript Date Object
* @param {string} s
* @returns
*/
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();
}
/**
* formats the given date into a dd-MM-yyyy String
* @param {Date} d
* @returns
*/
function toStringDate(d) {
console.log(d);
return ("" + d.getDate()).padStart(2, "0") +
"-" + ("" + (d.getMonth() + 1)).padStart(2, "0") +
"-" + d.getFullYear();
}
/**
* Maps each entry to a line in a ListView while adhering to the visible amount of char columns
* @param {any[]} entries
* @param {number} visibleCols
* @returns
*/
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;
});
}
exports.getAccounts = getAccounts;
exports.getSummation = getSummation;
exports.toDateByComponents = toDateByComponents;
exports.toListViewEntries = toListViewEntries;
exports.toStringDate = toStringDate;

View File

@ -0,0 +1,66 @@
var gui = require('gui');
function waitForAddAccountWindow(data) {
var rh = 14;
var sp = 4;
var y = sp;
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);
gui.closeWindow(win);
break;
}
}
}
}
return indexOfNewAccount;
}
exports.waitForAddAccountWindow = waitForAddAccountWindow;

View File

@ -0,0 +1,200 @@
var gui = require('gui');
const { getSummation, toStringDate, toDateByComponents, getAccounts } = require('./utils');
function waitForPrefsChartWindow() {
var rh = 14;
var sp = 4;
var y = sp;
let gadgets = [];
gadgets.push({
kind: "string",
left: sp,
width: 80,
top: sp,
height: rh,
label: "from",
value: "01-01-2026"
});
gadgets.push({
kind: "string",
left: sp,
width: 80,
top: sp,
height: rh,
label: "to",
value: toStringDate(new Date())
});
let win = gui.createWindow({
title: "Summary Chart Prefs",
width: sp + 128 + 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);
}
}
}
/**
* @typedef Account
* @property {String} name;
* @property {AccountEntry[]} entries;
*/
/**
* @typedef AccountEntry
* @property {Number} date
* @property {String} subject;
* @property {Number} amount;
* @property {String?} targetAccount;
*/
/**
* @typedef DataPoint
* @property {Number} timestamp;
* @property {Number} value;
* @property {String} accountName;
* @property {String} tooltip;
*/
/**
*
* @param {*} gui
* @param {*} win
* @param {Account[]} data
* @param {*} rangeFrom
* @param {*} rangeTo
*/
function renderChart(gui, win, data, /** @type {Date} */ rangeFrom, /** @type {Date} */ rangeTo) {
var rh = 14;
var sp = 4;
var y = sp;
const gfx = gui.gfx;
let datapoints = [];
for (let i = 0; i < data.entries.length;++i) {
datapoints.push({
x: data.entries[i].date,
v: data.entries[i].amount
});
}
// Compute cumulative series and min/max (include 0 to ensure Y=0 is visible)
let cumulative = [];
let cur = 0;
cumulative.push({ x: datapoints[0].x, y: 0 }); // start at 0
let lowest = 0;
let highest = 0;
for (let i = 0; i < datapoints.length; ++i) {
cur += datapoints[i].v;
cumulative.push({ x: datapoints[i].x, y: cur });
if (cur < lowest) lowest = cur;
if (cur > highest) highest = cur;
}
if (0 < lowest) lowest = 0;
if (0 > highest) highest = 0;
const totalW = Math.max(10, 400 - sp * 3);
const totalH = Math.max(10, 180 - sp * 3);
const left = sp;
const top = sp;
const right = left + totalW;
const bottom = top + totalH;
console.log(lowest, highest);
// X and Y ranges
const minX = cumulative[0].x;
const maxX = cumulative[cumulative.length - 1].x;
const xRange = (maxX === minX) ? 1 : (maxX - minX);
let yRange = highest - lowest;
if (yRange === 0) yRange = 1;
// mapping functions
const mapX = x => left + ((x - minX) / xRange) * totalW;
const mapY = y => bottom - ((y - lowest) / yRange) * totalH; // invert Y
// draw Y=0 axis
const y0 = mapY(0);
gfx.setColor(win, 1);
gfx.drawLine(win, left, y0, right, y0);
gfx.setColor(win, 4);
// draw polyline
let last = cumulative[0];
for (let i = 1; i < cumulative.length; ++i) {
let p = cumulative[i];
gfx.drawLine(win, mapX(last.x), mapY(last.y), mapX(p.x), mapY(p.y));
last = p;
}
}
function waitForChartWindow(data) {
var rh = 14;
var sp = 4;
var y = sp;
let gadgets = [];
let rangeTo = new Date(toDateByComponents("31-12-2024"));
let rangeFrom = new Date(toDateByComponents("01-01-" + rangeTo.getFullYear()));
let win = gui.createWindow({
title: "Summary Chart",
width: sp + 400 + sp,
height: y + 180 + sp,
left: 30,
top: 30,
gadgets: gadgets,
});
renderChart(gui, win, data, rangeFrom, rangeTo);
gui.setMenu(win, [
{
title: 'Chart',
items: [
{ label: 'Save', id: 401, key: 'S'},
{ label: 'Close', id: 499, key: 'Q' }
]
},
{
title: 'Prefs',
items: [
{ label: "Set Daterange", id: 404, key: 'D' },
]
}
]);
while (true) {
var evt = gui.waitEvent(win);
if (!evt) continue;
if (evt.type === 'menu') {
}
if (evt.type === 'close') {
gui.closeWindow(win);
return null;
}
}
}
exports.waitForChartWindow = waitForChartWindow;

View File

@ -0,0 +1,122 @@
var gui = require('gui');
const { getSummation } = require('./utils');
function waitForCloseAccountWindow(data, selectedAccount) {
var rh = 14;
var sp = 4;
var y = sp;
let gadgets = [];
const accountNameId = 301;
const remainingId = 302;
const transferCheckboxId = 303;
const targetAccountId = 304;
const executeButtonId = 305;
const openStanding = getSummation(selectedAccount.entries);
let accounts = [];
for (let i = 0; i < data.length; ++i) {
if (data[i].name != selectedAccount.name) {
accounts.push(data[i].name);
}
}
gadgets.push({
kind: 'text', id: accountNameId,
label: "Account:",
left: sp + 128,
top: y,
width: 200,
height: rh,
value: selectedAccount.name
});
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
});
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;
}
}
}
}
}
exports.waitForCloseAccountWindow = waitForCloseAccountWindow;

View File

@ -0,0 +1,121 @@
var gui = require('gui');
const { getAccounts, toDateByComponents } = require('./utils');
function waitForDataEntryWindow(data, projectData) {
var rh = 14;
var sp = 4;
var y = sp;
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 = projectData[targetAccountIndex].name;
}
}
gui.closeWindow(win);
return innerData;
}
if (evt.id === deleteButtonId) {
gui.closeWindow(win);
return -1;
}
}
}
}
exports.waitForDataEntryWindow = waitForDataEntryWindow;