diff --git a/.drone.yml b/.drone.yml index a3877dd..0274de1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -23,9 +23,10 @@ steps: commands: - task ci when: + branch: + - master event: - exclude: - - pull_request + - push - name: docker image: plugins/docker diff --git a/.vscode/settings.json b/.vscode/settings.json index 1820a53..b41677f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,7 @@ }, "gopls": { "formatting.gofumpt": true, - } + }, + "editor.detectIndentation": false, + "editor.tabSize": 2 } \ No newline at end of file diff --git a/postgres/queries/transactions.sql b/postgres/queries/transactions.sql index 2376d0b..aebc7ff 100644 --- a/postgres/queries/transactions.sql +++ b/postgres/queries/transactions.sql @@ -45,4 +45,20 @@ AND accounts.id = transactions.account_id; -- name: GetTransactionsByMonthAndCategory :many SELECT * FROM transactions_by_month -WHERE transactions_by_month.budget_id = @budget_id; \ No newline at end of file +WHERE transactions_by_month.budget_id = @budget_id; + +-- name: GetProblematicTransactions :many +SELECT transactions.* +FROM display_transactions AS transactions +LEFT JOIN accounts + ON transactions.account_id = accounts.id +LEFT JOIN transactions AS otherGroupTransaction + ON transactions.group_id = otherGroupTransaction.group_id + AND transactions.id != otherGroupTransaction.id + AND transactions.account_id != otherGroupTransaction.account_id +LEFT JOIn accounts AS otherGroupAccount + ON otherGroupTransaction.account_id = otherGroupAccount.id +WHERE transactions.category_id IS NULL +AND accounts.on_budget +AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget) +AND accounts.budget_id = $1; \ No newline at end of file diff --git a/postgres/transactions.sql.go b/postgres/transactions.sql.go index cdab72a..60fcf1d 100644 --- a/postgres/transactions.sql.go +++ b/postgres/transactions.sql.go @@ -117,6 +117,62 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid return items, nil } +const getProblematicTransactions = `-- name: GetProblematicTransactions :many +SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, transactions.account, transactions.payee_id, transactions.category_id, transactions.payee, transactions.category_group, transactions.category, transactions.transfer_account, transactions.budget_id, transactions.account_id +FROM display_transactions AS transactions +LEFT JOIN accounts + ON transactions.account_id = accounts.id +LEFT JOIN transactions AS otherGroupTransaction + ON transactions.group_id = otherGroupTransaction.group_id + AND transactions.id != otherGroupTransaction.id + AND transactions.account_id != otherGroupTransaction.account_id +LEFT JOIn accounts AS otherGroupAccount + ON otherGroupTransaction.account_id = otherGroupAccount.id +WHERE transactions.category_id IS NULL +AND accounts.on_budget +AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget) +AND accounts.budget_id = $1 +` + +func (q *Queries) GetProblematicTransactions(ctx context.Context, budgetID uuid.UUID) ([]DisplayTransaction, error) { + rows, err := q.db.QueryContext(ctx, getProblematicTransactions, budgetID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []DisplayTransaction + for rows.Next() { + var i DisplayTransaction + if err := rows.Scan( + &i.ID, + &i.Date, + &i.Memo, + &i.Amount, + &i.GroupID, + &i.Status, + &i.Account, + &i.PayeeID, + &i.CategoryID, + &i.Payee, + &i.CategoryGroup, + &i.Category, + &i.TransferAccount, + &i.BudgetID, + &i.AccountID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getTransaction = `-- name: GetTransaction :one SELECT id, date, memo, amount, group_id, status, account, payee_id, category_id, payee, category_group, category, transfer_account, budget_id, account_id FROM display_transactions WHERE id = $1 diff --git a/server/account.go b/server/account.go index d92746c..758ada8 100644 --- a/server/account.go +++ b/server/account.go @@ -8,6 +8,23 @@ import ( "github.com/google/uuid" ) +func (h *Handler) problematicTransactions(c *gin.Context) { + budgetID := c.Param("budgetid") + budgetUUID, err := uuid.Parse(budgetID) + if err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + transactions, err := h.Service.GetProblematicTransactions(c.Request.Context(), budgetUUID) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusOK, TransactionsResponse{nil, transactions}) +} + func (h *Handler) transactionsForAccount(c *gin.Context) { accountID := c.Param("accountid") accountUUID, err := uuid.Parse(accountID) @@ -28,11 +45,11 @@ func (h *Handler) transactionsForAccount(c *gin.Context) { return } - c.JSON(http.StatusOK, TransactionsResponse{account, transactions}) + c.JSON(http.StatusOK, TransactionsResponse{&account, transactions}) } type TransactionsResponse struct { - Account postgres.Account + Account *postgres.Account Transactions []postgres.DisplayTransaction } diff --git a/server/autocomplete.go b/server/autocomplete.go index 5472430..9d88c70 100644 --- a/server/autocomplete.go +++ b/server/autocomplete.go @@ -9,6 +9,28 @@ import ( "github.com/google/uuid" ) +func (h *Handler) autocompleteAccounts(c *gin.Context) { + budgetID := c.Param("budgetid") + budgetUUID, err := uuid.Parse(budgetID) + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"budgetid missing from URL"}) + return + } + + query := c.Request.URL.Query().Get("s") + searchParams := postgres.SearchAccountsParams{ + BudgetID: budgetUUID, + Search: "%" + query + "%", + } + categories, err := h.Service.SearchAccounts(c.Request.Context(), searchParams) + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusOK, categories) +} + func (h *Handler) autocompleteCategories(c *gin.Context) { budgetID := c.Param("budgetid") budgetUUID, err := uuid.Parse(budgetID) diff --git a/server/http.go b/server/http.go index b96a645..de5f7c8 100644 --- a/server/http.go +++ b/server/http.go @@ -65,7 +65,9 @@ func (h *Handler) LoadRoutes(router *gin.Engine) { budget.GET("/:budgetid/:year/:month", h.budgetingForMonth) budget.POST("/:budgetid/category/:categoryid/:year/:month", h.setCategoryAssignment) budget.GET("/:budgetid/autocomplete/payees", h.autocompletePayee) + budget.GET("/:budgetid/autocomplete/accounts", h.autocompleteAccounts) budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories) + budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions) budget.DELETE("/:budgetid", h.deleteBudget) budget.POST("/:budgetid/import/ynab", h.importYNAB) budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions) diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 60afd68..3aa8946 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -7,7 +7,10 @@ module.exports = { ], rules: { // override/add rules settings here, such as: - 'vue/max-attributes-per-line': 'off' + 'vue/max-attributes-per-line': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/first-attribute-linebreak': 'off', + 'vue/html-closing-bracket-newline': 'off', // 'vue/no-unused-vars': 'error' }, parser: "vue-eslint-parser", diff --git a/web/src/components/MainMenu.vue b/web/src/components/MainMenu.vue index 49dc2ee..9ea4820 100644 --- a/web/src/components/MainMenu.vue +++ b/web/src/components/MainMenu.vue @@ -10,37 +10,35 @@ const router = useRouter(); const CurrentBudgetName = computed(() => useBudgetsStore().CurrentBudgetName); const LoggedIn = computed(() => useSessionStore().LoggedIn); function logout() { - useSessionStore().logout(); - router.push("/login"); + useSessionStore().logout(); + router.push("/login"); } function toggleMenu() { - useSettingsStore().toggleMenu(); + useSettingsStore().toggleMenu(); } function toggleMenuSize() { - useSettingsStore().toggleMenuSize(); + useSettingsStore().toggleMenuSize(); } router.afterEach(function(to, from) { - useSettingsStore().Menu.Show = false; + useSettingsStore().Menu.Show = false; }) diff --git a/web/src/components/TransactionEditRow.vue b/web/src/components/TransactionEditRow.vue index f864b64..6a517a6 100644 --- a/web/src/components/TransactionEditRow.vue +++ b/web/src/components/TransactionEditRow.vue @@ -8,7 +8,8 @@ import Input from "./Input.vue"; import Button from "./SimpleButton.vue"; const props = defineProps<{ - transactionid: string + transactionid: string, + withAccount: boolean, }>() const emit = defineEmits(["save"]); @@ -18,67 +19,47 @@ const TX = transactionsStore.Transactions.get(props.transactionid)!; const payeeType = ref(undefined); const payload = computed(() => JSON.stringify({ - date: TX.Date.toISOString().split("T")[0], - payee: { - Name: TX.Payee, - ID: TX.PayeeID, - Type: payeeType.value, - }, - categoryId: TX.CategoryID, - memo: TX.Memo, - amount: TX.Amount.toString(), - state: "Uncleared" + date: TX.Date.toISOString().split("T")[0], + payee: { + Name: TX.Payee, + ID: TX.PayeeID, + Type: payeeType.value, + }, + categoryId: TX.CategoryID, + memo: TX.Memo, + amount: TX.Amount.toString(), + state: "Uncleared" })); function saveTransaction(e: MouseEvent) { - e.preventDefault(); - transactionsStore.editTransaction(TX.ID, payload.value); - emit('save'); + e.preventDefault(); + transactionsStore.editTransaction(TX.ID, payload.value); + emit('save'); }