Merge pull request 'Implement editing of transactions' (#23) from edit-transaction into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
This commit is contained in:
commit
98890f10eb
@ -130,7 +130,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchAccounts = `-- name: SearchAccounts :many
|
const searchAccounts = `-- name: SearchAccounts :many
|
||||||
SELECT accounts.id, accounts.budget_id, accounts.name, true as is_account FROM accounts
|
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
|
||||||
WHERE accounts.budget_id = $1
|
WHERE accounts.budget_id = $1
|
||||||
AND accounts.name LIKE $2
|
AND accounts.name LIKE $2
|
||||||
ORDER BY accounts.name
|
ORDER BY accounts.name
|
||||||
@ -142,10 +142,10 @@ type SearchAccountsParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SearchAccountsRow struct {
|
type SearchAccountsRow struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
BudgetID uuid.UUID
|
BudgetID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
IsAccount bool
|
Type interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams) ([]SearchAccountsRow, error) {
|
func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams) ([]SearchAccountsRow, error) {
|
||||||
@ -161,7 +161,7 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.BudgetID,
|
&i.BudgetID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.IsAccount,
|
&i.Type,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,8 @@ func (q *Queries) GetCategoryGroups(ctx context.Context, budgetID uuid.UUID) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchCategories = `-- name: SearchCategories :many
|
const searchCategories = `-- name: SearchCategories :many
|
||||||
SELECT CONCAT(category_groups.name, ' : ', categories.name) as name, categories.id FROM categories
|
SELECT CONCAT(category_groups.name, ' : ', categories.name) as name, categories.id, 'category' as type
|
||||||
|
FROM categories
|
||||||
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
WHERE category_groups.budget_id = $1
|
WHERE category_groups.budget_id = $1
|
||||||
AND categories.name LIKE $2
|
AND categories.name LIKE $2
|
||||||
@ -133,6 +134,7 @@ type SearchCategoriesParams struct {
|
|||||||
type SearchCategoriesRow struct {
|
type SearchCategoriesRow struct {
|
||||||
Name interface{}
|
Name interface{}
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
|
Type interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchCategories(ctx context.Context, arg SearchCategoriesParams) ([]SearchCategoriesRow, error) {
|
func (q *Queries) SearchCategories(ctx context.Context, arg SearchCategoriesParams) ([]SearchCategoriesRow, error) {
|
||||||
@ -144,7 +146,7 @@ func (q *Queries) SearchCategories(ctx context.Context, arg SearchCategoriesPara
|
|||||||
var items []SearchCategoriesRow
|
var items []SearchCategoriesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i SearchCategoriesRow
|
var i SearchCategoriesRow
|
||||||
if err := rows.Scan(&i.Name, &i.ID); err != nil {
|
if err := rows.Scan(&i.Name, &i.ID, &i.Type); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
|
@ -58,7 +58,7 @@ func (q *Queries) GetPayees(ctx context.Context, budgetID uuid.UUID) ([]Payee, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchPayees = `-- name: SearchPayees :many
|
const searchPayees = `-- name: SearchPayees :many
|
||||||
SELECT payees.id, payees.budget_id, payees.name FROM payees
|
SELECT payees.id, payees.budget_id, payees.name, 'payee' as type FROM payees
|
||||||
WHERE payees.budget_id = $1
|
WHERE payees.budget_id = $1
|
||||||
AND payees.name LIKE $2
|
AND payees.name LIKE $2
|
||||||
ORDER BY payees.name
|
ORDER BY payees.name
|
||||||
@ -69,16 +69,28 @@ type SearchPayeesParams struct {
|
|||||||
Search string
|
Search string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchPayees(ctx context.Context, arg SearchPayeesParams) ([]Payee, error) {
|
type SearchPayeesRow struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
BudgetID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Type interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SearchPayees(ctx context.Context, arg SearchPayeesParams) ([]SearchPayeesRow, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, searchPayees, arg.BudgetID, arg.Search)
|
rows, err := q.db.QueryContext(ctx, searchPayees, arg.BudgetID, arg.Search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Payee
|
var items []SearchPayeesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Payee
|
var i SearchPayeesRow
|
||||||
if err := rows.Scan(&i.ID, &i.BudgetID, &i.Name); err != nil {
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.BudgetID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Type,
|
||||||
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
|
@ -22,7 +22,7 @@ GROUP BY accounts.id, accounts.name
|
|||||||
ORDER BY accounts.name;
|
ORDER BY accounts.name;
|
||||||
|
|
||||||
-- name: SearchAccounts :many
|
-- name: SearchAccounts :many
|
||||||
SELECT accounts.id, accounts.budget_id, accounts.name, true as is_account FROM accounts
|
SELECT accounts.id, accounts.budget_id, accounts.name, 'account' as type FROM accounts
|
||||||
WHERE accounts.budget_id = @budget_id
|
WHERE accounts.budget_id = @budget_id
|
||||||
AND accounts.name LIKE @search
|
AND accounts.name LIKE @search
|
||||||
ORDER BY accounts.name;
|
ORDER BY accounts.name;
|
||||||
|
@ -21,7 +21,8 @@ WHERE category_groups.budget_id = $1
|
|||||||
ORDER BY category_groups.name, categories.name;
|
ORDER BY category_groups.name, categories.name;
|
||||||
|
|
||||||
-- name: SearchCategories :many
|
-- name: SearchCategories :many
|
||||||
SELECT CONCAT(category_groups.name, ' : ', categories.name) as name, categories.id FROM categories
|
SELECT CONCAT(category_groups.name, ' : ', categories.name) as name, categories.id, 'category' as type
|
||||||
|
FROM categories
|
||||||
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||||
WHERE category_groups.budget_id = @budget_id
|
WHERE category_groups.budget_id = @budget_id
|
||||||
AND categories.name LIKE @search
|
AND categories.name LIKE @search
|
||||||
|
@ -10,7 +10,7 @@ WHERE payees.budget_id = $1
|
|||||||
ORDER BY name;
|
ORDER BY name;
|
||||||
|
|
||||||
-- name: SearchPayees :many
|
-- name: SearchPayees :many
|
||||||
SELECT payees.* FROM payees
|
SELECT payees.*, 'payee' as type FROM payees
|
||||||
WHERE payees.budget_id = @budget_id
|
WHERE payees.budget_id = @budget_id
|
||||||
AND payees.name LIKE @search
|
AND payees.name LIKE @search
|
||||||
ORDER BY payees.name;
|
ORDER BY payees.name;
|
||||||
|
@ -13,10 +13,9 @@ UPDATE transactions
|
|||||||
SET date = $1,
|
SET date = $1,
|
||||||
memo = $2,
|
memo = $2,
|
||||||
amount = $3,
|
amount = $3,
|
||||||
account_id = $4,
|
payee_id = $4,
|
||||||
payee_id = $5,
|
category_id = $5
|
||||||
category_id = $6
|
WHERE id = $6;
|
||||||
WHERE id = $7;
|
|
||||||
|
|
||||||
-- name: DeleteTransaction :exec
|
-- name: DeleteTransaction :exec
|
||||||
DELETE FROM transactions
|
DELETE FROM transactions
|
||||||
@ -25,7 +24,7 @@ WHERE id = $1;
|
|||||||
-- name: GetAllTransactionsForBudget :many
|
-- name: GetAllTransactionsForBudget :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo,
|
SELECT transactions.id, transactions.date, transactions.memo,
|
||||||
transactions.amount, transactions.group_id, transactions.status,
|
transactions.amount, transactions.group_id, transactions.status,
|
||||||
accounts.name as account,
|
accounts.name as account, transactions.payee_id, transactions.category_id,
|
||||||
COALESCE(payees.name, '') as payee,
|
COALESCE(payees.name, '') as payee,
|
||||||
COALESCE(category_groups.name, '') as category_group,
|
COALESCE(category_groups.name, '') as category_group,
|
||||||
COALESCE(categories.name, '') as category,
|
COALESCE(categories.name, '') as category,
|
||||||
@ -47,7 +46,7 @@ ORDER BY transactions.date DESC;
|
|||||||
-- name: GetTransactionsForAccount :many
|
-- name: GetTransactionsForAccount :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo,
|
SELECT transactions.id, transactions.date, transactions.memo,
|
||||||
transactions.amount, transactions.group_id, transactions.status,
|
transactions.amount, transactions.group_id, transactions.status,
|
||||||
accounts.name as account,
|
accounts.name as account, transactions.payee_id, transactions.category_id,
|
||||||
COALESCE(payees.name, '') as payee,
|
COALESCE(payees.name, '') as payee,
|
||||||
COALESCE(category_groups.name, '') as category_group,
|
COALESCE(category_groups.name, '') as category_group,
|
||||||
COALESCE(categories.name, '') as category,
|
COALESCE(categories.name, '') as category,
|
||||||
|
@ -83,7 +83,7 @@ func (q *Queries) DeleteTransaction(ctx context.Context, id uuid.UUID) error {
|
|||||||
const getAllTransactionsForBudget = `-- name: GetAllTransactionsForBudget :many
|
const getAllTransactionsForBudget = `-- name: GetAllTransactionsForBudget :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo,
|
SELECT transactions.id, transactions.date, transactions.memo,
|
||||||
transactions.amount, transactions.group_id, transactions.status,
|
transactions.amount, transactions.group_id, transactions.status,
|
||||||
accounts.name as account,
|
accounts.name as account, transactions.payee_id, transactions.category_id,
|
||||||
COALESCE(payees.name, '') as payee,
|
COALESCE(payees.name, '') as payee,
|
||||||
COALESCE(category_groups.name, '') as category_group,
|
COALESCE(category_groups.name, '') as category_group,
|
||||||
COALESCE(categories.name, '') as category,
|
COALESCE(categories.name, '') as category,
|
||||||
@ -111,6 +111,8 @@ type GetAllTransactionsForBudgetRow struct {
|
|||||||
GroupID uuid.NullUUID
|
GroupID uuid.NullUUID
|
||||||
Status TransactionStatus
|
Status TransactionStatus
|
||||||
Account string
|
Account string
|
||||||
|
PayeeID uuid.NullUUID
|
||||||
|
CategoryID uuid.NullUUID
|
||||||
Payee string
|
Payee string
|
||||||
CategoryGroup string
|
CategoryGroup string
|
||||||
Category string
|
Category string
|
||||||
@ -134,6 +136,8 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid
|
|||||||
&i.GroupID,
|
&i.GroupID,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Account,
|
&i.Account,
|
||||||
|
&i.PayeeID,
|
||||||
|
&i.CategoryID,
|
||||||
&i.Payee,
|
&i.Payee,
|
||||||
&i.CategoryGroup,
|
&i.CategoryGroup,
|
||||||
&i.Category,
|
&i.Category,
|
||||||
@ -211,7 +215,7 @@ func (q *Queries) GetTransactionsByMonthAndCategory(ctx context.Context, budgetI
|
|||||||
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
|
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
|
||||||
SELECT transactions.id, transactions.date, transactions.memo,
|
SELECT transactions.id, transactions.date, transactions.memo,
|
||||||
transactions.amount, transactions.group_id, transactions.status,
|
transactions.amount, transactions.group_id, transactions.status,
|
||||||
accounts.name as account,
|
accounts.name as account, transactions.payee_id, transactions.category_id,
|
||||||
COALESCE(payees.name, '') as payee,
|
COALESCE(payees.name, '') as payee,
|
||||||
COALESCE(category_groups.name, '') as category_group,
|
COALESCE(category_groups.name, '') as category_group,
|
||||||
COALESCE(categories.name, '') as category,
|
COALESCE(categories.name, '') as category,
|
||||||
@ -240,6 +244,8 @@ type GetTransactionsForAccountRow struct {
|
|||||||
GroupID uuid.NullUUID
|
GroupID uuid.NullUUID
|
||||||
Status TransactionStatus
|
Status TransactionStatus
|
||||||
Account string
|
Account string
|
||||||
|
PayeeID uuid.NullUUID
|
||||||
|
CategoryID uuid.NullUUID
|
||||||
Payee string
|
Payee string
|
||||||
CategoryGroup string
|
CategoryGroup string
|
||||||
Category string
|
Category string
|
||||||
@ -263,6 +269,8 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.
|
|||||||
&i.GroupID,
|
&i.GroupID,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.Account,
|
&i.Account,
|
||||||
|
&i.PayeeID,
|
||||||
|
&i.CategoryID,
|
||||||
&i.Payee,
|
&i.Payee,
|
||||||
&i.CategoryGroup,
|
&i.CategoryGroup,
|
||||||
&i.Category,
|
&i.Category,
|
||||||
@ -286,17 +294,15 @@ UPDATE transactions
|
|||||||
SET date = $1,
|
SET date = $1,
|
||||||
memo = $2,
|
memo = $2,
|
||||||
amount = $3,
|
amount = $3,
|
||||||
account_id = $4,
|
payee_id = $4,
|
||||||
payee_id = $5,
|
category_id = $5
|
||||||
category_id = $6
|
WHERE id = $6
|
||||||
WHERE id = $7
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateTransactionParams struct {
|
type UpdateTransactionParams struct {
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Memo string
|
Memo string
|
||||||
Amount numeric.Numeric
|
Amount numeric.Numeric
|
||||||
AccountID uuid.UUID
|
|
||||||
PayeeID uuid.NullUUID
|
PayeeID uuid.NullUUID
|
||||||
CategoryID uuid.NullUUID
|
CategoryID uuid.NullUUID
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
@ -307,7 +313,6 @@ func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionPa
|
|||||||
arg.Date,
|
arg.Date,
|
||||||
arg.Memo,
|
arg.Memo,
|
||||||
arg.Amount,
|
arg.Amount,
|
||||||
arg.AccountID,
|
|
||||||
arg.PayeeID,
|
arg.PayeeID,
|
||||||
arg.CategoryID,
|
arg.CategoryID,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
|
@ -15,19 +15,16 @@ import (
|
|||||||
type NewTransactionPayload struct {
|
type NewTransactionPayload struct {
|
||||||
Date JSONDate `json:"date"`
|
Date JSONDate `json:"date"`
|
||||||
Payee struct {
|
Payee struct {
|
||||||
ID uuid.NullUUID
|
|
||||||
Name string
|
|
||||||
IsAccount bool
|
|
||||||
} `json:"payee"`
|
|
||||||
Category struct {
|
|
||||||
ID uuid.NullUUID
|
ID uuid.NullUUID
|
||||||
Name string
|
Name string
|
||||||
} `json:"category"`
|
Type string
|
||||||
Memo string `json:"memo"`
|
} `json:"payee"`
|
||||||
Amount string `json:"amount"`
|
CategoryID uuid.NullUUID `json:"categoryId"`
|
||||||
BudgetID uuid.UUID `json:"budgetId"`
|
Memo string `json:"memo"`
|
||||||
AccountID uuid.UUID `json:"accountId"`
|
Amount string `json:"amount"`
|
||||||
State string `json:"state"`
|
BudgetID uuid.UUID `json:"budgetId"`
|
||||||
|
AccountID uuid.UUID `json:"accountId"`
|
||||||
|
State string `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) newTransaction(c *gin.Context) {
|
func (h *Handler) newTransaction(c *gin.Context) {
|
||||||
@ -44,26 +41,27 @@ func (h *Handler) newTransaction(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newTransaction := postgres.CreateTransactionParams{
|
transactionID := c.Param("transactionid")
|
||||||
Memo: payload.Memo,
|
if transactionID != "" {
|
||||||
Date: time.Time(payload.Date),
|
h.UpdateTransaction(payload, amount, transactionID, c)
|
||||||
Amount: amount,
|
return
|
||||||
Status: postgres.TransactionStatus(payload.State),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Payee.IsAccount {
|
newTransaction := postgres.CreateTransactionParams{
|
||||||
newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
|
Memo: payload.Memo,
|
||||||
newTransaction.Amount = amount.Neg()
|
Date: time.Time(payload.Date),
|
||||||
newTransaction.AccountID = payload.Payee.ID.UUID
|
Amount: amount,
|
||||||
newTransaction.CategoryID = uuid.NullUUID{}
|
Status: postgres.TransactionStatus(payload.State),
|
||||||
|
CategoryID: payload.CategoryID,
|
||||||
|
AccountID: payload.AccountID,
|
||||||
|
}
|
||||||
|
|
||||||
_, err = h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
if payload.Payee.Type == "account" {
|
||||||
|
err := h.CreateTransferForOtherAccount(newTransaction, amount, payload, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transfer transaction: %w", err))
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newTransaction.Amount = amount
|
|
||||||
} else {
|
} else {
|
||||||
payeeID, err := GetPayeeID(c.Request.Context(), payload, h)
|
payeeID, err := GetPayeeID(c.Request.Context(), payload, h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -72,17 +70,54 @@ func (h *Handler) newTransaction(c *gin.Context) {
|
|||||||
newTransaction.PayeeID = payeeID
|
newTransaction.PayeeID = payeeID
|
||||||
}
|
}
|
||||||
|
|
||||||
newTransaction.CategoryID = payload.Category.ID
|
|
||||||
newTransaction.AccountID = payload.AccountID
|
|
||||||
transaction, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
transaction, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
|
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, transaction)
|
c.JSON(http.StatusOK, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateTransaction(payload NewTransactionPayload, amount numeric.Numeric, transactionID string, c *gin.Context) {
|
||||||
|
transactionUUID := uuid.MustParse(transactionID)
|
||||||
|
if amount.IsZero() {
|
||||||
|
err := h.Service.DeleteTransaction(c.Request.Context(), transactionUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("delete transaction: %w", err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editTransaction := postgres.UpdateTransactionParams{
|
||||||
|
Memo: payload.Memo,
|
||||||
|
Date: time.Time(payload.Date),
|
||||||
|
Amount: amount,
|
||||||
|
PayeeID: payload.Payee.ID,
|
||||||
|
CategoryID: payload.CategoryID,
|
||||||
|
ID: transactionUUID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.Service.UpdateTransaction(c.Request.Context(), editTransaction)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("edit transaction: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateTransferForOtherAccount(newTransaction postgres.CreateTransactionParams, amount numeric.Numeric, payload NewTransactionPayload, c *gin.Context) error {
|
||||||
|
newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
|
||||||
|
newTransaction.Amount = amount.Neg()
|
||||||
|
newTransaction.AccountID = payload.Payee.ID.UUID
|
||||||
|
|
||||||
|
// transfer does not need category. Either it's account is off-budget or no category was supplied.
|
||||||
|
newTransaction.CategoryID = uuid.NullUUID{}
|
||||||
|
|
||||||
|
_, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create transfer transaction: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetPayeeID(context context.Context, payload NewTransactionPayload, h *Handler) (uuid.NullUUID, error) {
|
func GetPayeeID(context context.Context, payload NewTransactionPayload, h *Handler) (uuid.NullUUID, error) {
|
||||||
payeeID := payload.Payee.ID
|
payeeID := payload.Payee.ID
|
||||||
if payeeID.Valid {
|
if payeeID.Valid {
|
||||||
|
@ -1,43 +1,38 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent, PropType, ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { GET } from "../api";
|
import { GET } from "../api";
|
||||||
import { useBudgetsStore } from "../stores/budget";
|
import { useBudgetsStore } from "../stores/budget";
|
||||||
|
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
ID: string
|
ID: string
|
||||||
Name: string
|
Name: string
|
||||||
}
|
Type: string
|
||||||
|
|
||||||
interface Data {
|
|
||||||
Selected: Suggestion | undefined
|
|
||||||
SearchQuery: String
|
|
||||||
Suggestions: Suggestion[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Suggestion | undefined,
|
text: String,
|
||||||
type: String
|
id: String | undefined,
|
||||||
|
model: String,
|
||||||
|
type?: string | undefined,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const Selected = ref<Suggestion | undefined>(props.modelValue || undefined);
|
const SearchQuery = ref(props.text || "");
|
||||||
const SearchQuery = ref(props.modelValue?.Name || "");
|
|
||||||
const Suggestions = ref<Array<Suggestion>>([]);
|
const Suggestions = ref<Array<Suggestion>>([]);
|
||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:id", "update:text", "update:type"]);
|
||||||
watch(SearchQuery, () => {
|
watch(SearchQuery, () => {
|
||||||
load(SearchQuery.value);
|
load(SearchQuery.value);
|
||||||
});
|
});
|
||||||
function saveTransaction(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
function load(text: String) {
|
function load(text: String) {
|
||||||
emit('update:modelValue', { ID: null, Name: text });
|
emit('update:id', null);
|
||||||
|
emit('update:text', text);
|
||||||
|
emit('update:type', undefined);
|
||||||
if (text == "") {
|
if (text == "") {
|
||||||
Suggestions.value = [];
|
Suggestions.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const budgetStore = useBudgetsStore();
|
const budgetStore = useBudgetsStore();
|
||||||
GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + props.type + "?s=" + text)
|
GET("/budget/" + budgetStore.CurrentBudgetID + "/autocomplete/" + props.model + "?s=" + text)
|
||||||
.then(x => x.json())
|
.then(x => x.json())
|
||||||
.then(x => {
|
.then(x => {
|
||||||
let suggestions = x || [];
|
let suggestions = x || [];
|
||||||
@ -56,13 +51,13 @@ function keypress(e: KeyboardEvent) {
|
|||||||
const currentIndex = inputElements.indexOf(el);
|
const currentIndex = inputElements.indexOf(el);
|
||||||
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
|
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
|
||||||
(<HTMLInputElement>nextElement).focus();
|
(<HTMLInputElement>nextElement).focus();
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function selectElement(element: Suggestion) {
|
function selectElement(element: Suggestion) {
|
||||||
Selected.value = element;
|
emit('update:id', element.ID);
|
||||||
|
emit('update:text', element.Name);
|
||||||
|
emit('update:type', element.Type);
|
||||||
Suggestions.value = [];
|
Suggestions.value = [];
|
||||||
emit('update:modelValue', element);
|
|
||||||
};
|
};
|
||||||
function select(e: MouseEvent) {
|
function select(e: MouseEvent) {
|
||||||
const target = (<HTMLInputElement>e.target);
|
const target = (<HTMLInputElement>e.target);
|
||||||
@ -74,8 +69,9 @@ function select(e: MouseEvent) {
|
|||||||
selectElement(selected);
|
selectElement(selected);
|
||||||
};
|
};
|
||||||
function clear() {
|
function clear() {
|
||||||
Selected.value = undefined;
|
emit('update:id', null);
|
||||||
emit('update:modelValue', { ID: null, Name: SearchQuery.value });
|
emit('update:text', SearchQuery.value);
|
||||||
|
emit('update:type', undefined);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -84,10 +80,10 @@ function clear() {
|
|||||||
<input
|
<input
|
||||||
class="border-b-2 border-black"
|
class="border-b-2 border-black"
|
||||||
@keypress="keypress"
|
@keypress="keypress"
|
||||||
v-if="Selected == undefined"
|
v-if="id == undefined"
|
||||||
v-model="SearchQuery"
|
v-model="SearchQuery"
|
||||||
/>
|
/>
|
||||||
<span @click="clear" v-if="Selected != undefined" class="bg-gray-300">{{ Selected.Name }}</span>
|
<span @click="clear" v-if="id != undefined" class="bg-gray-300">{{ text }}</span>
|
||||||
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 w-64 p-2">
|
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 w-64 p-2">
|
||||||
<span
|
<span
|
||||||
v-for="suggestion in Suggestions"
|
v-for="suggestion in Suggestions"
|
||||||
|
33
web/src/components/DateInput.vue
Normal file
33
web/src/components/DateInput.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps(["modelValue"]);
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
function dateToYYYYMMDD(d: Date) : string {
|
||||||
|
// alternative implementations in https://stackoverflow.com/q/23593052/1850609
|
||||||
|
//return new Date(d.getTime() - (d.getTimezoneOffset() * 60 * 1000)).toISOString().split('T')[0];
|
||||||
|
return d.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValue(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
emit('update:modelValue', target.valueAsDate);
|
||||||
|
}
|
||||||
|
function selectAll(event: FocusEvent) {
|
||||||
|
// Workaround for Safari bug
|
||||||
|
// http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome
|
||||||
|
setTimeout(function () {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
target.select()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
ref="input"
|
||||||
|
v-bind:value="dateToYYYYMMDD(modelValue)"
|
||||||
|
@input="updateValue"
|
||||||
|
@focus="selectAll"
|
||||||
|
/>
|
||||||
|
</template>
|
60
web/src/components/TransactionEditRow.vue
Normal file
60
web/src/components/TransactionEditRow.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import Autocomplete from './Autocomplete.vue'
|
||||||
|
import { useAccountStore } from '../stores/budget-account'
|
||||||
|
import DateInput from "./DateInput.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
transactionid: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const TX = accountStore.Transactions.get(props.transactionid)!;
|
||||||
|
const payeeType = ref<string|undefined>(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"
|
||||||
|
}));
|
||||||
|
|
||||||
|
function saveTransaction(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
accountStore.editTransaction(TX.ID, payload.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 90px;" class="text-sm">
|
||||||
|
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
|
||||||
|
</td>
|
||||||
|
<td style="max-width: 150px;">
|
||||||
|
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
|
||||||
|
</td>
|
||||||
|
<td style="max-width: 200px;">
|
||||||
|
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
|
||||||
|
</td>
|
||||||
|
<td style="width: 80px;" class="text-right">
|
||||||
|
<input
|
||||||
|
class="text-right block w-full border-b-2 border-black"
|
||||||
|
type="currency"
|
||||||
|
v-model="TX.Amount"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td style="width: 20px;">
|
||||||
|
<input type="submit" @click="saveTransaction" value="Save" />
|
||||||
|
</td>
|
||||||
|
<td style="width: 20px;"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
@ -1,27 +1,43 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
|
import Autocomplete, { Suggestion } from '../components/Autocomplete.vue'
|
||||||
import { useAccountStore } from '../stores/budget-account'
|
import { Transaction, useAccountStore } from '../stores/budget-account'
|
||||||
|
import DateInput from "./DateInput.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
budgetid: string
|
budgetid: string
|
||||||
accountid: string
|
accountid: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const TransactionDate = ref(new Date().toISOString().substring(0, 10));
|
const TX = ref<Transaction>({
|
||||||
const Payee = ref<Suggestion | undefined>(undefined);
|
Date: new Date(),
|
||||||
const Category = ref<Suggestion | undefined>(undefined);
|
Memo: "",
|
||||||
const Memo = ref("");
|
Amount: 0,
|
||||||
const Amount = ref("0");
|
Payee: "",
|
||||||
|
PayeeID: undefined,
|
||||||
|
Category: "",
|
||||||
|
CategoryID: undefined,
|
||||||
|
CategoryGroup: "",
|
||||||
|
GroupID: "",
|
||||||
|
ID: "",
|
||||||
|
Status: "Uncleared",
|
||||||
|
TransferAccount: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const payeeType = ref<string|undefined>(undefined);
|
||||||
|
|
||||||
const payload = computed(() => JSON.stringify({
|
const payload = computed(() => JSON.stringify({
|
||||||
budgetId: props.budgetid,
|
budgetId: props.budgetid,
|
||||||
accountId: props.accountid,
|
accountId: props.accountid,
|
||||||
date: TransactionDate.value,
|
date: TX.value.Date.toISOString().split("T")[0],
|
||||||
payee: Payee.value,
|
payee: {
|
||||||
category: Category.value,
|
Name: TX.value.Payee,
|
||||||
memo: Memo.value,
|
ID: TX.value.PayeeID,
|
||||||
amount: Amount.value,
|
Type: payeeType.value,
|
||||||
|
},
|
||||||
|
categoryId: TX.value.CategoryID,
|
||||||
|
memo: TX.value.Memo,
|
||||||
|
amount: TX.value.Amount.toString(),
|
||||||
state: "Uncleared"
|
state: "Uncleared"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -35,22 +51,22 @@ function saveTransaction(e: MouseEvent) {
|
|||||||
<template>
|
<template>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 90px;" class="text-sm">
|
<td style="width: 90px;" class="text-sm">
|
||||||
<input class="border-b-2 border-black" type="date" v-model="TransactionDate" />
|
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
|
||||||
</td>
|
</td>
|
||||||
<td style="max-width: 150px;">
|
<td style="max-width: 150px;">
|
||||||
<Autocomplete v-model="Payee" type="payees" />
|
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
|
||||||
</td>
|
</td>
|
||||||
<td style="max-width: 200px;">
|
<td style="max-width: 200px;">
|
||||||
<Autocomplete v-model="Category" type="categories" />
|
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input class="block w-full border-b-2 border-black" type="text" v-model="Memo" />
|
<input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 80px;" class="text-right">
|
<td style="width: 80px;" class="text-right">
|
||||||
<input
|
<input
|
||||||
class="text-right block w-full border-b-2 border-black"
|
class="text-right block w-full border-b-2 border-black"
|
||||||
type="currency"
|
type="currency"
|
||||||
v-model="Amount"
|
v-model="TX.Amount"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 20px;">
|
<td style="width: 20px;">
|
||||||
|
@ -1,24 +1,28 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useBudgetsStore } from "../stores/budget";
|
import { useBudgetsStore } from "../stores/budget";
|
||||||
import { Transaction } from "../stores/budget-account";
|
import { Transaction } from "../stores/budget-account";
|
||||||
import Currency from "./Currency.vue";
|
import Currency from "./Currency.vue";
|
||||||
|
import TransactionEditRow from "./TransactionEditRow.vue";
|
||||||
|
import { formatDate } from "../date";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
index: number,
|
index: number,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const edit = ref(false);
|
||||||
|
|
||||||
const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
|
const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr class="{{transaction.Date.After now ? 'future' : ''}}"
|
<tr v-if="!edit" class="{{new Date(transaction.Date) > new Date() ? 'future' : ''}}"
|
||||||
:class="[index % 6 < 3 ? 'bg-gray-300' : 'bg-gray-100']">
|
:class="[index % 6 < 3 ? 'bg-gray-300' : 'bg-gray-100']">
|
||||||
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
|
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
|
||||||
<td style="width: 90px;">{{ transaction.Date.substring(0, 10) }}</td>
|
<td>{{ formatDate(transaction.Date) }}</td>
|
||||||
<td style="max-width: 150px;">{{ transaction.TransferAccount ? "Transfer : " + transaction.TransferAccount : transaction.Payee }}</td>
|
<td>{{ transaction.TransferAccount ? "Transfer : " + transaction.TransferAccount : transaction.Payee }}</td>
|
||||||
<td style="max-width: 200px;">
|
<td>
|
||||||
{{ transaction.CategoryGroup ? transaction.CategoryGroup + " : " + transaction.Category : "" }}
|
{{ transaction.CategoryGroup ? transaction.CategoryGroup + " : " + transaction.Category : "" }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -29,11 +33,12 @@ const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
|
|||||||
<td>
|
<td>
|
||||||
<Currency class="block" :value="transaction.Amount" />
|
<Currency class="block" :value="transaction.Amount" />
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 20px;">
|
<td>
|
||||||
{{ transaction.Status == "Reconciled" ? "✔" : (transaction.Status == "Uncleared" ? "" : "*") }}
|
{{ transaction.Status == "Reconciled" ? "✔" : (transaction.Status == "Uncleared" ? "" : "*") }}
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 20px;">{{ transaction.GroupID ? "☀" : "" }}</td>
|
<td class="text-right">{{ transaction.GroupID ? "☀" : "" }}<a @click="edit = true;">✎</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<TransactionEditRow v-if="edit" :transactionid="transaction.ID" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
7
web/src/date.ts
Normal file
7
web/src/date.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function formatDate(date: Date): string {
|
||||||
|
return date.toLocaleDateString(undefined, { // you can use undefined as first argument
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
44
web/src/dialogs/EditAccount.vue
Normal file
44
web/src/dialogs/EditAccount.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import Modal from '../components/Modal.vue';
|
||||||
|
import { useAccountStore } from '../stores/budget-account';
|
||||||
|
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const CurrentAccount = computed(() => accountStore.CurrentAccount);
|
||||||
|
|
||||||
|
const accountName = ref("");
|
||||||
|
const accountOnBudget = ref(true);
|
||||||
|
|
||||||
|
function editAccount(e : any) {
|
||||||
|
accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditAccount(e : any) {
|
||||||
|
accountName.value = CurrentAccount.value?.Name ?? "";
|
||||||
|
accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
|
||||||
|
<template v-slot:placeholder>✎</template>
|
||||||
|
<div class="mt-2 px-7 py-3">
|
||||||
|
<input
|
||||||
|
class="border-2"
|
||||||
|
type="text"
|
||||||
|
v-model="accountName"
|
||||||
|
placeholder="Account name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 px-7 py-3">
|
||||||
|
<input
|
||||||
|
class="border-2"
|
||||||
|
type="checkbox"
|
||||||
|
v-model="accountOnBudget"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<label>On Budget</label>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
@ -4,7 +4,7 @@ import Currency from "../components/Currency.vue";
|
|||||||
import TransactionRow from "../components/TransactionRow.vue";
|
import TransactionRow from "../components/TransactionRow.vue";
|
||||||
import TransactionInputRow from "../components/TransactionInputRow.vue";
|
import TransactionInputRow from "../components/TransactionInputRow.vue";
|
||||||
import { useAccountStore } from "../stores/budget-account";
|
import { useAccountStore } from "../stores/budget-account";
|
||||||
import Modal from "../components/Modal.vue";
|
import EditAccount from "../dialogs/EditAccount.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
budgetid: string
|
budgetid: string
|
||||||
@ -15,42 +15,11 @@ const accountStore = useAccountStore();
|
|||||||
const CurrentAccount = computed(() => accountStore.CurrentAccount);
|
const CurrentAccount = computed(() => accountStore.CurrentAccount);
|
||||||
const TransactionsList = computed(() => accountStore.TransactionsList);
|
const TransactionsList = computed(() => accountStore.TransactionsList);
|
||||||
|
|
||||||
const accountName = ref("");
|
|
||||||
const accountOnBudget = ref(true);
|
|
||||||
|
|
||||||
function editAccount(e : any) {
|
|
||||||
accountStore.EditAccount(CurrentAccount.value?.ID ?? "", accountName.value, accountOnBudget.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditAccount(e : any) {
|
|
||||||
accountName.value = CurrentAccount.value?.Name ?? "";
|
|
||||||
accountOnBudget.value = CurrentAccount.value?.OnBudget ?? true;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1 class="inline">{{ CurrentAccount?.Name }}</h1>
|
<h1 class="inline">{{ CurrentAccount?.Name }}</h1>
|
||||||
<Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
|
<EditAccount />
|
||||||
<template v-slot:placeholder>✎</template>
|
|
||||||
<div class="mt-2 px-7 py-3">
|
|
||||||
<input
|
|
||||||
class="border-2"
|
|
||||||
type="text"
|
|
||||||
v-model="accountName"
|
|
||||||
placeholder="Account name"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 px-7 py-3">
|
|
||||||
<input
|
|
||||||
class="border-2"
|
|
||||||
type="checkbox"
|
|
||||||
v-model="accountOnBudget"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<label>On Budget</label>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Current Balance:
|
Current Balance:
|
||||||
@ -64,7 +33,7 @@ function openEditAccount(e : any) {
|
|||||||
<td>Memo</td>
|
<td>Memo</td>
|
||||||
<td class="text-right">Amount</td>
|
<td class="text-right">Amount</td>
|
||||||
<td style="width: 20px;"></td>
|
<td style="width: 20px;"></td>
|
||||||
<td style="width: 20px;"></td>
|
<td style="width: 40px;"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
|
<TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
|
||||||
<TransactionRow
|
<TransactionRow
|
||||||
|
@ -8,20 +8,22 @@ interface State {
|
|||||||
CurrentAccountID: string | null,
|
CurrentAccountID: string | null,
|
||||||
Categories: Map<string, Category>,
|
Categories: Map<string, Category>,
|
||||||
Months: Map<number, Map<number, Map<string, Category>>>,
|
Months: Map<number, Map<number, Map<string, Category>>>,
|
||||||
Transactions: any[],
|
Transactions: Map<string, Transaction>,
|
||||||
Assignments: []
|
Assignments: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
ID: string,
|
ID: string,
|
||||||
Date: string,
|
Date: Date,
|
||||||
TransferAccount: string,
|
TransferAccount: string,
|
||||||
CategoryGroup: string,
|
CategoryGroup: string,
|
||||||
Category: string,
|
Category: string,
|
||||||
|
CategoryID: string | undefined,
|
||||||
Memo: string,
|
Memo: string,
|
||||||
Status: string,
|
Status: string,
|
||||||
GroupID: string,
|
GroupID: string,
|
||||||
Payee: string,
|
Payee: string,
|
||||||
|
PayeeID: string | undefined,
|
||||||
Amount: number,
|
Amount: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ export interface Account {
|
|||||||
Name: string
|
Name: string
|
||||||
OnBudget: boolean
|
OnBudget: boolean
|
||||||
Balance: number
|
Balance: number
|
||||||
|
Transactions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
@ -48,7 +51,7 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
CurrentAccountID: null,
|
CurrentAccountID: null,
|
||||||
Months: new Map<number, Map<number, Map<string, Category>>>(),
|
Months: new Map<number, Map<number, Map<string, Category>>>(),
|
||||||
Categories: new Map<string, Category>(),
|
Categories: new Map<string, Category>(),
|
||||||
Transactions: [],
|
Transactions: new Map<string, Transaction>(),
|
||||||
Assignments: []
|
Assignments: []
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
@ -100,8 +103,10 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
OffBudgetAccountsBalance(state): number {
|
OffBudgetAccountsBalance(state): number {
|
||||||
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
|
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
|
||||||
},
|
},
|
||||||
TransactionsList(state) {
|
TransactionsList(state) : Transaction[] {
|
||||||
return (state.Transactions || []);
|
return this.CurrentAccount!.Transactions.map(x => {
|
||||||
|
return this.Transactions.get(x)!
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -110,16 +115,22 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
return
|
return
|
||||||
|
|
||||||
this.CurrentAccountID = accountid;
|
this.CurrentAccountID = accountid;
|
||||||
if (this.CurrentAccount == undefined)
|
const account = this.CurrentAccount;
|
||||||
|
if (account == undefined)
|
||||||
return
|
return
|
||||||
|
|
||||||
useSessionStore().setTitle(this.CurrentAccount.Name);
|
useSessionStore().setTitle(account.Name);
|
||||||
await this.FetchAccount(accountid);
|
await this.FetchAccount(account);
|
||||||
},
|
},
|
||||||
async FetchAccount(accountid: string) {
|
async FetchAccount(account: Account) {
|
||||||
const result = await GET("/account/" + accountid + "/transactions");
|
const result = await GET("/account/" + account.ID + "/transactions");
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
this.Transactions = response.Transactions;
|
account.Transactions = [];
|
||||||
|
for (const transaction of response.Transactions) {
|
||||||
|
transaction.Date = new Date(transaction.Date);
|
||||||
|
this.Transactions.set(transaction.ID, transaction);
|
||||||
|
account.Transactions.push(transaction.ID);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
||||||
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
|
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
|
||||||
@ -151,7 +162,12 @@ export const useAccountStore = defineStore("budget/account", {
|
|||||||
async saveTransaction(payload: string) {
|
async saveTransaction(payload: string) {
|
||||||
const result = await POST("/transaction/new", payload);
|
const result = await POST("/transaction/new", payload);
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
this.Transactions.unshift(response);
|
this.CurrentAccount?.Transactions.unshift(response);
|
||||||
|
},
|
||||||
|
async editTransaction(transactionid : string, payload: string) {
|
||||||
|
const result = await POST("/transaction/" + transactionid, payload);
|
||||||
|
const response = await result.json();
|
||||||
|
this.CurrentAccount?.Transactions.unshift(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user