Merge pull request 'Add abilty to filter transactions by account, payee and category' (#55) from filtered-transactions into master
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #55
This commit is contained in:
commit
dd0f620b7a
@ -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;
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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 />
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user