Add abilty to filter transactions by account, payee and category #55

Merged
jacob1123 merged 12 commits from filtered-transactions into master 2022-08-21 21:32:50 +02:00
9 changed files with 224 additions and 14 deletions

View File

@ -62,3 +62,12 @@ WHERE transactions.category_id IS NULL
AND accounts.on_budget
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
AND accounts.budget_id = $1;
-- name: GetFilteredTransactions :many
SELECT transactions.*
FROM display_transactions AS transactions
WHERE (NOT @filter_category::boolean OR transactions.category_id = @category_id)
AND (NOT @filter_account::boolean OR transactions.account_id = @account_id)
AND (NOT @filter_payee::boolean OR transactions.payee_id = @payee_id)
AND transactions.date BETWEEN @from_date AND @to_date
AND transactions.budget_id = @budget_id;

View File

@ -117,6 +117,77 @@ func (q *Queries) GetAllTransactionsForBudget(ctx context.Context, budgetID uuid
return items, nil
}
const getFilteredTransactions = `-- name: GetFilteredTransactions :many
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, transactions.account, transactions.payee_id, transactions.category_id, transactions.payee, transactions.category_group, transactions.category, transactions.transfer_account, transactions.budget_id, transactions.account_id
FROM display_transactions AS transactions
WHERE (NOT $1::boolean OR transactions.category_id = $2)
AND (NOT $3::boolean OR transactions.account_id = $4)
AND (NOT $5::boolean OR transactions.payee_id = $6)
AND transactions.date BETWEEN $7 AND $8
AND transactions.budget_id = $9
`
type GetFilteredTransactionsParams struct {
FilterCategory bool
CategoryID uuid.NullUUID
FilterAccount bool
AccountID uuid.UUID
FilterPayee bool
PayeeID uuid.NullUUID
FromDate time.Time
ToDate time.Time
BudgetID uuid.UUID
}
func (q *Queries) GetFilteredTransactions(ctx context.Context, arg GetFilteredTransactionsParams) ([]DisplayTransaction, error) {
rows, err := q.db.QueryContext(ctx, getFilteredTransactions,
arg.FilterCategory,
arg.CategoryID,
arg.FilterAccount,
arg.AccountID,
arg.FilterPayee,
arg.PayeeID,
arg.FromDate,
arg.ToDate,
arg.BudgetID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DisplayTransaction
for rows.Next() {
var i DisplayTransaction
if err := rows.Scan(
&i.ID,
&i.Date,
&i.Memo,
&i.Amount,
&i.GroupID,
&i.Status,
&i.Account,
&i.PayeeID,
&i.CategoryID,
&i.Payee,
&i.CategoryGroup,
&i.Category,
&i.TransferAccount,
&i.BudgetID,
&i.AccountID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProblematicTransactions = `-- name: GetProblematicTransactions :many
SELECT transactions.id, transactions.date, transactions.memo, transactions.amount, transactions.group_id, transactions.status, transactions.account, transactions.payee_id, transactions.category_id, transactions.payee, transactions.category_group, transactions.category, transactions.transfer_account, transactions.budget_id, transactions.account_id
FROM display_transactions AS transactions

View File

@ -2,12 +2,68 @@ package server
import (
"net/http"
"time"
"git.javil.eu/jacob1123/budgeteer/postgres"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type FilterTransactionsRequest struct {
CategoryID string `json:"category_id"`
PayeeID string `json:"payee_id"`
AccountID string `json:"account_id"`
FromDate time.Time `json:"from_date"`
ToDate time.Time `json:"to_date"`
}
func (h *Handler) filteredTransactions(c *gin.Context) {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
var request FilterTransactionsRequest
err = c.BindJSON(&request)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
params := postgres.GetFilteredTransactionsParams{
BudgetID: budgetUUID,
FromDate: request.FromDate,
ToDate: request.ToDate,
}
params.CategoryID, params.FilterCategory = parseEmptyUUID(request.CategoryID)
accountID, filterAccount := parseEmptyUUID(request.AccountID)
params.AccountID, params.FilterAccount = accountID.UUID, filterAccount
params.PayeeID, params.FilterPayee = parseEmptyUUID(request.PayeeID)
transactions, err := h.Service.GetFilteredTransactions(c.Request.Context(), params)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, TransactionsResponse{nil, transactions})
}
func parseEmptyUUID(value string) (uuid.NullUUID, bool) {
if value == "" {
return uuid.NullUUID{}, false
}
val, err := uuid.Parse(value)
if err != nil {
return uuid.NullUUID{}, false
}
return uuid.NullUUID{val, true}, true
}
func (h *Handler) problematicTransactions(c *gin.Context) {
budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID)

View File

@ -68,6 +68,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
budget.GET("/:budgetid/autocomplete/accounts", h.autocompleteAccounts)
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions)
budget.POST("/:budgetid/filtered-transactions", h.filteredTransactions)
budget.DELETE("/:budgetid", h.deleteBudget)
budget.POST("/:budgetid/import/ynab", h.importYNAB)
budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions)

View File

@ -15,7 +15,6 @@ function valueChanged(e: Event) {
emits('update:modelValue', target.valueAsNumber);
break;
default:
console.log("STR-INPUT", props.type)
emits('update:modelValue', target.value)
break;
}

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, ref, onMounted } from "vue"
import { computed, ref, onMounted, watch } from "vue"
import Currency from "../components/Currency.vue";
import TransactionRow from "../components/TransactionRow.vue";
import TransactionInputRow from "../components/TransactionInputRow.vue";
@ -11,6 +11,8 @@ import Modal from "../components/Modal.vue";
import Input from "../components/Input.vue";
import Checkbox from "../components/Checkbox.vue";
import { formatDate } from "../date";
import DateInput from "../components/DateInput.vue";
import Autocomplete from '../components/Autocomplete.vue'
defineProps<{
budgetid: string
@ -28,17 +30,71 @@ const transactions = useTransactionsStore();
onMounted(() => {
transactions.GetProblematicTransactions();
})
const filters = ref({
Account: null,
AccountID: null,
Payee: null,
PayeeID: null,
Category: null,
CategoryID: null,
FromDate: new Date(1900, 0, 2),
ToDate: new Date(2999,11,32),
});
watch(() => filters.value.AccountID
+ filters.value.PayeeID
+ filters.value.CategoryID
+ filters.value.FromDate?.toISOString()
+ filters.value.ToDate?.toISOString(), function() {
if(!hasFilters.value)
return;
transactions.GetFilteredTransactions(
filters.value.AccountID,
filters.value.CategoryID,
filters.value.PayeeID,
filters.value.FromDate?.toISOString(),
filters.value.ToDate?.toISOString(),
);
})
const hasFilters = computed(() =>
filters.value.AccountID != null
|| filters.value.PayeeID != null
|| filters.value.CategoryID != null
|| (filters.value.FromDate != null && filters.value.FromDate.getFullYear() > 1901)
|| (filters.value.ToDate != null && filters.value.ToDate.getFullYear() < 2998))
const transactionsList = computed(() => {
if (hasFilters.value){
return transactions.FilteredTransactionsList;
}
return transactions.ProblematicTransactionsList;
})
</script>
<template>
<div class="grid grid-cols-[1fr_auto]">
<div>
<h1 class="inline">
Problematic Transactions
{{ hasFilters ? "Filtered" : "Problematic" }} Transactions
</h1>
</div>
</div>
<div class="grid grid-cols-2 w-96">
Account:
<Autocomplete v-model:text="filters.Account" v-model:id="filters.AccountID" model="accounts" class="inline-block" /><br>
Payee:
<Autocomplete v-model:text="filters.Payee" v-model:id="filters.PayeeID" model="payees" class="inline-block" /><br>
Category:
<Autocomplete v-model:text="filters.Category" v-model:id="filters.CategoryID" model="categories" class="inline-block" /><br>
From Date:
<DateInput v-model="filters.FromDate" class="inline-block" /><br>
To Date:
<DateInput v-model="filters.ToDate" class="inline-block" />
</div>
<table>
<tr class="font-bold">
<td class="hidden md:block" style="width: 90px;">Date</td>
@ -49,7 +105,7 @@ onMounted(() => {
<td class="text-right">Amount</td>
</tr>
<TransactionRow
v-for="(transaction, index) in transactions.ProblematicTransactionsList"
v-for="(transaction, index) in transactionsList"
:key="transaction.ID"
:with-account="true"
:transactionid="transaction.ID"

View File

@ -40,7 +40,7 @@ function toggleMenu() {
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
<br>
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
<router-link :to="'/budget/'+CurrentBudgetID+'/problematic-transactions'">Problematic Transactions</router-link>
<router-link :to="'/budget/'+CurrentBudgetID+'/problematic-transactions'">Transactions</router-link>
</span>
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold">

View File

@ -3,10 +3,6 @@ import NewBudget from '../dialogs/NewBudget.vue';
import RowCard from '../components/RowCard.vue';
import { useSessionStore } from '../stores/session';
const props = defineProps<{
budgetid: string,
}>();
const BudgetsList = useSessionStore().BudgetsList;
</script>
@ -24,7 +20,7 @@ const BudgetsList = useSessionStore().BudgetsList;
<!--<svg class="w-24"></svg>-->
<p class="w-24 text-center text-6xl" />
<span class="text-lg">{{ budget.Name
}}{{ budget.ID == budgetid ? " *" : "" }}</span>
}}</span>
</router-link>
</RowCard>
<NewBudget />

View File

@ -8,6 +8,7 @@ interface State {
Transactions: Map<string, Transaction>;
Reconciling: boolean;
ProblematicTransactions: Array<string>;
FilteredTransactions: Array<string>;
}
export interface Transaction {
@ -33,6 +34,7 @@ export const useTransactionsStore = defineStore("budget/transactions", {
Transactions: new Map<string, Transaction>(),
Reconciling: false,
ProblematicTransactions: new Array<string>(),
FilteredTransactions: new Array<string>(),
}),
getters: {
ReconcilingBalance(state): number {
@ -47,7 +49,8 @@ export const useTransactionsStore = defineStore("budget/transactions", {
},
TransactionsByDate(state) : Record<string, Transaction[]> {
const accountsStore = useAccountStore();
const allTransactions = [...this.Transactions.values()];
const accountID = accountsStore.CurrentAccountID;
const allTransactions = [...this.Transactions.values()].filter(x => x.AccountID == accountID);
return groupBy(allTransactions, x => formatDate(x.Date));
},
TransactionsList(state) : Transaction[] {
@ -58,6 +61,9 @@ export const useTransactionsStore = defineStore("budget/transactions", {
},
ProblematicTransactionsList(state) : Transaction[] {
return [...state.ProblematicTransactions.map(x => state.Transactions!.get(x)!)];
},
FilteredTransactionsList(state) : Transaction[] {
return [...state.FilteredTransactions.map(x => state.Transactions!.get(x)!)];
}
},
actions: {
@ -80,8 +86,24 @@ export const useTransactionsStore = defineStore("budget/transactions", {
const budgetStore = useBudgetsStore();
const result = await GET("/budget/" + budgetStore.CurrentBudgetID + "/problematic-transactions");
const response = await result.json();
this.AddTransactions(response.Transactions);
this.ProblematicTransactions = [...response.Transactions.map((x : Transaction) => x.ID)];
const transactions = response.Transactions ?? [];
this.AddTransactions(transactions);
this.ProblematicTransactions = [...transactions?.map((x : Transaction) => x.ID)];
},
async GetFilteredTransactions(accountID : string | null, categoryID : string | null, payeeID : string | null, fromDate : string, toDate : string) {
const budgetStore = useBudgetsStore();
const payload = JSON.stringify({
category_id: categoryID,
payee_id: payeeID,
account_id: accountID,
from_date: fromDate,
to_date: toDate,
});
const result = await POST("/budget/" + budgetStore.CurrentBudgetID + "/filtered-transactions", payload);
const response = await result.json();
const transactions = response.Transactions ?? [];
this.AddTransactions(transactions);
this.FilteredTransactions = [...transactions.map((x : Transaction) => x.ID)];
},
async SubmitReconcilation(reconciliationTransactionAmount: number) {
const accountsStore = useAccountStore();