Merge pull request 'Implement basic budgeting input' (#39) from budgeting into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #39
This commit is contained in:
Jan Bader 2022-03-05 22:12:14 +01:00
commit a62ab543b0
17 changed files with 338 additions and 116 deletions

View File

@ -17,7 +17,7 @@ INSERT INTO assignments (
) VALUES ( ) VALUES (
$1, $2, $3 $1, $2, $3
) )
RETURNING id, category_id, date, memo, amount RETURNING category_id, date, memo, amount
` `
type CreateAssignmentParams struct { 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) row := q.db.QueryRowContext(ctx, createAssignment, arg.Date, arg.Amount, arg.CategoryID)
var i Assignment var i Assignment
err := row.Scan( err := row.Scan(
&i.ID,
&i.CategoryID, &i.CategoryID,
&i.Date, &i.Date,
&i.Memo, &i.Memo,
@ -130,3 +129,22 @@ func (q *Queries) GetAssignmentsByMonthAndCategory(ctx context.Context, budgetID
} }
return items, nil 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
}

View File

@ -40,7 +40,6 @@ type Account struct {
} }
type Assignment struct { type Assignment struct {
ID uuid.UUID
CategoryID uuid.UUID CategoryID uuid.UUID
Date time.Time Date time.Time
Memo sql.NullString Memo sql.NullString

View File

@ -53,6 +53,13 @@ func (n Numeric) IsZero() bool {
return float == 0 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 { func (n Numeric) MatchExp(exp int32) Numeric {
diffExp := n.Exp - exp diffExp := n.Exp - exp
factor := big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(diffExp)), nil) //nolint:gomnd 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 { func (n Numeric) Sub(other Numeric) Numeric {
left := n left := n
right := other right := other
@ -106,6 +129,22 @@ func (n Numeric) Add(other Numeric) Numeric {
panic("Cannot add with different exponents") 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 { func (n Numeric) String() string {
if n.Int == nil || n.Int.Int64() == 0 { if n.Int == nil || n.Int.Int64() == 0 {
return "0" return "0"

View File

@ -23,3 +23,10 @@ FROM assignments
INNER JOIN categories ON categories.id = assignments.category_id INNER JOIN categories ON categories.id = assignments.category_id
INNER JOIN category_groups ON categories.category_group_id = category_groups.id INNER JOIN category_groups ON categories.category_group_id = category_groups.id
WHERE category_groups.budget_id = @budget_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;

View 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;

View File

@ -3,7 +3,6 @@ package server
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"time" "time"
"git.javil.eu/jacob1123/budgeteer/postgres" "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) { func (h *Handler) budgetingForMonth(c *gin.Context) {
budgetID := c.Param("budgetid") budgetID := c.Param("budgetid")
budgetUUID, err := uuid.Parse(budgetID) budgetUUID, err := uuid.Parse(budgetID)
@ -97,8 +75,16 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
categoriesWithBalance, moneyUsed := h.calculateBalances( categoriesWithBalance, moneyUsed := h.calculateBalances(
budget, firstOfNextMonth, firstOfMonth, categories, cumultativeBalances) 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 { data := struct {
Categories []CategoryWithBalance Categories []CategoryWithBalance
@ -107,18 +93,13 @@ func (h *Handler) budgetingForMonth(c *gin.Context) {
c.JSON(http.StatusOK, data) 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, moneyUsed numeric.Numeric, cumultativeBalances []postgres.GetCumultativeBalancesRow,
firstOfNextMonth time.Time) numeric.Numeric { firstOfNextMonth time.Time) numeric.Numeric {
availableBalance := numeric.Zero() availableBalance := moneyUsed
for _, cat := range categories {
if cat.ID != budget.IncomeCategoryID {
continue
}
availableBalance = moneyUsed
for _, bal := range cumultativeBalances { for _, bal := range cumultativeBalances {
if bal.CategoryID != cat.ID { if bal.CategoryID != budget.IncomeCategoryID {
continue continue
} }
@ -126,8 +107,8 @@ func (*Handler) getAvailableBalance(categories []postgres.GetCategoriesRow, budg
continue continue
} }
availableBalance = availableBalance.Add(bal.Transactions) availableBalance.AddI(bal.Transactions)
} availableBalance.AddI(bal.Assignments)
} }
return availableBalance return availableBalance
} }
@ -171,21 +152,18 @@ func (h *Handler) calculateBalances(budget postgres.Budget,
cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, numeric.Numeric) { cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, numeric.Numeric) {
categoriesWithBalance := []CategoryWithBalance{} categoriesWithBalance := []CategoryWithBalance{}
moneyUsed := numeric.Zero() moneyUsed2 := numeric.Zero()
moneyUsed := &moneyUsed2
for i := range categories { for i := range categories {
cat := &categories[i] cat := &categories[i]
// do not show hidden categories // do not show hidden categories
categoryWithBalance := h.CalculateCategoryBalances(cat, cumultativeBalances, categoryWithBalance := h.CalculateCategoryBalances(cat, cumultativeBalances,
firstOfNextMonth, &moneyUsed, firstOfMonth, budget) firstOfNextMonth, moneyUsed, firstOfMonth, budget)
if cat.ID == budget.IncomeCategoryID {
continue
}
categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance) categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance)
} }
return categoriesWithBalance, moneyUsed return categoriesWithBalance, *moneyUsed
} }
func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow, func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow,
@ -202,11 +180,11 @@ func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow,
continue continue
} }
*moneyUsed = moneyUsed.Sub(bal.Assignments) moneyUsed.SubI(bal.Assignments)
categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Assignments) categoryWithBalance.Available.AddI(bal.Assignments)
categoryWithBalance.Available = categoryWithBalance.Available.Add(bal.Transactions) categoryWithBalance.Available.AddI(bal.Transactions)
if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) { if !categoryWithBalance.Available.IsPositive() && bal.Date.Before(firstOfMonth) {
*moneyUsed = moneyUsed.Add(categoryWithBalance.Available) moneyUsed.AddI(categoryWithBalance.Available)
categoryWithBalance.Available = numeric.Zero() categoryWithBalance.Available = numeric.Zero()
} }

55
server/category.go Normal file
View 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
}
}

