From 6c79788957f315333d3d23814f75db69af1692b3 Mon Sep 17 00:00:00 2001 From: oxypomme Date: Thu, 2 Jan 2025 14:33:24 +0100 Subject: [PATCH] feat: added remaining after budget --- calc-schedule-budget.js | 24 ++++-------------------- get-remaining.js | 29 +++++++++++++++++++++++++++++ lib/actual.js | 5 ++--- lib/format.js | 18 ++++++++++++++++++ package-lock.json | 36 ++++++++++++++++++++---------------- package.json | 8 ++++---- 6 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 get-remaining.js create mode 100644 lib/format.js diff --git a/calc-schedule-budget.js b/calc-schedule-budget.js index 4fa12d3..c29de6d 100644 --- a/calc-schedule-budget.js +++ b/calc-schedule-budget.js @@ -1,9 +1,10 @@ import { format } from 'date-fns'; +import chalk from 'chalk'; import { checkEnv } from './lib/env.js'; +import { formatAmount, formatSchedule } from './lib/format.js'; import * as actual from './lib/actual.js'; import { addFromFrequency } from './lib/dates.js'; -import chalk from 'chalk'; checkEnv([ 'BUDGET_INCOME_NAME', @@ -21,24 +22,7 @@ if (process.env.DRY_RUN) { const [month = format(new Date(), 'yyyy-MM')] = process.argv.slice(2); -const amountFormatter = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }); -function formatAmount(amount) { - return chalk.bold(amountFormatter.format(Math.abs(amount / 100))); -} - -function formatSchedule(s, c) { - const operator = s._amountOp === 'isapprox' ? '~ ' : ''; - let meta = formatAmount(s._amount); - if (s._date.frequency) { - meta += ` - ${s._date.frequency}`; - } - if (c?.name) { - meta += ` - ${c.name}`; - } - return `${chalk.underline(s.name)} (${operator}${meta})`; -} - -const schedules = await actual.getSchedules(); +const schedules = await actual.getSchedulesSQL(); const categoriesTotal = new Map(); let totalOutcome = 0; let totalIncome = 0; @@ -109,4 +93,4 @@ for (const [id, total] of categoriesTotal) { console.log(); -await actual.shutdown(); \ No newline at end of file +await actual.shutdown(); diff --git a/get-remaining.js b/get-remaining.js new file mode 100644 index 0000000..851143b --- /dev/null +++ b/get-remaining.js @@ -0,0 +1,29 @@ +import { format } from 'date-fns'; +import chalk from 'chalk'; + +import * as actual from './lib/actual.js'; +import { formatAmount, formatSchedule } from './lib/format.js'; +import { checkEnv } from './lib/env.js'; + +checkEnv([ + 'ACCOUNT_ID', +]); + +const month = format(new Date(), 'yyyy-MM'); + +const balance = await actual.getAccountBalance(process.env.ACCOUNT_ID); +const budget = await actual.getBudgetMonth(month); + +const remaining = balance - budget.totalBalance; + +console.log(); +console.log(`Balance: ${formatAmount(balance)}`); +// TODO: what if no income yet +console.log(`Budget balance for ${month}: ${chalk.red(formatAmount(budget.totalBalance))}`); +console.log(`Remaining: ${chalk.blue(formatAmount(remaining))}`); +console.log(); + +console.log(`Based on income for ${month}: you've got ${chalk.bgBlue((remaining / budget.totalBudgeted).toFixed(2))} times your budget left, even after spending everything`); +console.log(); + +await actual.shutdown(); diff --git a/lib/actual.js b/lib/actual.js index 9eb01a2..9240fec 100644 --- a/lib/actual.js +++ b/lib/actual.js @@ -19,9 +19,8 @@ await api.downloadBudget(process.env.ACTUAL_ID, { password: process.env.ACTUAL_ENCRYPTION, }); - -export async function getSchedules(select = '*') { +export async function getSchedulesSQL(select = '*') { return (await api.runQuery(api.q('schedules').select(select))).data; } -export * from '@actual-app/api'; \ No newline at end of file +export * from '@actual-app/api'; diff --git a/lib/format.js b/lib/format.js new file mode 100644 index 0000000..a29558b --- /dev/null +++ b/lib/format.js @@ -0,0 +1,18 @@ +import chalk from 'chalk'; + +const amountFormatter = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }); +export function formatAmount(amount) { + return chalk.bold(amountFormatter.format(Math.abs(amount / 100))); +} + +export function formatSchedule(s, c) { + const operator = s._amountOp === 'isapprox' ? '~ ' : ''; + let meta = formatAmount(s._amount); + if (s._date.frequency) { + meta += ` - ${s._date.frequency}`; + } + if (c?.name) { + meta += ` - ${c.name}`; + } + return `${chalk.underline(s.name)} (${operator}${meta})`; +} diff --git a/package-lock.json b/package-lock.json index ddbb1b0..3eecf63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,17 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@actual-app/api": "^6.8.2", - "chalk": "^5.3.0", - "date-fns": "^3.6.0", - "dotenv": "^16.4.5" + "@actual-app/api": "^24.12.0", + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "dotenv": "^16.4.7" } }, "node_modules/@actual-app/api": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/@actual-app/api/-/api-6.8.2.tgz", - "integrity": "sha512-BI9Zqip6eBRwmgQjDgXHpPSIJYsbm6e+Yyyvj9sNRn9GbWZr7WXsA8SySlIOZH/8EsA6DpS7CIW0RfOMWuLEcg==", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@actual-app/api/-/api-24.12.0.tgz", + "integrity": "sha512-LT7mnp79xpBZAfrRMdKpiZKQEQ+zDY3nopwFaZQTUoY05BwpoypujC5TQe6q3QKBm3aFrq0bZXBa/hSnVAOvgw==", + "license": "MIT", "dependencies": { "@actual-app/crdt": "^2.1.0", "better-sqlite3": "^9.6.0", @@ -111,9 +112,10 @@ } }, "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -140,9 +142,10 @@ } }, "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -179,9 +182,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index a4b919f..0ea80f8 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "author": "oxypomme", "license": "MIT", "dependencies": { - "@actual-app/api": "^6.8.2", - "chalk": "^5.3.0", - "date-fns": "^3.6.0", - "dotenv": "^16.4.5" + "@actual-app/api": "^24.12.0", + "chalk": "^5.4.1", + "date-fns": "^4.1.0", + "dotenv": "^16.4.7" } }