From 0201b368d46c8e1bb7a56270919fc531f62cb9fb Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 14 Mar 2022 19:26:54 +0000 Subject: [PATCH 1/4] Add column and use instead of last transactions date --- postgres/accounts.sql.go | 30 +++++++++++++++++++------- postgres/models.go | 11 +++++----- postgres/queries/accounts.sql | 10 ++++++--- postgres/schema/0018_reconciled-on.sql | 5 +++++ server/reconcile.go | 6 ++++++ web/src/pages/BudgetSidebar.vue | 5 ++++- web/src/stores/budget-account.ts | 7 +++++- web/src/stores/budget.ts | 3 ++- 8 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 postgres/schema/0018_reconciled-on.sql diff --git a/postgres/accounts.sql.go b/postgres/accounts.sql.go index ab5de4e..666e620 100644 --- a/postgres/accounts.sql.go +++ b/postgres/accounts.sql.go @@ -5,7 +5,7 @@ package postgres import ( "context" - "time" + "database/sql" "git.javil.eu/jacob1123/budgeteer/postgres/numeric" "github.com/google/uuid" @@ -15,7 +15,7 @@ const createAccount = `-- name: CreateAccount :one INSERT INTO accounts (name, budget_id) VALUES ($1, $2) -RETURNING id, budget_id, name, on_budget, is_open +RETURNING id, budget_id, name, on_budget, is_open, last_reconciled ` type CreateAccountParams struct { @@ -32,12 +32,13 @@ func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (A &i.Name, &i.OnBudget, &i.IsOpen, + &i.LastReconciled, ) return i, err } const getAccount = `-- name: GetAccount :one -SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts +SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled FROM accounts WHERE accounts.id = $1 ` @@ -50,12 +51,13 @@ func (q *Queries) GetAccount(ctx context.Context, id uuid.UUID) (Account, error) &i.Name, &i.OnBudget, &i.IsOpen, + &i.LastReconciled, ) return i, err } const getAccounts = `-- name: GetAccounts :many -SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open FROM accounts +SELECT accounts.id, accounts.budget_id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled FROM accounts WHERE accounts.budget_id = $1 AND accounts.is_open = TRUE ORDER BY accounts.name @@ -76,6 +78,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun &i.Name, &i.OnBudget, &i.IsOpen, + &i.LastReconciled, ); err != nil { return nil, err } @@ -91,8 +94,7 @@ func (q *Queries) GetAccounts(ctx context.Context, budgetID uuid.UUID) ([]Accoun } const getAccountsWithBalance = `-- name: GetAccountsWithBalance :many -SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, - (SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled, +SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance @@ -107,7 +109,7 @@ type GetAccountsWithBalanceRow struct { Name string OnBudget bool IsOpen bool - LastReconciled time.Time + LastReconciled sql.NullTime WorkingBalance numeric.Numeric ClearedBalance numeric.Numeric ReconciledBalance numeric.Numeric @@ -193,13 +195,24 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams) return items, nil } +const setLastReconciled = `-- name: SetLastReconciled :exec +UPDATE accounts +SET last_reconciled = NOW() +WHERE accounts.id = $1 +` + +func (q *Queries) SetLastReconciled(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, setLastReconciled, id) + return err +} + const updateAccount = `-- name: UpdateAccount :one UPDATE accounts SET name = $1, on_budget = $2, is_open = $3 WHERE accounts.id = $4 -RETURNING id, budget_id, name, on_budget, is_open +RETURNING id, budget_id, name, on_budget, is_open, last_reconciled ` type UpdateAccountParams struct { @@ -223,6 +236,7 @@ func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (A &i.Name, &i.OnBudget, &i.IsOpen, + &i.LastReconciled, ) return i, err } diff --git a/postgres/models.go b/postgres/models.go index 7ff33e8..8e01747 100644 --- a/postgres/models.go +++ b/postgres/models.go @@ -32,11 +32,12 @@ func (e *TransactionStatus) Scan(src interface{}) error { } type Account struct { - ID uuid.UUID - BudgetID uuid.UUID - Name string - OnBudget bool - IsOpen bool + ID uuid.UUID + BudgetID uuid.UUID + Name string + OnBudget bool + IsOpen bool + LastReconciled sql.NullTime } type Assignment struct { diff --git a/postgres/queries/accounts.sql b/postgres/queries/accounts.sql index 0e62900..6390403 100644 --- a/postgres/queries/accounts.sql +++ b/postgres/queries/accounts.sql @@ -15,8 +15,7 @@ AND accounts.is_open = TRUE ORDER BY accounts.name; -- name: GetAccountsWithBalance :many -SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, - (SELECT MAX(transactions.date) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.status = 'Reconciled')::date as last_reconciled, +SELECT accounts.id, accounts.name, accounts.on_budget, accounts.is_open, accounts.last_reconciled, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW())::decimal(12,2) as working_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status IN ('Cleared', 'Reconciled'))::decimal(12,2) as cleared_balance, (SELECT SUM(transactions.amount) FROM transactions WHERE transactions.account_id = accounts.id AND transactions.date < NOW() AND transactions.status = 'Reconciled')::decimal(12,2) as reconciled_balance @@ -38,4 +37,9 @@ SET name = $1, on_budget = $2, is_open = $3 WHERE accounts.id = $4 -RETURNING *; \ No newline at end of file +RETURNING *; + +-- name: SetLastReconciled :exec +UPDATE accounts +SET last_reconciled = NOW() +WHERE accounts.id = $1; \ No newline at end of file diff --git a/postgres/schema/0018_reconciled-on.sql b/postgres/schema/0018_reconciled-on.sql new file mode 100644 index 0000000..a8adb68 --- /dev/null +++ b/postgres/schema/0018_reconciled-on.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE accounts ADD COLUMN last_reconciled date NULL; + +-- +goose Down +ALTER TABLE accounts DROP COLUMN last_reconciled; \ No newline at end of file diff --git a/server/reconcile.go b/server/reconcile.go index 7b9cd5a..2ac619b 100644 --- a/server/reconcile.go +++ b/server/reconcile.go @@ -65,6 +65,12 @@ func (h *Handler) reconcileTransactions(c *gin.Context) { return } + err = h.Service.SetLastReconciled(c.Request.Context(), accountUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("set last reconciled: %w", err)) + return + } + err = tx.Commit() if err != nil { c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("commit: %w", err)) diff --git a/web/src/pages/BudgetSidebar.vue b/web/src/pages/BudgetSidebar.vue index e3e2045..4b2697d 100644 --- a/web/src/pages/BudgetSidebar.vue +++ b/web/src/pages/BudgetSidebar.vue @@ -20,9 +20,12 @@ const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBala const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance); function isRecentlyReconciled(account: Account) { + if(!account.LastReconciled.Valid) + return false; + const now = new Date().getTime(); const recently = 7 * 24 * 60 * 60 * 1000; - return new Date(now - recently).getTime() < account.LastReconciled.getTime(); + return new Date(now - recently).getTime() < account.LastReconciled.Time.getTime(); } function getAccountName(account: Account) { diff --git a/web/src/stores/budget-account.ts b/web/src/stores/budget-account.ts index bf64694..e582248 100644 --- a/web/src/stores/budget-account.ts +++ b/web/src/stores/budget-account.ts @@ -21,7 +21,12 @@ export interface Account { WorkingBalance: number ReconciledBalance: number Transactions: string[] - LastReconciled: Date + LastReconciled: NullDate +} + +interface NullDate { + Valid: boolean + Time: Date } export interface Category { diff --git a/web/src/stores/budget.ts b/web/src/stores/budget.ts index 57200c7..49b8310 100644 --- a/web/src/stores/budget.ts +++ b/web/src/stores/budget.ts @@ -58,7 +58,8 @@ export const useBudgetsStore = defineStore('budget', { for (const account of response.Accounts || []) { const existingAccount = accounts.Accounts.get(account.ID); account.Transactions = existingAccount?.Transactions ?? []; - account.LastReconciled = new Date(account.LastReconciled); + if(account.LastReconciled.Valid) + account.LastReconciled.Time = new Date(account.LastReconciled.Time); accounts.Accounts.set(account.ID, account); } for (const category of response.Categories || []) { -- 2.47.2 From 324e13f5c53acadfc47f78b84f5356b34f7041ce Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 14 Mar 2022 19:34:17 +0000 Subject: [PATCH 2/4] Show age in days --- web/src/pages/BudgetSidebar.vue | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/web/src/pages/BudgetSidebar.vue b/web/src/pages/BudgetSidebar.vue index 4b2697d..cf793a1 100644 --- a/web/src/pages/BudgetSidebar.vue +++ b/web/src/pages/BudgetSidebar.vue @@ -19,18 +19,36 @@ const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts); const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance); const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance); +const days = 24 * 60 * 60 * 1000; +function daysSinceLastReconciled(account: Account) { + if(!account.LastReconciled.Valid) + return false; + + const now = new Date().getTime(); + const diff = new Date(now).getTime() - account.LastReconciled.Time.getTime(); + return Math.floor(diff / days); + //const recently = 7 * days; +} + function isRecentlyReconciled(account: Account) { if(!account.LastReconciled.Valid) return false; const now = new Date().getTime(); - const recently = 7 * 24 * 60 * 60 * 1000; + const recently = 7 * days; return new Date(now - recently).getTime() < account.LastReconciled.Time.getTime(); } function getAccountName(account: Account) { - const reconciledMarker = isRecentlyReconciled(account) ? "" : " *"; - return account.Name + reconciledMarker; + const days = daysSinceLastReconciled(account); + if(days === false) + return account.Name + " *"; + + if(days <= 7) + return account.Name; + + //const reconciledMarker = isRecentlyReconciled(account) ? "" : " *"; + return account.Name + " " + days; } -- 2.47.2 From 6f4e286b7a8a28c8b1357b45dae7f8bd147bd7ef Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 14 Mar 2022 19:46:21 +0000 Subject: [PATCH 3/4] Initialize last_reconciled from transactions --- postgres/schema/0018_reconciled-on.sql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/postgres/schema/0018_reconciled-on.sql b/postgres/schema/0018_reconciled-on.sql index a8adb68..42a16ed 100644 --- a/postgres/schema/0018_reconciled-on.sql +++ b/postgres/schema/0018_reconciled-on.sql @@ -1,5 +1,12 @@ -- +goose Up ALTER TABLE accounts ADD COLUMN last_reconciled date NULL; +UPDATE accounts +SET last_reconciled = ( + SELECT MAX(transactions.date) + FROM transactions + WHERE transactions.account_id = accounts.id + AND transactions.status = 'Reconciled' +); -- +goose Down ALTER TABLE accounts DROP COLUMN last_reconciled; \ No newline at end of file -- 2.47.2 From 450324d29eb1d4746e086e20ca45381ddff81246 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 14 Mar 2022 19:48:45 +0000 Subject: [PATCH 4/4] Extract AccountWithReconciled --- web/src/components/AccountWithReconciled.vue | 31 +++++++++++++++ web/src/pages/BudgetSidebar.vue | 41 ++------------------ 2 files changed, 34 insertions(+), 38 deletions(-) create mode 100644 web/src/components/AccountWithReconciled.vue diff --git a/web/src/components/AccountWithReconciled.vue b/web/src/components/AccountWithReconciled.vue new file mode 100644 index 0000000..044c754 --- /dev/null +++ b/web/src/components/AccountWithReconciled.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/web/src/pages/BudgetSidebar.vue b/web/src/pages/BudgetSidebar.vue index cf793a1..0407e76 100644 --- a/web/src/pages/BudgetSidebar.vue +++ b/web/src/pages/BudgetSidebar.vue @@ -4,6 +4,7 @@ import Currency from "../components/Currency.vue" import { useBudgetsStore } from "../stores/budget" import { Account, useAccountStore } from "../stores/budget-account" import { useSettingsStore } from "../stores/settings" +import AccountWithReconciled from "../components/AccountWithReconciled.vue"; const settings = useSettingsStore(); const ExpandMenu = computed(() => settings.Menu.Expand); @@ -18,38 +19,6 @@ const OnBudgetAccounts = computed(() => accountStore.OnBudgetAccounts); const OffBudgetAccounts = computed(() => accountStore.OffBudgetAccounts); const OnBudgetAccountsBalance = computed(() => accountStore.OnBudgetAccountsBalance); const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBalance); - -const days = 24 * 60 * 60 * 1000; -function daysSinceLastReconciled(account: Account) { - if(!account.LastReconciled.Valid) - return false; - - const now = new Date().getTime(); - const diff = new Date(now).getTime() - account.LastReconciled.Time.getTime(); - return Math.floor(diff / days); - //const recently = 7 * days; -} - -function isRecentlyReconciled(account: Account) { - if(!account.LastReconciled.Valid) - return false; - - const now = new Date().getTime(); - const recently = 7 * days; - return new Date(now - recently).getTime() < account.LastReconciled.Time.getTime(); -} - -function getAccountName(account: Account) { - const days = daysSinceLastReconciled(account); - if(days === false) - return account.Name + " *"; - - if(days <= 7) - return account.Name; - - //const reconciledMarker = isRecentlyReconciled(account) ? "" : " *"; - return account.Name + " " + days; -}