16 Commits

Author SHA1 Message Date
4ee82dce26 Send Content-Type on POST
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-21 19:32:20 +00:00
1da86e30c3 Finish migration
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-08-20 22:45:07 +00:00
51902cd65d Continue migration to echo 2022-08-20 22:44:59 +00:00
014c3818ba Continue migration to echo 2022-08-20 22:23:42 +00:00
210ddd65a5 Start migration to echo 2022-08-20 22:09:42 +00:00
5741236e2c Fix filters
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-04-24 20:48:08 +00:00
99549fb441 Implement filtering on frontend 2022-04-24 20:47:57 +00:00
67c79e252e Remove old logging 2022-04-24 20:40:12 +00:00
3b1174225a Add date filtering to backend 2022-04-24 20:40:03 +00:00
92f56b1046 Add date filters to UI 2022-04-24 20:21:32 +00:00
f068dd5009 Make menu entry generic 2022-04-24 20:14:45 +00:00
ebc34d7031 Make UI work for problematic and filtered transactions
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is failing
2022-04-24 20:12:41 +00:00
03ea4a31ad Implement UI 2022-04-24 20:12:23 +00:00
8636d04b84 Improve Backend 2022-04-24 20:03:53 +00:00
c63a8bc5d3 Add filters to UI 2022-04-24 19:40:57 +00:00
0aae7236ae Implement backend 2022-04-24 19:40:44 +00:00
13 changed files with 50 additions and 87 deletions

View File

