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 was merged in pull request #55.
	This commit is contained in:
		| @@ -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(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user