Implement editing of transactions #23
@ -130,7 +130,7 @@ func (q *Queries) GetAccountsWithBalance(ctx context.Context, budgetID uuid.UUID
|
||||
}
|
||||
|
||||
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
|
||||
AND accounts.name LIKE $2
|
||||
ORDER BY accounts.name
|
||||
@ -145,7 +145,7 @@ type SearchAccountsRow struct {
|
||||
ID uuid.UUID
|
||||
BudgetID uuid.UUID
|
||||
Name string
|
||||
IsAccount bool
|
||||
Type interface{}
|
||||
}
|
||||
|
||||
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.BudgetID,
|
||||
&i.Name,
|
||||
&i.IsAccount,
|
||||
&i.Type,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -118,7 +118,8 @@ func (q *Queries) GetCategoryGroups(ctx context.Context, budgetID uuid.UUID) ([]
|
||||
}
|
||||
|
||||
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
|
||||
WHERE category_groups.budget_id = $1
|
||||
AND categories.name LIKE $2
|
||||
@ -133,6 +134,7 @@ type SearchCategoriesParams struct {
|
||||
type SearchCategoriesRow struct {
|
||||
Name interface{}
|
||||
ID uuid.UUID
|
||||
Type interface{}
|
||||
}
|
||||
|
||||
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
|
||||
for rows.Next() {
|
||||
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
|
||||
}
|
||||
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
|
||||
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
|
||||
AND payees.name LIKE $2
|
||||
ORDER BY payees.name
|
||||
@ -69,16 +69,28 @@ type SearchPayeesParams struct {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Payee
|
||||
var items []SearchPayeesRow
|
||||
for rows.Next() {
|
||||
var i Payee
|
||||
if err := rows.Scan(&i.ID, &i.BudgetID, &i.Name); err != nil {
|
||||
var i SearchPayeesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.BudgetID,
|
||||
&i.Name,
|
||||
&i.Type,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
|
@ -22,7 +22,7 @@ GROUP BY accounts.id, accounts.name
|
||||
ORDER BY accounts.name;
|
||||
|
||||
-- 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
|
||||
AND accounts.name LIKE @search
|
||||
ORDER BY accounts.name;
|
||||
|
@ -21,7 +21,8 @@ WHERE category_groups.budget_id = $1
|
||||
ORDER BY category_groups.name, categories.name;
|
||||
|
||||
-- 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
|
||||
WHERE category_groups.budget_id = @budget_id
|
||||
AND categories.name LIKE @search
|
||||
|
@ -10,7 +10,7 @@ WHERE payees.budget_id = $1
|
||||
ORDER BY name;
|
||||
|
||||
-- name: SearchPayees :many
|
||||
SELECT payees.* FROM payees
|
||||
SELECT payees.*, 'payee' as type FROM payees
|
||||
WHERE payees.budget_id = @budget_id
|
||||
AND payees.name LIKE @search
|
||||
ORDER BY payees.name;
|
||||
|
@ -13,10 +13,9 @@ UPDATE transactions
|
||||
SET date = $1,
|
||||
memo = $2,
|
||||
amount = $3,
|
||||
account_id = $4,
|
||||
payee_id = $5,
|
||||
category_id = $6
|
||||
WHERE id = $7;
|
||||
payee_id = $4,
|
||||
category_id = $5
|
||||
WHERE id = $6;
|
||||
|
||||
-- name: DeleteTransaction :exec
|
||||
DELETE FROM transactions
|
||||
@ -25,7 +24,7 @@ WHERE id = $1;
|
||||
-- name: GetAllTransactionsForBudget :many
|
||||
SELECT transactions.id, transactions.date, transactions.memo,
|
||||
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(category_groups.name, '') as category_group,
|
||||
COALESCE(categories.name, '') as category,
|
||||
@ -47,7 +46,7 @@ ORDER BY transactions.date DESC;
|
||||
-- name: GetTransactionsForAccount :many
|
||||
SELECT transactions.id, transactions.date, transactions.memo,
|
||||
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(category_groups.name, '') as category_group,
|
||||
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
|
||||
SELECT transactions.id, transactions.date, transactions.memo,
|
||||
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(category_groups.name, '') as category_group,
|
||||
COALESCE(categories.name, '') as category,
|
||||
@ -111,6 +111,8 @@ type GetAllTransactionsForBudgetRow struct {
|
||||
GroupID uuid.NullUUID
|
||||
Status TransactionStatus
|
||||
Account string
|
||||
PayeeID uuid.NullUUID
|
||||
CategoryID uuid.NullUUID
|
||||
Payee string
|
||||
CategoryGroup string
|
||||
Category string
|
||||
@ -134,6 +136,8 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid
|
||||
&i.GroupID,
|
||||
&i.Status,
|
||||
&i.Account,
|
||||
&i.PayeeID,
|
||||
&i.CategoryID,
|
||||
&i.Payee,
|
||||
&i.CategoryGroup,
|
||||
&i.Category,
|
||||
@ -211,7 +215,7 @@ func (q *Queries) GetTransactionsByMonthAndCategory(ctx context.Context, budgetI
|
||||
const getTransactionsForAccount = `-- name: GetTransactionsForAccount :many
|
||||
SELECT transactions.id, transactions.date, transactions.memo,
|
||||
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(category_groups.name, '') as category_group,
|
||||
COALESCE(categories.name, '') as category,
|
||||
@ -240,6 +244,8 @@ type GetTransactionsForAccountRow struct {
|
||||
GroupID uuid.NullUUID
|
||||
Status TransactionStatus
|
||||
Account string
|
||||
PayeeID uuid.NullUUID
|
||||
CategoryID uuid.NullUUID
|
||||
Payee string
|
||||
CategoryGroup string
|
||||
Category string
|
||||
@ -263,6 +269,8 @@ func (q *Queries) GetTransactionsForAccount(ctx context.Context, accountID uuid.
|
||||
&i.GroupID,
|
||||
&i.Status,
|
||||
&i.Account,
|
||||
&i.PayeeID,
|
||||
&i.CategoryID,
|
||||
&i.Payee,
|
||||
&i.CategoryGroup,
|
||||
&i.Category,
|
||||
@ -286,17 +294,15 @@ UPDATE transactions
|
||||
SET date = $1,
|
||||
memo = $2,
|
||||
amount = $3,
|
||||
account_id = $4,
|
||||
payee_id = $5,
|
||||
category_id = $6
|
||||
WHERE id = $7
|
||||
payee_id = $4,
|
||||
category_id = $5
|
||||
WHERE id = $6
|
||||
`
|
||||
|
||||
type UpdateTransactionParams struct {
|
||||
Date time.Time
|
||||
Memo string
|
||||
Amount numeric.Numeric
|
||||
AccountID uuid.UUID
|
||||
PayeeID uuid.NullUUID
|
||||
CategoryID uuid.NullUUID
|
||||
ID uuid.UUID
|
||||
@ -307,7 +313,6 @@ func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionPa
|
||||
arg.Date,
|
||||
arg.Memo,
|
||||
arg.Amount,
|
||||
arg.AccountID,
|
||||
arg.PayeeID,
|
||||
arg.CategoryID,
|
||||
arg.ID,
|
||||
|
@ -17,12 +17,9 @@ type NewTransactionPayload struct {
|
||||
Payee struct {
|
||||
ID uuid.NullUUID
|
||||
Name string
|
||||
IsAccount bool
|
||||
Type string
|
||||
} `json:"payee"`
|
||||
Category struct {
|
||||
ID uuid.NullUUID
|
||||
Name string
|
||||
} `json:"category"`
|
||||
CategoryID uuid.NullUUID `json:"categoryId"`
|
||||
Memo string `json:"memo"`
|
||||
Amount string `json:"amount"`
|
||||
BudgetID uuid.UUID `json:"budgetId"`
|
||||
@ -44,26 +41,27 @@ func (h *Handler) newTransaction(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
transactionID := c.Param("transactionid")
|
||||
if transactionID != "" {
|
||||
h.UpdateTransaction(payload, amount, transactionID, c)
|
||||
return
|
||||
}
|
||||
|
||||
newTransaction := postgres.CreateTransactionParams{
|
||||
Memo: payload.Memo,
|
||||
Date: time.Time(payload.Date),
|
||||
Amount: amount,
|
||||
Status: postgres.TransactionStatus(payload.State),
|
||||
CategoryID: payload.CategoryID,
|
||||
AccountID: payload.AccountID,
|
||||
}
|
||||
|
||||
if payload.Payee.IsAccount {
|
||||
newTransaction.GroupID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
|
||||
newTransaction.Amount = amount.Neg()
|
||||
newTransaction.AccountID = payload.Payee.ID.UUID
|
||||
newTransaction.CategoryID = uuid.NullUUID{}
|
||||
|
||||
_, err = h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
||||
if payload.Payee.Type == "account" {
|
||||
err := h.CreateTransferForOtherAccount(newTransaction, amount, payload, c)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transfer transaction: %w", err))
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
newTransaction.Amount = amount
|
||||
} else {
|
||||
payeeID, err := GetPayeeID(c.Request.Context(), payload, h)
|
||||
if err != nil {
|
||||
@ -72,17 +70,54 @@ func (h *Handler) newTransaction(c *gin.Context) {
|
||||
newTransaction.PayeeID = payeeID
|
||||
}
|
||||
|
||||
newTransaction.CategoryID = payload.Category.ID
|
||||
newTransaction.AccountID = payload.AccountID
|
||||
transaction, err := h.Service.CreateTransaction(c.Request.Context(), newTransaction)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("create transaction: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
payeeID := payload.Payee.ID
|
||||
if payeeID.Valid {
|
||||
|
@ -1,43 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, PropType, ref, watch } from "vue"
|
||||
import { ref, watch } from "vue"
|
||||
import { GET } from "../api";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
|
||||
export interface Suggestion {
|
||||
ID: string
|
||||
Name: string
|
||||
}
|
||||
|
||||
interface Data {
|
||||
Selected: Suggestion | undefined
|
||||
SearchQuery: String
|
||||
Suggestions: Suggestion[]
|
||||
Type: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Suggestion | undefined,
|
||||
type: String
|
||||
text: String,
|
||||
id: String | undefined,
|
||||
model: String,
|
||||
type?: string | undefined,
|
||||
}>();
|
||||
|
||||
const Selected = ref<Suggestion | undefined>(props.modelValue || undefined);
|
||||
const SearchQuery = ref(props.modelValue?.Name || "");
|
||||
const SearchQuery = ref(props.text || "");
|
||||
const Suggestions = ref<Array<Suggestion>>([]);
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const emit = defineEmits(["update:id", "update:text", "update:type"]);
|
||||
watch(SearchQuery, () => {
|
||||
load(SearchQuery.value);
|
||||
});
|
||||
function saveTransaction(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
};
|
||||
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 == "") {
|
||||
Suggestions.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
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 => {
|
||||
let suggestions = x || [];
|
||||
@ -56,13 +51,13 @@ function keypress(e: KeyboardEvent) {
|
||||
const currentIndex = inputElements.indexOf(el);
|
||||
const nextElement = inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0];
|
||||
(<HTMLInputElement>nextElement).focus();
|
||||
|
||||
}
|
||||
};
|
||||
function selectElement(element: Suggestion) {
|
||||
Selected.value = element;
|
||||
emit('update:id', element.ID);
|
||||
emit('update:text', element.Name);
|
||||
emit('update:type', element.Type);
|
||||
Suggestions.value = [];
|
||||
emit('update:modelValue', element);
|
||||
};
|
||||
function select(e: MouseEvent) {
|
||||
const target = (<HTMLInputElement>e.target);
|
||||
@ -74,8 +69,9 @@ function select(e: MouseEvent) {
|
||||
selectElement(selected);
|
||||
};
|
||||
function clear() {
|
||||
Selected.value = undefined;
|
||||
emit('update:modelValue', { ID: null, Name: SearchQuery.value });
|
||||
emit('update:id', null);
|
||||
emit('update:text', SearchQuery.value);
|
||||
emit('update:type', undefined);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -84,10 +80,10 @@ function clear() {
|
||||
<input
|
||||
class="border-b-2 border-black"
|
||||
@keypress="keypress"
|
||||
v-if="Selected == undefined"
|
||||
v-if="id == undefined"
|
||||
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">
|
||||
<span
|
||||
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>
|
||||
import { computed, ref } from "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<{
|
||||
budgetid: string
|
||||
accountid: string
|
||||
}>()
|
||||
|
||||
const TransactionDate = ref(new Date().toISOString().substring(0, 10));
|
||||
const Payee = ref<Suggestion | undefined>(undefined);
|
||||
const Category = ref<Suggestion | undefined>(undefined);
|
||||
const Memo = ref("");
|
||||
const Amount = ref("0");
|
||||
const TX = ref<Transaction>({
|
||||
Date: new Date(),
|
||||
Memo: "",
|
||||
Amount: 0,
|
||||
Payee: "",
|
||||
PayeeID: undefined,
|
||||
Category: "",
|
||||
CategoryID: undefined,
|
||||
CategoryGroup: "",
|
||||
GroupID: "",
|
||||
ID: "",
|
||||
Status: "Uncleared",
|
||||
TransferAccount: "",
|
||||
});
|
||||
|
||||
const payeeType = ref<string|undefined>(undefined);
|
||||
|
||||
const payload = computed(() => JSON.stringify({
|
||||
budgetId: props.budgetid,
|
||||
accountId: props.accountid,
|
||||
date: TransactionDate.value,
|
||||
payee: Payee.value,
|
||||
category: Category.value,
|
||||
memo: Memo.value,
|
||||
amount: Amount.value,
|
||||
date: TX.value.Date.toISOString().split("T")[0],
|
||||
payee: {
|
||||
Name: TX.value.Payee,
|
||||
ID: TX.value.PayeeID,
|
||||
Type: payeeType.value,
|
||||
},
|
||||
categoryId: TX.value.CategoryID,
|
||||
memo: TX.value.Memo,
|
||||
amount: TX.value.Amount.toString(),
|
||||
state: "Uncleared"
|
||||
}));
|
||||
|
||||
@ -35,22 +51,22 @@ function saveTransaction(e: MouseEvent) {
|
||||
<template>
|
||||
<tr>
|
||||
<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 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 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>
|
||||
<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 style="width: 80px;" class="text-right">
|
||||
<input
|
||||
class="text-right block w-full border-b-2 border-black"
|
||||
type="currency"
|
||||
v-model="Amount"
|
||||
v-model="TX.Amount"
|
||||
/>
|
||||
</td>
|
||||
<td style="width: 20px;">
|
||||
|
@ -1,24 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
import { Transaction } from "../stores/budget-account";
|
||||
import Currency from "./Currency.vue";
|
||||
import TransactionEditRow from "./TransactionEditRow.vue";
|
||||
import { formatDate } from "../date";
|
||||
|
||||
const props = defineProps<{
|
||||
transaction: Transaction,
|
||||
index: number,
|
||||
}>();
|
||||
|
||||
const edit = ref(false);
|
||||
|
||||
const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
|
||||
</script>
|
||||
|
||||
<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 ? 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 style="max-width: 150px;">{{ transaction.TransferAccount ? "Transfer : " + transaction.TransferAccount : transaction.Payee }}</td>
|
||||
<td style="max-width: 200px;">
|
||||
<td>{{ formatDate(transaction.Date) }}</td>
|
||||
<td>{{ transaction.TransferAccount ? "Transfer : " + transaction.TransferAccount : transaction.Payee }}</td>
|
||||
<td>
|
||||
{{ transaction.CategoryGroup ? transaction.CategoryGroup + " : " + transaction.Category : "" }}
|
||||
</td>
|
||||
<td>
|
||||
@ -29,11 +33,12 @@ const CurrentBudgetID = computed(()=> useBudgetsStore().CurrentBudgetID);
|
||||
<td>
|
||||
<Currency class="block" :value="transaction.Amount" />
|
||||
</td>
|
||||
<td style="width: 20px;">
|
||||
<td>
|
||||
{{ transaction.Status == "Reconciled" ? "✔" : (transaction.Status == "Uncleared" ? "" : "*") }}
|
||||
</td>
|
||||
<td style="width: 20px;">{{ transaction.GroupID ? "☀" : "" }}</td>
|
||||
<td class="text-right">{{ transaction.GroupID ? "☀" : "" }}<a @click="edit = true;">✎</a></td>
|
||||
</tr>
|
||||
<TransactionEditRow v-if="edit" :transactionid="transaction.ID" />
|
||||
</template>
|
||||
|
||||
<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 TransactionInputRow from "../components/TransactionInputRow.vue";
|
||||
import { useAccountStore } from "../stores/budget-account";
|
||||
import Modal from "../components/Modal.vue";
|
||||
import EditAccount from "../dialogs/EditAccount.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
budgetid: string
|
||||
@ -15,42 +15,11 @@ const accountStore = useAccountStore();
|
||||
const CurrentAccount = computed(() => accountStore.CurrentAccount);
|
||||
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>
|
||||
|
||||
<template>
|
||||
<h1 class="inline">{{ CurrentAccount?.Name }}</h1>
|
||||
<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>
|
||||
<EditAccount />
|
||||
|
||||
<p>
|
||||
Current Balance:
|
||||
@ -64,7 +33,7 @@ function openEditAccount(e : any) {
|
||||
<td>Memo</td>
|
||||
<td class="text-right">Amount</td>
|
||||
<td style="width: 20px;"></td>
|
||||
<td style="width: 20px;"></td>
|
||||
<td style="width: 40px;"></td>
|
||||
</tr>
|
||||
<TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
|
||||
<TransactionRow
|
||||
|
@ -8,20 +8,22 @@ interface State {
|
||||
CurrentAccountID: string | null,
|
||||
Categories: Map<string, Category>,
|
||||
Months: Map<number, Map<number, Map<string, Category>>>,
|
||||
Transactions: any[],
|
||||
Transactions: Map<string, Transaction>,
|
||||
Assignments: []
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
ID: string,
|
||||
Date: string,
|
||||
Date: Date,
|
||||
TransferAccount: string,
|
||||
CategoryGroup: string,
|
||||
Category: string,
|
||||
CategoryID: string | undefined,
|
||||
Memo: string,
|
||||
Status: string,
|
||||
GroupID: string,
|
||||
Payee: string,
|
||||
PayeeID: string | undefined,
|
||||
Amount: number,
|
||||
}
|
||||
|
||||
@ -30,6 +32,7 @@ export interface Account {
|
||||
Name: string
|
||||
OnBudget: boolean
|
||||
Balance: number
|
||||
Transactions: string[]
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@ -48,7 +51,7 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
CurrentAccountID: null,
|
||||
Months: new Map<number, Map<number, Map<string, Category>>>(),
|
||||
Categories: new Map<string, Category>(),
|
||||
Transactions: [],
|
||||
Transactions: new Map<string, Transaction>(),
|
||||
Assignments: []
|
||||
}),
|
||||
getters: {
|
||||
@ -100,8 +103,10 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
OffBudgetAccountsBalance(state): number {
|
||||
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.Balance), 0);
|
||||
},
|
||||
TransactionsList(state) {
|
||||
return (state.Transactions || []);
|
||||
TransactionsList(state) : Transaction[] {
|
||||
return this.CurrentAccount!.Transactions.map(x => {
|
||||
return this.Transactions.get(x)!
|
||||
});
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@ -110,16 +115,22 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
return
|
||||
|
||||
this.CurrentAccountID = accountid;
|
||||
if (this.CurrentAccount == undefined)
|
||||
const account = this.CurrentAccount;
|
||||
if (account == undefined)
|
||||
return
|
||||
|
||||
useSessionStore().setTitle(this.CurrentAccount.Name);
|
||||
await this.FetchAccount(accountid);
|
||||
useSessionStore().setTitle(account.Name);
|
||||
await this.FetchAccount(account);
|
||||
},
|
||||
async FetchAccount(accountid: string) {
|
||||
const result = await GET("/account/" + accountid + "/transactions");
|
||||
async FetchAccount(account: Account) {
|
||||
const result = await GET("/account/" + account.ID + "/transactions");
|
||||
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) {
|
||||
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
|
||||
@ -151,7 +162,12 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
async saveTransaction(payload: string) {
|
||||
const result = await POST("/transaction/new", payload);
|
||||
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