@ -14,7 +14,6 @@ linters:
- gci # not working, shows errors on freshly formatted file - gci # not working, shows errors on freshly formatted file
- varnamelen - varnamelen
- lll - lll
linters-settings: linters-settings:
errcheck: errcheck:
exclude-functions: exclude-functions:
@ -26,18 +25,3 @@ linters-settings:
varnamelen: varnamelen:
ignore-decls: ignore-decls:
- c *gin.Context - c *gin.Context
wrapcheck:
ignoreSigs:
- .JSON(
- .Redirect(
- .String(
- .Errorf(
- errors.New(
- errors.Unwrap(
- .Wrap(
- .Wrapf(
- .WithMessage(
- .WithMessagef(
- .WithStack(
ignorePackageGlobs:
- git.javil.eu/jacob1123/budgeteer/postgres

View File

@ -1,18 +1,20 @@
# Budgeteer # Budgeteer
Budgeting Web-Application written in Go and inspired by [YNAB](https://youneedabudget.com). Budgeting Web-Application
## Getting started ## Data structure
The fastest way to get up and running quickly, is using docker-compose. Just download the [docker-compose.yml](https://git.javil.eu/jacob1123/budgeteer/src/branch/master/docker/docker-compose.yml) to some empty directory and run `docker-compose up -d`. This starts budgeteer, a postgres database and an adminer instance. The latter is optional and can be removed from the docker-compose.yml. 1 User
N Budgets
AccountID[]
CategoryID[]
PayeeID[]
## Known issues N Accounts
TransactionID[]
N Categories
AssignmentID[]
N Payees
Currently the application is usable when importing from YNAB via their CSV export. All balances should match the balances from YNAB. There are even unit-tests that confirm that using my personal budget. N Transactions
N Assignments
For people wishing to start fresh in Budgeteer, there currently are some blockers though:
- The ability to create new accounts and categories is missing (#59)
## Contributing
If you're willing to help, please check the issues for [help-wanted labels](https://git.javil.eu/jacob1123/budgeteer/issues?labels=4). Just using Budgeteer and reporting any issues is although very helpful.

View File

@ -1,12 +0,0 @@
(def go
(from (linux/alpine)
($ apk add go)))
(-> ($ go mod download)
(with-image go)
(with-mount *dir*/go.mod ./go.mod)
(with-mount *dir*/go.sum ./go.sum))
(def go-mods
(from go
($ go mod download)))

View File

@ -10,11 +10,11 @@ import (
) )
type FilterTransactionsRequest struct { type FilterTransactionsRequest struct {
CategoryID string `json:"categoryId"` CategoryID string `json:"category_id"`
PayeeID string `json:"payeeId"` PayeeID string `json:"payee_id"`
AccountID string `json:"accountId"` AccountID string `json:"account_id"`
FromDate time.Time `json:"fromDate"` FromDate time.Time `json:"from_date"`
ToDate time.Time `json:"toDate"` ToDate time.Time `json:"to_date"`
} }
func (h *Handler) filteredTransactions(c echo.Context) error { func (h *Handler) filteredTransactions(c echo.Context) error {
@ -58,7 +58,7 @@ func parseEmptyUUID(value string) (uuid.NullUUID, bool) {
return uuid.NullUUID{}, false return uuid.NullUUID{}, false
} }
return uuid.NullUUID{UUID: val, Valid: true}, true return uuid.NullUUID{val, true}, true
} }
func (h *Handler) problematicTransactions(c echo.Context) error { func (h *Handler) problematicTransactions(c echo.Context) error {

View File

@ -1,7 +1,6 @@
package server package server
import ( import (
"context"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -36,23 +35,17 @@ func TestRegisterUser(t *testing.T) {
t.Run("RegisterUser", func(t *testing.T) { t.Run("RegisterUser", func(t *testing.T) {
t.Parallel() t.Parallel()
request, err := http.NewRequestWithContext(context.Background(), request, err := http.NewRequest(
http.MethodPost, http.MethodPost,
"/api/v1/user/register", "/api/v1/user/register",
strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`)) strings.NewReader(`{"password":"pass","email":"info@example.com","name":"Test"}`))
request.Header.Set("Content-Type", "application/json")
context := engine.NewContext(request, recorder) context := engine.NewContext(request, recorder)
if err != nil { if err != nil {
t.Errorf("error creating request: %s", err) t.Errorf("error creating request: %s", err)
return return
} }
err = h.registerPost(context) h.registerPost(context)
if err != nil {
t.Error(err.Error())
t.Error("Error registering")
return
}
if recorder.Code != http.StatusOK { if recorder.Code != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK) t.Errorf("handler returned wrong status code: got %v want %v", recorder.Code, http.StatusOK)

View File

@ -41,7 +41,7 @@ func (h *Handler) budgetingForMonth(c echo.Context) error {
month, err := getDate(c) month, err := getDate(c)
if err != nil { if err != nil {
return c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String()) c.Redirect(http.StatusTemporaryRedirect, "/budget/"+budget.ID.String())
} }
data, err := h.getBudgetingViewForMonth(c.Request().Context(), budget, month) data, err := h.getBudgetingViewForMonth(c.Request().Context(), budget, month)

View File

@ -35,7 +35,7 @@ func (h *Handler) verifyLogin(c echo.Context) (budgeteer.Token, error) { //nolin
tokenString = tokenString[7:] tokenString = tokenString[7:]
token, err := h.TokenVerifier.VerifyToken(tokenString) token, err := h.TokenVerifier.VerifyToken(tokenString)
if err != nil { if err != nil {
return nil, fmt.Errorf("verify token '%s': %w", tokenString, err) return nil, fmt.Errorf("verify token '%s': %s", tokenString, err)
} }
return token, nil return token, nil
@ -63,7 +63,7 @@ func (h *Handler) loginPost(c echo.Context) error {
var login loginInformation var login loginInformation
err := c.Bind(&login) err := c.Bind(&login)
if err != nil { if err != nil {
return fmt.Errorf("parse payload: %w", err) return err
} }
user, err := h.Service.GetUserByUsername(c.Request().Context(), login.User) user, err := h.Service.GetUserByUsername(c.Request().Context(), login.User)
@ -72,12 +72,12 @@ func (h *Handler) loginPost(c echo.Context) error {
} }
if err = h.CredentialsVerifier.Verify(login.Password, user.Password); err != nil { if err = h.CredentialsVerifier.Verify(login.Password, user.Password); err != nil {
return fmt.Errorf("verify password: %w", err) return err
} }
token, err := h.TokenVerifier.CreateToken(&user) token, err := h.TokenVerifier.CreateToken(&user)
if err != nil { if err != nil {
return fmt.Errorf("create token: %w", err) return err
} }
go h.UpdateLastLogin(user.ID) go h.UpdateLastLogin(user.ID)
@ -120,7 +120,7 @@ func (h *Handler) registerPost(c echo.Context) error {
hash, err := h.CredentialsVerifier.Hash(register.Password) hash, err := h.CredentialsVerifier.Hash(register.Password)
if err != nil { if err != nil {
return fmt.Errorf("hash password: %w", err) return err
} }
createUser := postgres.CreateUserParams{ createUser := postgres.CreateUserParams{
@ -135,7 +135,7 @@ func (h *Handler) registerPost(c echo.Context) error {
token, err := h.TokenVerifier.CreateToken(&user) token, err := h.TokenVerifier.CreateToken(&user)
if err != nil { if err != nil {
return fmt.Errorf("create token: %w", err) return err
} }
go h.UpdateLastLogin(user.ID) go h.UpdateLastLogin(user.ID)

View File

@ -1,7 +1,6 @@
package server package server
import ( import (
"fmt"
"net/http" "net/http"
"git.javil.eu/jacob1123/budgeteer/postgres" "git.javil.eu/jacob1123/budgeteer/postgres"
@ -27,12 +26,12 @@ func (h *Handler) importYNAB(c echo.Context) error {
transactionsFile, err := c.FormFile("transactions") transactionsFile, err := c.FormFile("transactions")
if err != nil { if err != nil {
return fmt.Errorf("get transactions: %w", err) return err
} }
transactions, err := transactionsFile.Open() transactions, err := transactionsFile.Open()
if err != nil { if err != nil {
return fmt.Errorf("open transactions: %w", err) return err
} }
err = ynab.ImportTransactions(c.Request().Context(), transactions) err = ynab.ImportTransactions(c.Request().Context(), transactions)
@ -42,12 +41,12 @@ func (h *Handler) importYNAB(c echo.Context) error {
assignmentsFile, err := c.FormFile("assignments") assignmentsFile, err := c.FormFile("assignments")
if err != nil { if err != nil {
return fmt.Errorf("get assignments: %w", err) return err
} }
assignments, err := assignmentsFile.Open() assignments, err := assignmentsFile.Open()
if err != nil { if err != nil {
return fmt.Errorf("open assignments: %w", err) return err
} }
err = ynab.ImportAssignments(c.Request().Context(), assignments) err = ynab.ImportAssignments(c.Request().Context(), assignments)

View File

@ -11,10 +11,10 @@ export interface Suggestion {
} }
const props = defineProps<{ const props = defineProps<{
text: string | null, text: string,
id: string | null, id: string | undefined,
model: string, model: string,
type?: string | null, type?: string | undefined,
}>(); }>();
const SearchQuery = ref(props.text || ""); const SearchQuery = ref(props.text || "");

View File

@ -16,9 +16,9 @@ const TX = ref<Transaction>({
Memo: "", Memo: "",
Amount: 0, Amount: 0,
Payee: "", Payee: "",
PayeeID: null, PayeeID: undefined,
Category: "", Category: "",
CategoryID: null, CategoryID: undefined,
CategoryGroup: "", CategoryGroup: "",
GroupID: "", GroupID: "",
ID: "", ID: "",

View File

@ -42,7 +42,7 @@ const filters = ref({
ToDate: new Date(2999,11,32), ToDate: new Date(2999,11,32),
}); });
watch(() => (filters.value.AccountID ?? "") watch(() => filters.value.AccountID
+ filters.value.PayeeID + filters.value.PayeeID
+ filters.value.CategoryID + filters.value.CategoryID
+ filters.value.FromDate?.toISOString() + filters.value.FromDate?.toISOString()

View File

@ -2,7 +2,7 @@ import { defineStore } from "pinia";
import { GET, POST } from "../api"; import { GET, POST } from "../api";
import { useBudgetsStore } from "./budget"; import { useBudgetsStore } from "./budget";
import { useSessionStore } from "./session"; import { useSessionStore } from "./session";
import { Transaction, useTransactionsStore } from "./transactions"; import { useTransactionsStore } from "./transactions";
interface State { interface State {
Accounts: Map<string, Account>; Accounts: Map<string, Account>;
@ -200,7 +200,6 @@ export const useAccountStore = defineStore("budget/account", {
transactionsStore.AddTransactions( transactionsStore.AddTransactions(
response.Transactions response.Transactions
); );
account.Transactions = response.Transactions.map((x : Transaction) =>x.ID);
}, },
async FetchMonthBudget(budgetid: string, year: number, month: number) { async FetchMonthBudget(budgetid: string, year: number, month: number) {
const result = await GET( const result = await GET(

View File

@ -17,12 +17,12 @@ export interface Transaction {
TransferAccount: string; TransferAccount: string;
CategoryGroup: string; CategoryGroup: string;
Category: string; Category: string;
CategoryID: string | null; CategoryID: string | undefined;
Memo: string; Memo: string;
Status: string; Status: string;
GroupID: string; GroupID: string;
Payee: string; Payee: string;
PayeeID: string | null; PayeeID: string | undefined;
Amount: number; Amount: number;
Reconciled: boolean; Reconciled: boolean;
Account: string; Account: string;
@ -47,12 +47,10 @@ export const useTransactionsStore = defineStore("budget/transactions", {
} }
return reconciledBalance; return reconciledBalance;
}, },
TransactionsByDate(state) : Record<string, Transaction[]>|undefined{ TransactionsByDate(state) : Record<string, Transaction[]> {
const accountsStore = useAccountStore(); const accountsStore = useAccountStore();
const account = accountsStore.CurrentAccount; const accountID = accountsStore.CurrentAccountID;
if(account === undefined) const allTransactions = [...this.Transactions.values()].filter(x => x.AccountID == accountID);
return undefined;
const allTransactions = account!.Transactions.map(x => this.Transactions.get(x) ?? {} as Transaction);
return groupBy(allTransactions, x => formatDate(x.Date)); return groupBy(allTransactions, x => formatDate(x.Date));
}, },
TransactionsList(state) : Transaction[] { TransactionsList(state) : Transaction[] {
@ -95,11 +93,11 @@ export const useTransactionsStore = defineStore("budget/transactions", {
async GetFilteredTransactions(accountID : string | null, categoryID : string | null, payeeID : string | null, fromDate : string, toDate : string) { async GetFilteredTransactions(accountID : string | null, categoryID : string | null, payeeID : string | null, fromDate : string, toDate : string) {
const budgetStore = useBudgetsStore(); const budgetStore = useBudgetsStore();
const payload = JSON.stringify({ const payload = JSON.stringify({
categoryId: categoryID, category_id: categoryID,
payeeId: payeeID, payee_id: payeeID,
accountId: accountID, account_id: accountID,
fromDate: fromDate, from_date: fromDate,
toDate: toDate, to_date: toDate,
}); });
const result = await POST("/budget/" + budgetStore.CurrentBudgetID + "/filtered-transactions", payload); const result = await POST("/budget/" + budgetStore.CurrentBudgetID + "/filtered-transactions", payload);
const response = await result.json(); const response = await result.json();