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.getDay().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 subWindow(data) { let gadgets = []; let removable = false; 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.getDay()).padStart(2, "0") + "-" + (""+(d.getMonth() + 1)).padStart(2, "0") + "-" + d.getFullYear(); gadgets.push({ kind: 'string', id: 1, label: 'Subject:', left: sp + 80, top: y, width: 400 - sp - 80 - sp, height: rh, value: innerData.subject }); gadgets.push({ kind: 'string', id: 2, label: 'Amount:', left: sp + 80, top: sp + rh + sp, width: 100, height: rh, value: innerData.amount.toFixed(2) }); gadgets.push({ kind: 'string', id: 3, label: 'Date:', left: sp + 80, top: sp + rh + sp + rh + sp, width: 150, height: rh, value: formattedDate }); gadgets.push({ kind: 'checkbox', id: 6, 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: 7, 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: 4, left: sp + 80, top: sp + rh + sp + rh + sp + rh + sp + rh + sp, width: 80, label: "Save", height: rh }); gadgets.push({ kind: 'button', id: 5, 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, 6, 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, 7, false); } else { gui.setDisabled(win, 7, true); } gui.setDisabled(win, 5, !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 === 6) { gui.setDisabled(win, 7, !gui.get(win, 6)); } if (evt.id === 4) { innerData.subject = gui.get(win, 1); innerData.amount = parseFloat(gui.get(win, 2)); innerData.date = toDateByComponents(gui.get(win, 3)); if (gui.get(win, 6)) { const targetAccountIndex = gui.get(win, 7); if (targetAccountIndex !== null && targetAccountIndex !== undefined) { innerData.targetAccount = data[targetAccountIndex].name; } } gui.closeWindow(win); return innerData; } if (evt.id === 5) { 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' } ] }, ]); let currentAccountIndex = 0; let updateView = (g, w, accountIndex, data) => { g.set(w, 2, toListViewEntries(data[accountIndex].entries, cols)); g.set(w, 3, getSummation(data[accountIndex].entries).toFixed(2)); } let doNew = () => { invalidate = true; return [ { name: "Current", entries: [] } ]; } let doOpen = () => { var r = gui.fileRequest({ title: 'Open File', pattern: '#?.json' }); if (r) { return JSON.parse(fs.readFileSync(r.file)); } invalidate = false; } function doSave() { var r = gui.fileRequest({ title: 'Save File', pattern: '#?.json', save: true }); if (r) { fs.writeFileSync(r.file, JSON.stringify(internalProjectData)); } } while (!invalidate) { var evt = gui.waitEvent(win); if (!evt) continue; if (evt.type === 'close') { invalidate = true; internalProjectData = null; } if (evt.type === 'menu') { if (evt.id === 101) { internalProjectData = doNew(); break; } else if (evt.id === 102) { internalProjectData = doOpen(); break; } else if (evt.id === 103) doSave(); else if (evt.id === 199) { invalidate = true; internalProjectData = null; } } if (evt.type === 'gadgetup') { if (evt.id === cycleId) { currentAccountIndex = evt.code; updateView(gui, win, currentAccountIndex, internalProjectData); } else if (evt.id === listViewId) { const entry = subWindow(internalProjectData[currentAccountIndex].entries[evt.code]); if (entry != null) { if (entry === -1) { internalProjectData[currentAccountIndex].entries.splice(evt.code, 1); } else { internalProjectData[currentAccountIndex].entries[evt.code] = entry; } if (entry.targetAccount) { const targetAccount = internalProjectData.find(d => d.name === entry.targetAccount); if (targetAccount) { targetAccount.entries.push({ date: entry.date, subject: "(" + internalProjectData[currentAccountIndex].name + ")" + entry.subject, amount: -entry.amount }); } } updateView(gui, win, 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(gui, win, currentAccountIndex, internalProjectData); } } } }; gui.closeWindow(win); return internalProjectData; } let projectData = [ { name: "Current", entries: [ { date: 1704067200000, subject: '(Savings) Initial Deposit', amount: -500, targetAccount: "Savings" }, { date: 1704067200000, subject: 'Salary', amount: 2182.15 }, { date: 1704153600000, subject: 'Rent', amount: -800 }, { date: 1704240000000, subject: 'Groceries', amount: -24.19 } ], }, { name: "Savings", entries: [ { date: 1704067200000, subject: 'Initial Deposit', amount: 500 } ], } ]; do { projectData = mainWindow(projectData); } while(projectData != null); console.log("bye!");