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);