From 4f7ce9a1bae5d4da1238fd36267ce7df295db0cf Mon Sep 17 00:00:00 2001 From: macniel Date: Sun, 10 May 2026 21:00:45 +0200 Subject: [PATCH] commit --- consts.js | 7 + data-utilities.js | 88 ++++++++ index.js | 402 +++++++---------------------------- project.json | 48 +---- project.json.uaem | 1 + test.js | 92 ++++++++ utils.js | 73 +++++++ waitForAddAccountWindow.js | 66 ++++++ waitForChartWindow.js | 200 +++++++++++++++++ waitForCloseAccountWindow.js | 122 +++++++++++ waitForDataEntryWindow.js | 121 +++++++++++ 11 files changed, 842 insertions(+), 378 deletions(-) create mode 100644 consts.js create mode 100644 data-utilities.js create mode 100644 project.json.uaem create mode 100644 test.js create mode 100644 utils.js create mode 100644 waitForAddAccountWindow.js create mode 100644 waitForChartWindow.js create mode 100644 waitForCloseAccountWindow.js create mode 100644 waitForDataEntryWindow.js diff --git a/consts.js b/consts.js new file mode 100644 index 0000000..89d955a --- /dev/null +++ b/consts.js @@ -0,0 +1,7 @@ +var rh = 14; +var sp = 4; +var y = sp; + +exports.rh = rh; +exports.sp = sp; +exports.y = y; \ No newline at end of file diff --git a/data-utilities.js b/data-utilities.js new file mode 100644 index 0000000..2686743 --- /dev/null +++ b/data-utilities.js @@ -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() + } + +} \ No newline at end of file diff --git a/index.js b/index.js index 844cf8b..84b97d0 100644 --- a/index.js +++ b/index.js @@ -1,279 +1,17 @@ var gui = require('gui'); 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 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; @@ -293,29 +31,42 @@ function mainWindow(projectData) { 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 + kind: 'button', id: addButtonId, + left: -(sp + 80), + top: y, + width: 80, + label: "+Transfer", + height: rh }); gadgets.push({ - kind: 'text', id: balanceId, label: 'Balance:', - left: 100, top: y + rh + sp + 186, width: 400 - sp, height: rh, + kind: 'listview', id: listViewId, + 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) }); - - let win = gui.createWindow({ title: 'Budget', width: 500, height: 14 + 4 + 4 + 200 + 4, left: 20, top: 15, - gadgets: gadgets + gadgets: gadgets, }); /* Set up menu bar */ @@ -333,8 +84,15 @@ function mainWindow(projectData) { { title: 'Accounts', items: [ - { label: "Add...", id:104, key: 'A'}, - { label: "Close...", id:105, key: 'C'} + { label: "Add...", id: 104, key: 'A' }, + { 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 updateView = (accountIndex, data) => { - if (accountIndex!=null) { + if (accountIndex != null) { gui.set(win, cycleId, getAccounts(data)); gui.set(win, cycleId, accountIndex); - }else { + } else { 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, 3, getSummation(data[accountIndex].entries).toFixed(2)); + gui.set(win, 3, getSummation(data[accountIndex].entries).toFixed(2)); } 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) { @@ -400,7 +160,7 @@ function mainWindow(projectData) { doSave(); } else if (evt.id === 104) { - let indexOfNewAccount = addAccount(internalProjectData); + let indexOfNewAccount = waitForAddAccountWindow(internalProjectData); if (indexOfNewAccount > -1) { currentAccountIndex = indexOfNewAccount; } @@ -408,7 +168,7 @@ function mainWindow(projectData) { } else if (evt.id === 105) { if (internalProjectData.length > 1) { - const rValue = closeAccount(internalProjectData, internalProjectData[currentAccountIndex]); + const rValue = waitForCloseAccountWindow(internalProjectData, internalProjectData[currentAccountIndex]); if (rValue === -1) { internalProjectData.splice(currentAccountIndex, 1); updateView(0, internalProjectData); @@ -416,9 +176,11 @@ function mainWindow(projectData) { } else { 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; internalProjectData = null; } @@ -430,12 +192,15 @@ function mainWindow(projectData) { currentAccountIndex = evt.code; updateView(null, internalProjectData); } 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) { + console.log(entry); if (entry === -1) { - internalProjectData[currentAccountIndex].entries.splice(evt.code, 1); + console.log(selectedEntry); + internalProjectData[currentAccountIndex].entries.splice(selectedEntry, 1); } else { - internalProjectData[currentAccountIndex].entries[evt.code] = entry; + internalProjectData[currentAccountIndex].entries[selectedEntry] = entry; } if (entry.targetAccount) { const targetAccount = internalProjectData.find(d => d.name === entry.targetAccount); @@ -447,10 +212,11 @@ function mainWindow(projectData) { }); } } + updateView(currentAccountIndex, internalProjectData); } } else if (evt.id === addButtonId) { - const entry = subWindow(); + const entry = waitForDataEntryWindow(null, internalProjectData); if (entry != null) { internalProjectData[currentAccountIndex].entries.push(entry); @@ -475,45 +241,19 @@ function mainWindow(projectData) { } - 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 - } - ], - } +let projectData = [ + { "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": 1704153600000, "subject": "Initial Deposit", "amount": 500 }] + } +]; - ]; +exports.projectData = projectData; mainWindow(projectData); \ No newline at end of file diff --git a/project.json b/project.json index 6f5956f..fab099e 100644 --- a/project.json +++ b/project.json @@ -1,47 +1 @@ -[ - { - "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": [] - } -] \ No newline at end of file +[{"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}]}] \ No newline at end of file diff --git a/project.json.uaem b/project.json.uaem new file mode 100644 index 0000000..f36f6d9 --- /dev/null +++ b/project.json.uaem @@ -0,0 +1 @@ +----rwed 2026-05-10 20:58:03.64 diff --git a/test.js b/test.js new file mode 100644 index 0000000..89b07da --- /dev/null +++ b/test.js @@ -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"); diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..f5172c9 --- /dev/null +++ b/utils.js @@ -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; \ No newline at end of file diff --git a/waitForAddAccountWindow.js b/waitForAddAccountWindow.js new file mode 100644 index 0000000..a5a409a --- /dev/null +++ b/waitForAddAccountWindow.js @@ -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; diff --git a/waitForChartWindow.js b/waitForChartWindow.js new file mode 100644 index 0000000..fc277d5 --- /dev/null +++ b/waitForChartWindow.js @@ -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; diff --git a/waitForCloseAccountWindow.js b/waitForCloseAccountWindow.js new file mode 100644 index 0000000..cbe80fd --- /dev/null +++ b/waitForCloseAccountWindow.js @@ -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; diff --git a/waitForDataEntryWindow.js b/waitForDataEntryWindow.js new file mode 100644 index 0000000..1659778 --- /dev/null +++ b/waitForDataEntryWindow.js @@ -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;