Implement editing of Accounts #20
@@ -175,3 +175,29 @@ func (q *Queries) SearchAccounts(ctx context.Context, arg SearchAccountsParams)
 | 
			
		||||
	}
 | 
			
		||||
	return items, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const updateAccount = `-- name: UpdateAccount :one
 | 
			
		||||
UPDATE accounts
 | 
			
		||||
SET name = $1,
 | 
			
		||||
    on_budget = $2
 | 
			
		||||
WHERE accounts.id = $3
 | 
			
		||||
RETURNING id, budget_id, name, on_budget
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
type UpdateAccountParams struct {
 | 
			
		||||
	Name     string
 | 
			
		||||
	OnBudget bool
 | 
			
		||||
	ID       uuid.UUID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (q *Queries) UpdateAccount(ctx context.Context, arg UpdateAccountParams) (Account, error) {
 | 
			
		||||
	row := q.db.QueryRowContext(ctx, updateAccount, arg.Name, arg.OnBudget, arg.ID)
 | 
			
		||||
	var i Account
 | 
			
		||||
	err := row.Scan(
 | 
			
		||||
		&i.ID,
 | 
			
		||||
		&i.BudgetID,
 | 
			
		||||
		&i.Name,
 | 
			
		||||
		&i.OnBudget,
 | 
			
		||||
	)
 | 
			
		||||
	return i, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,4 +25,11 @@ ORDER BY accounts.name;
 | 
			
		||||
SELECT accounts.id, accounts.budget_id, accounts.name, true as is_account FROM accounts
 | 
			
		||||
WHERE accounts.budget_id = @budget_id
 | 
			
		||||
AND accounts.name LIKE @search
 | 
			
		||||
ORDER BY accounts.name;
 | 
			
		||||
ORDER BY accounts.name;
 | 
			
		||||
 | 
			
		||||
-- name: UpdateAccount :one
 | 
			
		||||
UPDATE accounts
 | 
			
		||||
SET name = $1,
 | 
			
		||||
    on_budget = $2
 | 
			
		||||
WHERE accounts.id = $3
 | 
			
		||||
RETURNING *;
 | 
			
		||||
@@ -35,3 +35,37 @@ type TransactionsResponse struct {
 | 
			
		||||
	Account      postgres.Account
 | 
			
		||||
	Transactions []postgres.GetTransactionsForAccountRow
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EditAccountRequest struct {
 | 
			
		||||
	Name     string `json:"name"`
 | 
			
		||||
	OnBudget bool   `json:"onBudget"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Handler) editAccount(c *gin.Context) {
 | 
			
		||||
	accountID := c.Param("accountid")
 | 
			
		||||
	accountUUID, err := uuid.Parse(accountID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.AbortWithError(http.StatusBadRequest, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var request EditAccountRequest
 | 
			
		||||
	err = c.BindJSON(&request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.AbortWithError(http.StatusBadRequest, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updateParams := postgres.UpdateAccountParams{
 | 
			
		||||
		Name:     request.Name,
 | 
			
		||||
		OnBudget: request.OnBudget,
 | 
			
		||||
		ID:       accountUUID,
 | 
			
		||||
	}
 | 
			
		||||
	account, err := h.Service.UpdateAccount(c.Request.Context(), updateParams)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.AbortWithError(http.StatusNotFound, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.returnBudgetingData(c, account.BudgetID)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,11 @@ func (*Handler) getAvailableBalance(categories []postgres.GetCategoriesRow, budg
 | 
			
		||||
	return availableBalance
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type BudgetingResponse struct {
 | 
			
		||||
	Accounts []postgres.GetAccountsWithBalanceRow
 | 
			
		||||
	Budget   postgres.Budget
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Handler) budgeting(c *gin.Context) {
 | 
			
		||||
	budgetID := c.Param("budgetid")
 | 
			
		||||
	budgetUUID, err := uuid.Parse(budgetID)
 | 
			
		||||
@@ -140,6 +145,10 @@ func (h *Handler) budgeting(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.returnBudgetingData(c, budgetUUID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Handler) returnBudgetingData(c *gin.Context, budgetUUID uuid.UUID) {
 | 
			
		||||
	budget, err := h.Service.GetBudget(c.Request.Context(), budgetUUID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.AbortWithError(http.StatusNotFound, err)
 | 
			
		||||
@@ -152,10 +161,7 @@ func (h *Handler) budgeting(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := struct {
 | 
			
		||||
		Accounts []postgres.GetAccountsWithBalanceRow
 | 
			
		||||
		Budget   postgres.Budget
 | 
			
		||||
	}{accounts, budget}
 | 
			
		||||
	data := BudgetingResponse{accounts, budget}
 | 
			
		||||
 | 
			
		||||
	c.JSON(http.StatusOK, data)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
 | 
			
		||||
	authenticated.Use(h.verifyLoginWithForbidden)
 | 
			
		||||
	authenticated.GET("/dashboard", h.dashboard)
 | 
			
		||||
	authenticated.GET("/account/:accountid/transactions", h.transactionsForAccount)
 | 
			
		||||
	authenticated.POST("/account/:accountid", h.editAccount)
 | 
			
		||||
	authenticated.GET("/admin/clear-database", h.clearDatabase)
 | 
			
		||||
	authenticated.GET("/budget/:budgetid", h.budgeting)
 | 
			
		||||
	authenticated.GET("/budget/:budgetid/:year/:month", h.budgetingForMonth)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,9 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
        <button class="px-4 py-2 text-base font-medium rounded-md shadow-sm focus:outline-none focus:ring-2">
 | 
			
		||||
                <slot></slot>
 | 
			
		||||
        </button>
 | 
			
		||||
    <button
 | 
			
		||||
        class="px-4 py-2 text-base font-medium rounded-md shadow-sm focus:outline-none focus:ring-2"
 | 
			
		||||
    >
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
    </button>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										58
									
								
								web/src/components/Modal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								web/src/components/Modal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import Card from '../components/Card.vue';
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    buttonText: string,
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    (e: 'submit'): void,
 | 
			
		||||
    (e: 'open'): void,
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const visible = ref(false);
 | 
			
		||||
function closeDialog() {
 | 
			
		||||
    visible.value = false;
 | 
			
		||||
};
 | 
			
		||||
function openDialog() {
 | 
			
		||||
    emit("open");
 | 
			
		||||
    visible.value = true;
 | 
			
		||||
};
 | 
			
		||||
function submitDialog() {
 | 
			
		||||
    visible.value = false;
 | 
			
		||||
    emit("submit");
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <button @click="openDialog">
 | 
			
		||||
        <slot name="placeholder">
 | 
			
		||||
            <Card>
 | 
			
		||||
                <p class="w-24 text-center text-6xl">+</p>
 | 
			
		||||
                <span class="text-lg" dark>{{ buttonText }}</span>
 | 
			
		||||
            </Card>
 | 
			
		||||
        </slot>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="visible"
 | 
			
		||||
        class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
 | 
			
		||||
    >
 | 
			
		||||
        <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
 | 
			
		||||
            <div class="mt-3 text-center">
 | 
			
		||||
                <h3 class="mt-3 text-lg leading-6 font-medium text-gray-900">{{ buttonText }}</h3>
 | 
			
		||||
                <slot></slot>
 | 
			
		||||
                <div class="grid grid-cols-2 gap-6">
 | 
			
		||||
                    <button
 | 
			
		||||
                        @click="closeDialog"
 | 
			
		||||
                        class="px-4 py-2 bg-red-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
 | 
			
		||||
                    >Close</button>
 | 
			
		||||
                    <button
 | 
			
		||||
                        @click="submitDialog"
 | 
			
		||||
                        class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
 | 
			
		||||
                    >Save</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -1,64 +1,18 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import Card from '../components/Card.vue';
 | 
			
		||||
import Modal from '../components/Modal.vue';
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import { useBudgetsStore } from '../stores/budget';
 | 
			
		||||
 | 
			
		||||
const dialog = ref(false);
 | 
			
		||||
const budgetName = ref("");
 | 
			
		||||
function saveBudget() {
 | 
			
		||||
  useBudgetsStore().NewBudget(budgetName.value);
 | 
			
		||||
  dialog.value = false;
 | 
			
		||||
};
 | 
			
		||||
function newBudget() {
 | 
			
		||||
  dialog.value = true;
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <Card>
 | 
			
		||||
    <p class="w-24 text-center text-6xl">+</p>
 | 
			
		||||
    <button class="text-lg" dark @click="newBudget">New Budget</button>
 | 
			
		||||
  </Card>
 | 
			
		||||
  <div v-if="dialog" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full">
 | 
			
		||||
    <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
 | 
			
		||||
      <div class="mt-3 text-center">
 | 
			
		||||
        <!--<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
 | 
			
		||||
          <svg
 | 
			
		||||
            class="h-6 w-6 text-green-600"
 | 
			
		||||
            fill="none"
 | 
			
		||||
            stroke="currentColor"
 | 
			
		||||
            viewBox="0 0 24 24"
 | 
			
		||||
          >
 | 
			
		||||
            <path
 | 
			
		||||
              stroke-linecap="round"
 | 
			
		||||
              stroke-linejoin="round"
 | 
			
		||||
              stroke-width="2"
 | 
			
		||||
              d="M5 13l4 4L19 7"
 | 
			
		||||
            />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>-->
 | 
			
		||||
        <h3 class="mt-3 text-lg leading-6 font-medium text-gray-900">New Budget</h3>
 | 
			
		||||
        <div class="mt-2 px-7 py-3">
 | 
			
		||||
          <input
 | 
			
		||||
            class="border-2"
 | 
			
		||||
            type="text"
 | 
			
		||||
            v-model="budgetName"
 | 
			
		||||
            placeholder="Budget name"
 | 
			
		||||
            required
 | 
			
		||||
          />
 | 
			
		||||
          <!--<p class="text-sm text-gray-500">Account has been successfully registered!</p>-->
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="items-center px-4 py-3">
 | 
			
		||||
          <button
 | 
			
		||||
            @click="dialog = false"
 | 
			
		||||
            class="px-4 py-2 bg-red-500 text-white text-base font-medium rounded-md w-1/2 shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
 | 
			
		||||
          >Close</button>
 | 
			
		||||
          <button
 | 
			
		||||
            class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md w-1/2 shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
 | 
			
		||||
            @click="saveBudget"
 | 
			
		||||
          >Save</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
  <Modal button-text="New Budget" @submit="saveBudget">
 | 
			
		||||
    <div class="mt-2 px-7 py-3">
 | 
			
		||||
      <input class="border-2" type="text" v-model="budgetName" placeholder="Budget name" required />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  </Modal>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -4,19 +4,54 @@ 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";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  budgetid: string
 | 
			
		||||
  accountid: string
 | 
			
		||||
    budgetid: string
 | 
			
		||||
    accountid: string
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
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>{{ CurrentAccount?.Name }}</h1>
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
    <p>
 | 
			
		||||
        Current Balance:
 | 
			
		||||
        <Currency :value="CurrentAccount?.Balance" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { defineStore } from "pinia"
 | 
			
		||||
import { GET, POST } from "../api";
 | 
			
		||||
import { useBudgetsStore } from "./budget";
 | 
			
		||||
import { useSessionStore } from "./session";
 | 
			
		||||
 | 
			
		||||
interface State {
 | 
			
		||||
@@ -124,6 +125,11 @@ export const useAccountStore = defineStore("budget/account", {
 | 
			
		||||
                return;
 | 
			
		||||
            this.addCategoriesForMonth(year, month, response.Categories);
 | 
			
		||||
        },
 | 
			
		||||
        async EditAccount(accountid : string, name : string, onBudget : boolean) {
 | 
			
		||||
            const result = await POST("/account/" + accountid, JSON.stringify({name: name, onBudget: onBudget}));
 | 
			
		||||
            const response = await result.json();
 | 
			
		||||
            useBudgetsStore().MergeBudgetingData(response);
 | 
			
		||||
        },
 | 
			
		||||
        addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
 | 
			
		||||
            this.$patch((state) => {
 | 
			
		||||
                const yearMap = state.Months.get(year) || new Map<number, Map<string, Category>>();
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,9 @@ export const useBudgetsStore = defineStore('budget', {
 | 
			
		||||
        async FetchBudget(budgetid: string) {
 | 
			
		||||
            const result = await GET("/budget/" + budgetid);
 | 
			
		||||
            const response = await result.json();
 | 
			
		||||
            this.MergeBudgetingData(response);
 | 
			
		||||
        },
 | 
			
		||||
        MergeBudgetingData(response : any) {
 | 
			
		||||
            for (const account of response.Accounts || []) {
 | 
			
		||||
                useAccountStore().Accounts.set(account.ID, account);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user