Add abilty to filter transactions by account, payee and category #55
@ -62,3 +62,12 @@ WHERE transactions.category_id IS NULL
|
|||||||
AND accounts.on_budget
|
AND accounts.on_budget
|
||||||
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
|
AND (otherGroupAccount.id IS NULL OR NOT otherGroupAccount.on_budget)
|
||||||
AND accounts.budget_id = $1;
|
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
|
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
|
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
|
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
|
FROM display_transactions AS transactions
|
||||||
|
@ -2,12 +2,68 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"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) {
|
func (h *Handler) problematicTransactions(c *gin.Context) {
|
||||||
budgetID := c.Param("budgetid")
|
budgetID := c.Param("budgetid")
|
||||||
budgetUUID, err := uuid.Parse(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/accounts", h.autocompleteAccounts)
|
||||||
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
||||||
budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions)
|
budget.GET("/:budgetid/problematic-transactions", h.problematicTransactions)
|
||||||
|
budget.POST("/:budgetid/filtered-transactions", h.filteredTransactions)
|
||||||
budget.DELETE("/:budgetid", h.deleteBudget)
|
budget.DELETE("/:budgetid", h.deleteBudget)
|
||||||
budget.POST("/:budgetid/import/ynab", h.importYNAB)
|
budget.POST("/:budgetid/import/ynab", h.importYNAB)
|
||||||
budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions)
|
budget.POST("/:budgetid/export/ynab/transactions", h.exportYNABTransactions)
|
||||||
|
@ -15,7 +15,6 @@ function valueChanged(e: Event) {
|
|||||||
emits('update:modelValue', target.valueAsNumber);
|
emits('update:modelValue', target.valueAsNumber);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("STR-INPUT", props.type)
|
|
||||||
emits('update:modelValue', target.value)
|
emits('update:modelValue', target.value)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, onMounted } from "vue"
|
import { computed, ref, onMounted, watch } from "vue"
|
||||||
import Currency from "../components/Currency.vue";
|
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";
|
||||||
@ -11,6 +11,8 @@ import Modal from "../components/Modal.vue";
|
|||||||
import Input from "../components/Input.vue";
|
import Input from "../components/Input.vue";
|
||||||
import Checkbox from "../components/Checkbox.vue";
|
import Checkbox from "../components/Checkbox.vue";
|
||||||
import { formatDate } from "../date";
|
import { formatDate } from "../date";
|
||||||
|
import DateInput from "../components/DateInput.vue";
|
||||||
|
import Autocomplete from '../components/Autocomplete.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
budgetid: string
|
budgetid: string
|
||||||
@ -28,17 +30,71 @@ const transactions = useTransactionsStore();
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
transactions.GetProblematicTransactions();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-[1fr_auto]">
|
<div class="grid grid-cols-[1fr_auto]">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="inline">
|
<h1 class="inline">
|
||||||
Problematic Transactions
|
{{ hasFilters ? "Filtered" : "Problematic" }} Transactions
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<table>
|
||||||
<tr class="font-bold">
|
<tr class="font-bold">
|
||||||
<td class="hidden md:block" style="width: 90px;">Date</td>
|
<td class="hidden md:block" style="width: 90px;">Date</td>
|
||||||
@ -49,7 +105,7 @@ onMounted(() => {
|
|||||||
<td class="text-right">Amount</td>
|
<td class="text-right">Amount</td>
|
||||||
</tr>
|
</tr>
|
||||||
<TransactionRow
|
<TransactionRow
|
||||||
v-for="(transaction, index) in transactions.ProblematicTransactionsList"
|
v-for="(transaction, index) in transactionsList"
|
||||||
:key="transaction.ID"
|
:key="transaction.ID"
|
||||||
:with-account="true"
|
:with-account="true"
|
||||||
:transactionid="transaction.ID"
|
:transactionid="transaction.ID"
|
||||||
|
@ -40,7 +40,7 @@ function toggleMenu() {
|
|||||||
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
|
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
|
||||||
<br>
|
<br>
|
||||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
<!--<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>
|
</span>
|
||||||
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
||||||
<div class="flex flex-row justify-between font-bold">
|
<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 RowCard from '../components/RowCard.vue';
|
||||||
import { useSessionStore } from '../stores/session';
|
import { useSessionStore } from '../stores/session';
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
budgetid: string,
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const BudgetsList = useSessionStore().BudgetsList;
|
const BudgetsList = useSessionStore().BudgetsList;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -24,7 +20,7 @@ const BudgetsList = useSessionStore().BudgetsList;
|
|||||||
<!--<svg class="w-24"></svg>-->
|
<!--<svg class="w-24"></svg>-->
|
||||||
<p class="w-24 text-center text-6xl" />
|
<p class="w-24 text-center text-6xl" />
|
||||||
<span class="text-lg">{{ budget.Name
|
<span class="text-lg">{{ budget.Name
|
||||||
}}{{ budget.ID == budgetid ? " *" : "" }}</span>
|
}}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</RowCard>
|
</RowCard>
|
||||||
<NewBudget />
|
<NewBudget />
|
||||||
|
@ -8,6 +8,7 @@ interface State {
|
|||||||
Transactions: Map<string, Transaction>;
|
Transactions: Map<string, Transaction>;
|
||||||
Reconciling: boolean;
|
Reconciling: boolean;
|
||||||
ProblematicTransactions: Array<string>;
|
ProblematicTransactions: Array<string>;
|
||||||
|
FilteredTransactions: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
@ -33,6 +34,7 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
Transactions: new Map<string, Transaction>(),
|
Transactions: new Map<string, Transaction>(),
|
||||||
Reconciling: false,
|
Reconciling: false,
|
||||||
ProblematicTransactions: new Array<string>(),
|
ProblematicTransactions: new Array<string>(),
|
||||||
|
FilteredTransactions: new Array<string>(),
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
ReconcilingBalance(state): number {
|
ReconcilingBalance(state): number {
|
||||||
@ -47,7 +49,8 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
},
|
},
|
||||||
TransactionsByDate(state) : Record<string, Transaction[]> {
|
TransactionsByDate(state) : Record<string, Transaction[]> {
|
||||||
const accountsStore = useAccountStore();
|
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));
|
return groupBy(allTransactions, x => formatDate(x.Date));
|
||||||
},
|
},
|
||||||
TransactionsList(state) : Transaction[] {
|
TransactionsList(state) : Transaction[] {
|
||||||
@ -58,6 +61,9 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
},
|
},
|
||||||
ProblematicTransactionsList(state) : Transaction[] {
|
ProblematicTransactionsList(state) : Transaction[] {
|
||||||
return [...state.ProblematicTransactions.map(x => state.Transactions!.get(x)!)];
|
return [...state.ProblematicTransactions.map(x => state.Transactions!.get(x)!)];
|
||||||
|
},
|
||||||
|
FilteredTransactionsList(state) : Transaction[] {
|
||||||
|
return [...state.FilteredTransactions.map(x => state.Transactions!.get(x)!)];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -80,8 +86,24 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
|||||||
const budgetStore = useBudgetsStore();
|
const budgetStore = useBudgetsStore();
|
||||||
const result = await GET("/budget/" + budgetStore.CurrentBudgetID + "/problematic-transactions");
|
const result = await GET("/budget/" + budgetStore.CurrentBudgetID + "/problematic-transactions");
|
||||||
const response = await result.json();
|
const response = await result.json();
|
||||||
this.AddTransactions(response.Transactions);
|
const transactions = response.Transactions ?? [];
|
||||||
this.ProblematicTransactions = [...response.Transactions.map((x : Transaction) => x.ID)];
|
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) {
|
async SubmitReconcilation(reconciliationTransactionAmount: number) {
|
||||||
const accountsStore = useAccountStore();
|
const accountsStore = useAccountStore();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user