Implement basic budgeting input #39
@ -17,7 +17,7 @@ INSERT INTO assignments (
|
||||
) VALUES (
|
||||
$1, $2, $3
|
||||
)
|
||||
RETURNING id, category_id, date, memo, amount
|
||||
RETURNING category_id, date, memo, amount
|
||||
`
|
||||
|
||||
type CreateAssignmentParams struct {
|
||||
@ -30,7 +30,6 @@ func (q *Queries) CreateAssignment(ctx context.Context, arg CreateAssignmentPara
|
||||
row := q.db.QueryRowContext(ctx, createAssignment, arg.Date, arg.Amount, arg.CategoryID)
|
||||
var i Assignment
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CategoryID,
|
||||
&i.Date,
|
||||
&i.Memo,
|
||||
@ -130,3 +129,22 @@ func (q *Queries) GetAssignmentsByMonthAndCategory(ctx context.Context, budgetID
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateAssignment = `-- name: UpdateAssignment :exec
|
||||
INSERT INTO assignments (category_id, date, amount)
|
||||
VALUES($1, $2, $3)
|
||||
ON CONFLICT (category_id, date)
|
||||
DO
|
||||
UPDATE SET amount = $3
|
||||
`
|
||||
|
||||
type UpdateAssignmentParams struct {
|
||||
CategoryID uuid.UUID
|
||||
Date time.Time
|
||||
Amount numeric.Numeric
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAssignment(ctx context.Context, arg UpdateAssignmentParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAssignment, arg.CategoryID, arg.Date, arg.Amount)
|
||||
return err
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ type Account struct {
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
ID uuid.UUID
|
||||
CategoryID uuid.UUID
|
||||
Date time.Time
|
||||
Memo sql.NullString
|
||||
|
@ -53,6 +53,13 @@ func (n Numeric) IsZero() bool {
|
||||
return float == 0
|
||||
}
|
||||
|
||||
func (n *Numeric) MatchExpI(exp int32) {
|
||||
diffExp := n.Exp - exp
|
||||
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) //nolint:gomnd
|
||||
n.Exp = exp
|
||||
n.Int = big.NewInt(0).Mul(n.Int, factor)
|
||||
}
|
||||
|
||||
func (n Numeric) MatchExp(exp int32) Numeric {
|
||||
diffExp := n.Exp - exp
|
||||
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) //nolint:gomnd
|
||||
@ -64,6 +71,22 @@ func (n Numeric) MatchExp(exp int32) Numeric {
|
||||
}}
|
||||
}
|
||||
|
||||
func (n *Numeric) SubI(other Numeric) *Numeric {
|
||||
right := other
|
||||
if n.Exp < other.Exp {
|
||||
right = other.MatchExp(n.Exp)
|
||||
} else if n.Exp > other.Exp {
|
||||
n.MatchExpI(other.Exp)
|
||||
}
|
||||
|
||||
if n.Exp == right.Exp {
|
||||
n.Int = big.NewInt(0).Sub(n.Int, right.Int)
|
||||
return n
|
||||
}
|
||||
|
||||
panic("Cannot subtract with different exponents")
|
||||
}
|
||||
|
||||
func (n Numeric) Sub(other Numeric) Numeric {
|
||||
left := n
|
||||
right := other
|
||||
@ -106,6 +129,22 @@ func (n Numeric) Add(other Numeric) Numeric {
|
||||
panic("Cannot add with different exponents")
|
||||
}
|
||||
|
||||
func (n *Numeric) AddI(other Numeric) *Numeric {
|
||||
right := other
|
||||
if n.Exp < other.Exp {
|
||||
right = other.MatchExp(n.Exp)
|
||||
} else if n.Exp > other.Exp {
|
||||
n.MatchExpI(other.Exp)
|
||||
}
|
||||
|
||||
if n.Exp == right.Exp {
|
||||
n.Int = big.NewInt(0).Add(n.Int, right.Int)
|
||||
return n
|
||||
}
|
||||
|
||||
panic("Cannot add with different exponents")
|
||||
}
|
||||
|
||||
func (n Numeric) String() string {
|
||||
if n.Int == nil || n.Int.Int64() == 0 {
|
||||
return "0"
|
||||
|
@ -23,3 +23,10 @@ FROM assignments
|
||||
INNER JOIN categories ON categories.id = assignments.category_id
|
||||
INNER JOIN category_groups ON categories.category_group_id = category_groups.id
|
||||
WHERE category_groups.budget_id = @budget_id;
|
||||
|
||||
-- name: UpdateAssignment :exec
|
||||
INSERT INTO assignments (category_id, date, amount)
|
||||
VALUES($1, $2, $3)
|
||||
ON CONFLICT (category_id, date)
|
||||
DO
|
||||
UPDATE SET amount = $3;
|
6
postgres/schema/0017_natural-key-assignments.sql
Normal file
6
postgres/schema/0017_natural-key-assignments.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE assignments DROP id;
|
||||
ALTER TABLE assignments ADD PRIMARY KEY (category_id, date);
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE assignments ADD COLUMN id uuid DEFAULT uuid_generate_v4() PRIMARY KEY;
|
@ -3,7 +3,6 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
@ -41,27 +40,6 @@ func NewCategoryWithBalance(category *postgres.GetCategoriesRow) CategoryWithBal
|
||||
}
|
||||
}
|
||||
|
||||
func getDate(c *gin.Context) (time.Time, error) {
|
||||
var year, month int
|
||||
yearString := c.Param("year")
|
||||
monthString := c.Param("month")
|
||||
if yearString == "" && monthString == "" {
|
||||
return getFirstOfMonthTime(time.Now()), nil
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(yearString)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parse year: %w", err)
|
||||
}
|
||||
|
||||
month, err = strconv.Atoi(monthString)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parse month: %w", err)
|
||||
}
|
||||
|
||||
return getFirstOfMonth(year, month, time.Now().Location()), nil
|
||||
}
|
||||
|
||||
func (h *Handler) budgetingForMonth(c *gin.Context) {
|
||||
budgetID := c.Param("budgetid")
|
||||
budgetUUID, err := uuid.Parse(budgetID)
|
||||
@ -97,8 +75,16 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
|
||||
|
||||
categoriesWithBalance, moneyUsed := h.calculateBalances(
|
||||
budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances)
|
||||
availableBalance := h.getAvailableBalance(budget, moneyUsed, cumultativeBalances, firstOfNextMonth)
|
||||
for i := range categoriesWithBalance {
|
||||
cat := &categoriesWithBalance[i]
|
||||
if cat.ID != budget.IncomeCategoryID {
|
||||
continue
|
||||
}
|
||||
|
||||
availableBalance := h.getAvailableBalance(categories, budget, moneyUsed, cumultativeBalances, firstOfNextMonth)
|
||||
cat.Available = availableBalance
|
||||
cat.AvailableLastMonth = availableBalance
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Categories []CategoryWithBalance
|
||||
@ -107,18 +93,13 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (*Handler) getAvailableBalance(categories []postgres.GetCategoriesRow, budget postgres.Budget,
|
||||
func (*Handler) getAvailableBalance(budget postgres.Budget,
|
||||
moneyUsed numeric.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow,
|
||||
firstOfNextMonth time.Time) numeric.Numeric {
|
||||
availableBalance := numeric.Zero()
|
||||
for _, cat := range categories {
|
||||
if cat.ID != budget.IncomeCategoryID {
|
||||
continue
|
||||
}
|
||||
availableBalance = moneyUsed
|
||||
availableBalance := moneyUsed
|
||||
|
||||
for _, bal := range cumultativeBalances {
|
||||
if bal.CategoryID != cat.ID {
|
||||
if bal.CategoryID != budget.IncomeCategoryID {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -126,8 +107,8 @@ func (*Handler) getAvailableBalance(categories []postgres.GetCategoriesRow, budg
|
||||
continue
|
||||
}
|
||||
|
||||
availableBalance = availableBalance.Add(bal.Transactions)
|
||||
}
|
||||
availableBalance.AddI(bal.Transactions)
|
||||
availableBalance.AddI(bal.Assignments)
|
||||
}
|
||||
return availableBalance
|
||||
}
|
||||
@ -171,21 +152,18 @@ func (h *Handler) calculateBalances(budget postgres.Budget,
|
||||
cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, numeric.Numeric) {
|
||||
categoriesWithBalance := []CategoryWithBalance{}
|
||||
|
||||
moneyUsed := numeric.Zero()
|
||||
moneyUsed2 := numeric.Zero()
|
||||
moneyUsed := &moneyUsed2
|
||||
for i := range categories {
|
||||
cat := &categories[i]
|
||||
// do not show hidden categories
|
||||
categoryWithBalance := h.CalculateCategoryBalances(cat, cumultativeBalances,
|
||||
firstOfNextMonth, &moneyUsed, firstOfMonth, budget)
|
||||
|
||||
if cat.ID == budget.IncomeCategoryID {
|
||||
continue
|
||||
}
|
||||
firstOfNextMonth, moneyUsed, firstOfMonth, budget)
|
||||
|
||||
categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance)
|
||||
}
|
||||
|
||||
return categoriesWithBalance, moneyUsed
|
||||
return categoriesWithBalance, *moneyUsed
|
||||
}
|
||||
|
||||
func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow,
|
||||
@ -202,11 +180,11 @@ func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow,
|
||||
continue
|
||||
}
|
||||
|
||||
*moneyUsed = moneyUsed.Sub(bal.Assignments)
|
||||
categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Assignments)
|
||||
categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Transactions)
|
||||
moneyUsed.SubI(bal.Assignments)
|
||||
categoryWithBalance.Available.AddI(bal.Assignments)
|
||||
categoryWithBalance.Available.AddI(bal.Transactions)
|
||||
if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) {
|
||||
*moneyUsed = moneyUsed.Add(categoryWithBalance.Available)
|
||||
moneyUsed.AddI(categoryWithBalance.Available)
|
||||
categoryWithBalance.Available = numeric.Zero()
|
||||
}
|
||||
|
||||
|
55
server/category.go
Normal file
55
server/category.go
Normal file
@ -0,0 +1,55 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres"
|
||||
"git.javil.eu/jacob1123/budgeteer/postgres/numeric"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SetCategoryAssignmentRequest struct {
|
||||
Assigned string
|
||||
}
|
||||
|
||||
func (h *Handler) setCategoryAssignment(c *gin.Context) {
|
||||
categoryID := c.Param("categoryid")
|
||||
categoryUUID, err := uuid.Parse(categoryID)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{"categoryid missing from URL"})
|
||||
return
|
||||
}
|
||||
|
||||
var request SetCategoryAssignmentRequest
|
||||
err = c.BindJSON(&request)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid payload: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
date, err := getDate(c)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("date invalid: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var amount numeric.Numeric
|
||||
err = amount.Set(request.Assigned)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("parse amount: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
updateArgs := postgres.UpdateAssignmentParams{
|
||||
CategoryID: categoryUUID,
|
||||
Date: date,
|
||||
Amount: amount,
|
||||
}
|
||||
err = h.Service.UpdateAssignment(c.Request.Context(), updateArgs)
|
||||
if err != nil {
|
||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("update assignment: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
@ -64,6 +64,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
|
||||
budget.POST("/new", h.newBudget)
|
||||
budget.GET("/:budgetid", h.budgeting)
|
||||
budget.GET("/:budgetid/:year/:month", h.budgetingForMonth)
|
||||
budget.POST("/:budgetid/category/:categoryid/:year/:month", h.setCategoryAssignment)
|
||||
budget.GET("/:budgetid/autocomplete/payees", h.autocompletePayee)
|
||||
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
|
||||
budget.DELETE("/:budgetid", h.deleteBudget)
|
||||
|
30
server/util.go
Normal file
30
server/util.go
Normal file
@ -0,0 +1,30 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getDate(c *gin.Context) (time.Time, error) {
|
||||
var year, month int
|
||||
yearString := c.Param("year")
|
||||
monthString := c.Param("month")
|
||||
if yearString == "" && monthString == "" {
|
||||
return getFirstOfMonthTime(time.Now()), nil
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(yearString)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parse year: %w", err)
|
||||
}
|
||||
|
||||
month, err = strconv.Atoi(monthString)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parse month: %w", err)
|
||||
}
|
||||
|
||||
return getFirstOfMonth(year, month, time.Now().Location()), nil
|
||||
}
|
@ -8,7 +8,6 @@ import { useSettingsStore } from "./stores/settings";
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useBudgetsStore, ["CurrentBudgetName"]),
|
||||
...mapState(useSettingsStore, ["Menu"]),
|
||||
...mapState(useSessionStore, ["LoggedIn"]),
|
||||
},
|
||||
methods: {
|
||||
@ -27,15 +26,10 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col md:flex-row flex-1">
|
||||
<div
|
||||
:class="[Menu.Expand ? 'md:w-72' : 'md:w-36', Menu.Show ? '' : 'hidden']"
|
||||
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row flex-1 h-screen">
|
||||
<router-view name="sidebar"></router-view>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
|
||||
<span
|
||||
class="flex-1 font-bold text-5xl -my-3 hidden md:inline"
|
||||
|
@ -6,6 +6,7 @@ import { createPinia } from 'pinia'
|
||||
import { useBudgetsStore } from './stores/budget';
|
||||
import { useAccountStore } from './stores/budget-account'
|
||||
import PiniaLogger from './pinia-logger'
|
||||
import { useSessionStore } from './stores/session'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
@ -26,3 +27,46 @@ router.beforeEach(async (to, from, next) => {
|
||||
await accountStore.SetCurrentAccount((<string>to.params.budgetid), (<string>to.params.accountid));
|
||||
next();
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const sessionStore = useSessionStore();
|
||||
const token = sessionStore.Session?.Token;
|
||||
let loggedIn = false;
|
||||
|
||||
if (token != null) {
|
||||
const jwt = parseJwt(token);
|
||||
if (jwt.exp > Date.now() / 1000)
|
||||
loggedIn = true;
|
||||
}
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (!loggedIn) {
|
||||
next({ path: '/login' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
} else if (to.matched.some(record => record.meta.hideForAuth)) {
|
||||
if (loggedIn) {
|
||||
next({ path: '/dashboard' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
function parseJwt(token: string) {
|
||||
var base64Url = token.split('.')[1];
|
||||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
};
|
||||
|
||||
|
||||
1646426130
|
||||
1646512855755
|
@ -5,7 +5,9 @@ import { useBudgetsStore } from "../stores/budget"
|
||||
import { Account, useAccountStore } from "../stores/budget-account"
|
||||
import { useSettingsStore } from "../stores/settings"
|
||||
|
||||
const ExpandMenu = computed(() => useSettingsStore().Menu.Expand);
|
||||
const settings = useSettingsStore();
|
||||
const ExpandMenu = computed(() => settings.Menu.Expand);
|
||||
const ShowMenu = computed(() => settings.Menu.Show);
|
||||
|
||||
const budgetStore = useBudgetsStore();
|
||||
const CurrentBudgetName = computed(() => budgetStore.CurrentBudgetName);
|
||||
@ -30,13 +32,21 @@ function getAccountName(account : Account) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[ExpandMenu ? 'md:w-72' : 'md:w-36', ShowMenu ? '' : 'hidden']"
|
||||
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black"
|
||||
>
|
||||
<div class="flex flex-col mt-14 md:mt-0">
|
||||
<span class="m-2 p-1 px-3 h-10 overflow-hidden" :class="[ExpandMenu ? 'text-2xl' : 'text-md']">
|
||||
<span
|
||||
class="m-2 p-1 px-3 h-10 overflow-hidden"
|
||||
:class="[ExpandMenu ? 'text-2xl' : 'text-md']"
|
||||
>
|
||||
<router-link to="/dashboard" style="font-size:150%">⌂</router-link>
|
||||
{{ CurrentBudgetName }}
|
||||
</span>
|
||||
<span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col">
|
||||
<router-link :to="'/budget/'+CurrentBudgetID+'/budgeting'">Budget</router-link><br />
|
||||
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
|
||||
<br />
|
||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
|
||||
</span>
|
||||
@ -46,7 +56,9 @@ function getAccountName(account : Account) {
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" />
|
||||
</div>
|
||||
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
|
||||
<router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{getAccountName(account)}}</router-link>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID"
|
||||
>{{ getAccountName(account) }}</router-link>
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
|
||||
</div>
|
||||
</li>
|
||||
@ -56,7 +68,9 @@ function getAccountName(account : Account) {
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" />
|
||||
</div>
|
||||
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
|
||||
<router-link :to="'/budget/'+CurrentBudgetID+'/account/'+account.ID">{{getAccountName(account)}}</router-link>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID"
|
||||
>{{ getAccountName(account) }}</router-link>
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
|
||||
</div>
|
||||
</li>
|
||||
@ -76,4 +90,5 @@ function getAccountName(account : Account) {
|
||||
</li>
|
||||
<!--<li><router-link to="/admin">Admin</router-link></li>-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -2,8 +2,10 @@
|
||||
import { computed, defineProps, onMounted, ref, watchEffect } from "vue";
|
||||
import Currency from "../components/Currency.vue";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
import { useAccountStore } from "../stores/budget-account";
|
||||
import { Category, useAccountStore } from "../stores/budget-account";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import Input from "../components/Input.vue";
|
||||
import { POST } from "../api";
|
||||
|
||||
const props = defineProps<{
|
||||
budgetid: string,
|
||||
@ -63,10 +65,18 @@ function toggleGroup(group: { Name: string, Expand: boolean }) {
|
||||
function getGroupState(group: { Name: string, Expand: boolean }): boolean {
|
||||
return expandedGroups.value.get(group.Name) ?? group.Expand;
|
||||
}
|
||||
|
||||
function assignedChanged(e : Event, category : Category){
|
||||
const target = e.target as HTMLInputElement;
|
||||
const value = target.valueAsNumber;
|
||||
POST("/budget/"+CurrentBudgetID.value+"/category/" + category.ID + "/" + selected.value.Year + "/" + (selected.value.Month+1),
|
||||
JSON.stringify({Assigned: category.Assigned}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
|
||||
<span>Available balance: <Currency :value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)" /></span>
|
||||
<div>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
||||
@ -92,9 +102,9 @@ function getGroupState(group: { Name: string, Expand: boolean }): boolean {
|
||||
<template v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
|
||||
<span class="whitespace-nowrap overflow-hidden">{{ category.Name }}</span>
|
||||
<Currency :value="category.AvailableLastMonth" class="hidden lg:block" />
|
||||
<Currency :value="category.Assigned" class="hidden sm:block" />
|
||||
<Input type="number" v-model="category.Assigned" @input="(evt) => assignedChanged(evt, category)" class="hidden sm:block mx-2 text-right" />
|
||||
<Currency :value="category.Activity" class="hidden sm:block" />
|
||||
<Currency :value="category.Available" />
|
||||
<Currency :value="accountStore.GetCategoryAvailable(category)" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -10,15 +10,16 @@ import BudgetSidebar from '../pages/BudgetSidebar.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: "/", name: "Index", component: Index },
|
||||
{ path: "/dashboard", name: "Dashboard", component: Dashboard },
|
||||
{ path: "/login", name: "Login", component: Login },
|
||||
{ path: "/register", name: "Register", component: Register },
|
||||
{ path: "/dashboard", name: "Dashboard", component: Dashboard, meta: { requiresAuth: true } },
|
||||
{ path: "/login", name: "Login", component: Login, meta: { hideForAuth: true } },
|
||||
{ path: "/register", name: "Register", component: Register, meta: { hideForAuth: true } },
|
||||
{ path: "/budget/:budgetid/budgeting", name: "Budget", redirect: (to : RouteLocationNormalized) =>
|
||||
'/budget/' + to.params.budgetid + '/budgeting/' + new Date().getFullYear() + '/' + new Date().getMonth()
|
||||
'/budget/' + to.params.budgetid + '/budgeting/' + new Date().getFullYear() + '/' + new Date().getMonth(),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{ path: "/budget/:budgetid/budgeting/:year/:month", name: "Budget with date", components: { default: Budgeting, sidebar: BudgetSidebar }, props: true },
|
||||
{ path: "/budget/:budgetid/Settings", name: "Budget Settings", components: { default: Settings, sidebar: BudgetSidebar }, props: true },
|
||||
{ path: "/budget/:budgetid/account/:accountid", name: "Account", components: { default: Account, sidebar: BudgetSidebar }, props: true },
|
||||
{ path: "/budget/:budgetid/budgeting/:year/:month", name: "Budget with date", components: { default: Budgeting, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
|
||||
{ path: "/budget/:budgetid/Settings", name: "Budget Settings", components: { default: Settings, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
|
||||
{ path: "/budget/:budgetid/account/:accountid", name: "Account", components: { default: Account, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
@ -31,7 +31,6 @@ export interface Category {
|
||||
AvailableLastMonth: number
|
||||
Assigned: number
|
||||
Activity: number
|
||||
Available: number
|
||||
}
|
||||
|
||||
export const useAccountStore = defineStore("budget/account", {
|
||||
@ -51,12 +50,37 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
const monthMap = yearMap?.get(month);
|
||||
return [...monthMap?.values() || []];
|
||||
},
|
||||
GetCategoryAvailable(state) {
|
||||
return (category : Category) : number => {
|
||||
return category.AvailableLastMonth + Number(category.Assigned) + category.Activity;
|
||||
}
|
||||
},
|
||||
GetIncomeCategoryID(state) {
|
||||
const budget = useBudgetsStore();
|
||||
return budget.CurrentBudget?.IncomeCategoryID;
|
||||
},
|
||||
GetIncomeAvailable(state) {
|
||||
return (year: number, month: number) => {
|
||||
const IncomeCategoryID = this.GetIncomeCategoryID;
|
||||
if(IncomeCategoryID == null)
|
||||
return 0;
|
||||
|
||||
const categories = this.AllCategoriesForMonth(year, month);
|
||||
const category = categories.filter(x => x.ID == IncomeCategoryID)[0];
|
||||
if (category == null)
|
||||
return 0;
|
||||
return category.AvailableLastMonth;
|
||||
}
|
||||
},
|
||||
CategoryGroupsForMonth(state) {
|
||||
return (year: number, month: number) => {
|
||||
const categories = this.AllCategoriesForMonth(year, month);
|
||||
const categoryGroups = [];
|
||||
let prev = undefined;
|
||||
for (const category of categories) {
|
||||
if (category.ID == this.GetIncomeCategoryID)
|
||||
continue;
|
||||
|
||||
if (category.Group != prev)
|
||||
categoryGroups.push({
|
||||
Name: category.Group,
|
||||
|
@ -16,6 +16,7 @@ export interface Budget {
|
||||
ID: string
|
||||
Name: string
|
||||
AvailableBalance: number
|
||||
IncomeCategoryID: string
|
||||
}
|
||||
|
||||
export const useSessionStore = defineStore('session', {
|
||||
|
Loading…
x
Reference in New Issue
Block a user