View File

@ -64,6 +64,7 @@ func (h *Handler) LoadRoutes(router *gin.Engine) {
budget.POST("/new", h.newBudget) budget.POST("/new", h.newBudget)
budget.GET("/:budgetid", h.budgeting) budget.GET("/:budgetid", h.budgeting)
budget.GET("/:budgetid/:year/:month", h.budgetingForMonth) 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/payees", h.autocompletePayee)
budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories) budget.GET("/:budgetid/autocomplete/categories", h.autocompleteCategories)
budget.DELETE("/:budgetid", h.deleteBudget) budget.DELETE("/:budgetid", h.deleteBudget)

30
server/util.go Normal file
View 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
}

View File

@ -8,7 +8,6 @@ import { useSettingsStore } from "./stores/settings";
export default defineComponent({ export default defineComponent({
computed: { computed: {
...mapState(useBudgetsStore, ["CurrentBudgetName"]), ...mapState(useBudgetsStore, ["CurrentBudgetName"]),
...mapState(useSettingsStore, ["Menu"]),
...mapState(useSessionStore, ["LoggedIn"]), ...mapState(useSessionStore, ["LoggedIn"]),
}, },
methods: { methods: {
@ -27,15 +26,10 @@ export default defineComponent({
</script> </script>
<template> <template>
<div class="flex flex-col md:flex-row flex-1"> <div class="flex flex-col md:flex-row flex-1 h-screen">
<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"
>
<router-view name="sidebar"></router-view> <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"> <div class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
<span <span
class="flex-1 font-bold text-5xl -my-3 hidden md:inline" class="flex-1 font-bold text-5xl -my-3 hidden md:inline"

View File

@ -6,6 +6,7 @@ import { createPinia } from 'pinia'
import { useBudgetsStore } from './stores/budget'; import { useBudgetsStore } from './stores/budget';
import { useAccountStore } from './stores/budget-account' import { useAccountStore } from './stores/budget-account'
import PiniaLogger from './pinia-logger' import PiniaLogger from './pinia-logger'
import { useSessionStore } from './stores/session'
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)
@ -26,3 +27,46 @@ router.beforeEach(async (to, from, next) => {
await accountStore.SetCurrentAccount((<string>to.params.budgetid), (<string>to.params.accountid)); await accountStore.SetCurrentAccount((<string>to.params.budgetid), (<string>to.params.accountid));
next(); 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

View File

@ -5,7 +5,9 @@ import { useBudgetsStore } from "../stores/budget"
import { Account, useAccountStore } from "../stores/budget-account" import { Account, useAccountStore } from "../stores/budget-account"
import { useSettingsStore } from "../stores/settings" 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 budgetStore = useBudgetsStore();
const CurrentBudgetName = computed(() => budgetStore.CurrentBudgetName); const CurrentBudgetName = computed(() => budgetStore.CurrentBudgetName);
@ -30,13 +32,21 @@ function getAccountName(account : Account) {
</script> </script>
<template> <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"> <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> <router-link to="/dashboard" style="font-size:150%"></router-link>
{{ CurrentBudgetName }} {{ CurrentBudgetName }}
</span> </span>
<span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col"> <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+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>--> <!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
</span> </span>
@ -46,7 +56,9 @@ function getAccountName(account : Account) {
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" />
</div> </div>
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between"> <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" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
</div> </div>
</li> </li>
@ -56,7 +68,9 @@ function getAccountName(account : Account) {
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" />
</div> </div>
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between"> <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" /> <Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
</div> </div>
</li> </li>
@ -76,4 +90,5 @@ function getAccountName(account : Account) {
</li> </li>
<!--<li><router-link to="/admin">Admin</router-link></li>--> <!--<li><router-link to="/admin">Admin</router-link></li>-->
</div> </div>
</div>
</template> </template>

View File

@ -2,8 +2,10 @@
import { computed, defineProps, onMounted, ref, watchEffect } from "vue"; import { computed, defineProps, onMounted, ref, watchEffect } from "vue";
import Currency from "../components/Currency.vue"; import Currency from "../components/Currency.vue";
import { useBudgetsStore } from "../stores/budget"; import { useBudgetsStore } from "../stores/budget";
import { useAccountStore } from "../stores/budget-account"; import { Category, useAccountStore } from "../stores/budget-account";
import { useSessionStore } from "../stores/session"; import { useSessionStore } from "../stores/session";
import Input from "../components/Input.vue";
import { POST } from "../api";
const props = defineProps<{ const props = defineProps<{
budgetid: string, budgetid: string,
@ -63,10 +65,18 @@ function toggleGroup(group: { Name: string, Expand: boolean }) {
function getGroupState(group: { Name: string, Expand: boolean }): boolean { function getGroupState(group: { Name: string, Expand: boolean }): boolean {
return expandedGroups.value.get(group.Name) ?? group.Expand; 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> </script>
<template> <template>
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1> <h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
<span>Available balance: <Currency :value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)" /></span>
<div> <div>
<router-link <router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month" :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)"> <template v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
<span class="whitespace-nowrap overflow-hidden">{{ category.Name }}</span> <span class="whitespace-nowrap overflow-hidden">{{ category.Name }}</span>
<Currency :value="category.AvailableLastMonth" class="hidden lg:block" /> <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.Activity" class="hidden sm:block" />
<Currency :value="category.Available" /> <Currency :value="accountStore.GetCategoryAvailable(category)" />
</template> </template>
</template> </template>
</div> </div>

View File

@ -10,15 +10,16 @@ import BudgetSidebar from '../pages/BudgetSidebar.vue';
const routes = [ const routes = [
{ path: "/", name: "Index", component: Index }, { path: "/", name: "Index", component: Index },
{ path: "/dashboard", name: "Dashboard", component: Dashboard }, { path: "/dashboard", name: "Dashboard", component: Dashboard, meta: { requiresAuth: true } },
{ path: "/login", name: "Login", component: Login }, { path: "/login", name: "Login", component: Login, meta: { hideForAuth: true } },
{ path: "/register", name: "Register", component: Register }, { path: "/register", name: "Register", component: Register, meta: { hideForAuth: true } },
{ path: "/budget/:budgetid/budgeting", name: "Budget", redirect: (to : RouteLocationNormalized) => { 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/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 }, { 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 }, { path: "/budget/:budgetid/account/:accountid", name: "Account", components: { default: Account, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
] ]
const router = createRouter({ const router = createRouter({

View File

@ -31,7 +31,6 @@ export interface Category {
AvailableLastMonth: number AvailableLastMonth: number
Assigned: number Assigned: number
Activity: number Activity: number
Available: number
} }
export const useAccountStore = defineStore("budget/account", { export const useAccountStore = defineStore("budget/account", {
@ -51,12 +50,37 @@ export const useAccountStore = defineStore("budget/account", {
const monthMap = yearMap?.get(month); const monthMap = yearMap?.get(month);
return [...monthMap?.values() || []]; 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) { CategoryGroupsForMonth(state) {
return (year: number, month: number) => { return (year: number, month: number) => {
const categories = this.AllCategoriesForMonth(year, month); const categories = this.AllCategoriesForMonth(year, month);
const categoryGroups = []; const categoryGroups = [];
let prev = undefined; let prev = undefined;
for (const category of categories) { for (const category of categories) {
if (category.ID == this.GetIncomeCategoryID)
continue;
if (category.Group != prev) if (category.Group != prev)
categoryGroups.push({ categoryGroups.push({
Name: category.Group, Name: category.Group,

View File

@ -16,6 +16,7 @@ export interface Budget {
ID: string ID: string
Name: string Name: string
AvailableBalance: number AvailableBalance: number
IncomeCategoryID: string
} }
export const useSessionStore = defineStore('session', { export const useSessionStore = defineStore('session', {