Merge pull request 'Handle more of categories locally and update Modal' (#17) from categories-improvements into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #17
This commit is contained in:
Jan Bader 2022-02-23 22:58:33 +01:00
commit dae9abeeea
6 changed files with 142 additions and 133 deletions

View File

@ -1,17 +1,16 @@
FROM alpine as godeps FROM alpine as godeps
RUN apk add go RUN apk --no-cache add go
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
RUN go install github.com/go-task/task/v3/cmd/task@latest RUN go install github.com/go-task/task/v3/cmd/task@latest
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
FROM alpine FROM alpine
RUN apk add go RUN apk --no-cache add go nodejs yarn bash curl git git-perl tmux
RUN apk add nodejs yarn bash curl git git-perl tmux
ADD docker/build.sh / ADD docker/build.sh /
RUN yarn global add @vue/cli RUN yarn global add @vue/cli
ENV PATH="/root/.yarn/bin/:${PATH}" ENV PATH="/root/.yarn/bin/:${PATH}"
WORKDIR /src WORKDIR /src
ADD web/package.json /src/web/ ADD web/package.json web/yarn.lock /src/web/
RUN yarn RUN yarn
COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/ COPY --from=godeps /root/go/bin/task /root/go/bin/sqlc /root/go/bin/golangci-lint /usr/local/bin/
CMD /build.sh CMD /build.sh

View File

@ -30,6 +30,16 @@ type CategoryWithBalance struct {
Assigned postgres.Numeric Assigned postgres.Numeric
} }
func NewCategoryWithBalance(category *postgres.GetCategoriesRow) CategoryWithBalance {
return CategoryWithBalance{
GetCategoriesRow: category,
Available: postgres.NewZeroNumeric(),
AvailableLastMonth: postgres.NewZeroNumeric(),
Activity: postgres.NewZeroNumeric(),
Assigned: postgres.NewZeroNumeric(),
}
}
func getDate(c *gin.Context) (time.Time, error) { func getDate(c *gin.Context) (time.Time, error) {
var year, month int var year, month int
yearString := c.Param("year") yearString := c.Param("year")
@ -153,30 +163,13 @@ func (h *Handler) calculateBalances(budget postgres.Budget,
firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow, firstOfNextMonth time.Time, firstOfMonth time.Time, categories []postgres.GetCategoriesRow,
cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, postgres.Numeric) { cumultativeBalances []postgres.GetCumultativeBalancesRow) ([]CategoryWithBalance, postgres.Numeric) {
categoriesWithBalance := []CategoryWithBalance{} categoriesWithBalance := []CategoryWithBalance{}
hiddenCategory := CategoryWithBalance{
GetCategoriesRow: &postgres.GetCategoriesRow{
Name: "",
Group: "Hidden Categories",
},
Available: postgres.NewZeroNumeric(),
AvailableLastMonth: postgres.NewZeroNumeric(),
Activity: postgres.NewZeroNumeric(),
Assigned: postgres.NewZeroNumeric(),
}
moneyUsed := postgres.NewZeroNumeric() moneyUsed := postgres.NewZeroNumeric()
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, hiddenCategory, budget) firstOfNextMonth, &moneyUsed, firstOfMonth, budget)
if cat.Group == "Hidden Categories" {
hiddenCategory.Available = hiddenCategory.Available.Add(categoryWithBalance.Available)
hiddenCategory.AvailableLastMonth = hiddenCategory.AvailableLastMonth.Add(categoryWithBalance.AvailableLastMonth)
hiddenCategory.Activity = hiddenCategory.Activity.Add(categoryWithBalance.Activity)
hiddenCategory.Assigned = hiddenCategory.Assigned.Add(categoryWithBalance.Assigned)
continue
}
if cat.ID == budget.IncomeCategoryID { if cat.ID == budget.IncomeCategoryID {
continue continue
@ -185,22 +178,13 @@ func (h *Handler) calculateBalances(budget postgres.Budget,
categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance) categoriesWithBalance = append(categoriesWithBalance, categoryWithBalance)
} }
categoriesWithBalance = append(categoriesWithBalance, hiddenCategory)
return categoriesWithBalance, moneyUsed return categoriesWithBalance, moneyUsed
} }
func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow, func (*Handler) CalculateCategoryBalances(cat *postgres.GetCategoriesRow,
cumultativeBalances []postgres.GetCumultativeBalancesRow, firstOfNextMonth time.Time, cumultativeBalances []postgres.GetCumultativeBalancesRow, firstOfNextMonth time.Time,
moneyUsed *postgres.Numeric, firstOfMonth time.Time, hiddenCategory CategoryWithBalance, moneyUsed *postgres.Numeric, firstOfMonth time.Time, budget postgres.Budget) CategoryWithBalance {
budget postgres.Budget) CategoryWithBalance { categoryWithBalance := NewCategoryWithBalance(cat)
categoryWithBalance := CategoryWithBalance{
GetCategoriesRow: cat,
Available: postgres.NewZeroNumeric(),
AvailableLastMonth: postgres.NewZeroNumeric(),
Activity: postgres.NewZeroNumeric(),
Assigned: postgres.NewZeroNumeric(),
}
for _, bal := range cumultativeBalances { for _, bal := range cumultativeBalances {
if bal.CategoryID != cat.ID { if bal.CategoryID != cat.ID {
continue continue

View File

@ -19,17 +19,45 @@ function newBudget() {
<p class="w-24 text-center text-6xl">+</p> <p class="w-24 text-center text-6xl">+</p>
<button class="text-lg" dark @click="newBudget">New Budget</button> <button class="text-lg" dark @click="newBudget">New Budget</button>
</Card> </Card>
<div v-if="dialog" justify="center"> <div v-if="dialog" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full">
<div> <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div> <div class="mt-3 text-center">
<span class="text-h5">New Budget</span> <!--<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>
<div> <div class="items-center px-4 py-3">
<input type="text" v-model="budgetName" label="Budget name" required /> <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>
<div>
<button @click="dialog = false">Close</button>
<button @click="saveBudget">Save</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -14,11 +14,19 @@ const props = defineProps<{
const budgetsStore = useBudgetsStore(); const budgetsStore = useBudgetsStore();
const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID); const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID);
const categoriesForMonth = useAccountStore().CategoriesForMonth; const accountStore = useAccountStore();
const Categories = computed(() => { const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
return [...categoriesForMonth(selected.value.Year, selected.value.Month)];
function GetCategories(group : string) {
return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
};
const groupsForMonth = accountStore.CategoryGroupsForMonth;
const GroupsForMonth = computed(() => {
return [...groupsForMonth(selected.value.Year, selected.value.Month)];
}); });
const previous = computed(() => ({ const previous = computed(() => ({
Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(), Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(), Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
@ -61,7 +69,6 @@ onMounted(() => {
</div> </div>
<table class="container col-lg-12" id="content"> <table class="container col-lg-12" id="content">
<tr> <tr>
<th>Group</th>
<th>Category</th> <th>Category</th>
<th></th> <th></th>
<th></th> <th></th>
@ -70,8 +77,9 @@ onMounted(() => {
<th>Activity</th> <th>Activity</th>
<th>Available</th> <th>Available</th>
</tr> </tr>
<tr v-for="category in Categories"> <tbody v-for="group in GroupsForMonth">
<td>{{ category.Group }}</td> <p class="text-lg font-bold">{{ group }}</p>
<tr v-for="category in GetCategories(group)">
<td>{{ category.Name }}</td> <td>{{ category.Name }}</td>
<td></td> <td></td>
<td></td> <td></td>
@ -88,5 +96,6 @@ onMounted(() => {
<Currency :value="category.Available" /> <Currency :value="category.Available" />
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
</template> </template>

View File

@ -4,6 +4,7 @@ import { useRouter } from "vue-router";
import { DELETE, POST } from "../api"; import { DELETE, POST } from "../api";
import { useBudgetsStore } from "../stores/budget"; import { useBudgetsStore } from "../stores/budget";
import { useSessionStore } from "../stores/session"; import { useSessionStore } from "../stores/session";
import Card from "../components/Card.vue";
const transactionsFile = ref<File | undefined>(undefined); const transactionsFile = ref<File | undefined>(undefined);
const assignmentsFile = ref<File | undefined>(undefined); const assignmentsFile = ref<File | undefined>(undefined);
@ -54,55 +55,27 @@ function ynabImport() {
</script> </script>
<template> <template>
<v-container> <div>
<h1>Danger Zone</h1> <h1>Danger Zone</h1>
<v-row> <div class="grid md:grid-cols-2 gap-6">
<v-col cols="12" md="6" xl="3"> <Card>
<v-card> <h2>Clear Budget</h2>
<v-card-header> <p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
<v-card-header-text>
<v-card-title>Clear Budget</v-card-title> <button @click="clearBudget">Clear budget</button>
<v-card-subtitle>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</v-card-subtitle> </Card>
</v-card-header-text> <Card>
</v-card-header> <h2>Delete Budget</h2>
<v-card-actions class="justify-center"> <p>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</p>
<v-btn @click="clearBudget">Clear budget</v-btn> <button @click="deleteBudget">Delete budget</button>
</v-card-actions> </Card>
</v-card> <Card>
</v-col> <h2>Fix all historic negative category-balances</h2>
<v-col cols="12" md="6" xl="3"> <p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
<v-card> <button @click="cleanNegative">Fix negative</button>
<v-card-header> </Card>
<v-card-header-text> <Card>
<v-card-title>Delete Budget</v-card-title> <h2>Import YNAB Budget</h2>
<v-card-subtitle>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</v-card-subtitle>
</v-card-header-text>
</v-card-header>
<v-card-actions class="justify-center">
<v-btn @click="deleteBudget">Delete budget</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="12" md="6" xl="3">
<v-card>
<v-card-header>
<v-card-header-text>
<v-card-title>Fix all historic negative category-balances</v-card-title>
<v-card-subtitle>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</v-card-subtitle>
</v-card-header-text>
</v-card-header>
<v-card-actions class="justify-center">
<v-btn @click="cleanNegative">Fix negative</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="12" xl="6">
<v-card>
<v-card-header>
<v-card-header-text>
<v-card-title>Import YNAB Budget</v-card-title>
</v-card-header-text>
</v-card-header>
<label for="transactions_file"> <label for="transactions_file">
Transaktionen: Transaktionen:
@ -114,12 +87,8 @@ function ynabImport() {
<input type="file" @change="gotAssignments" accept="text/*" /> <input type="file" @change="gotAssignments" accept="text/*" />
</label> </label>
<v-card-actions class="justify-center"> <button :disabled="filesIncomplete" @click="ynabImport">Importieren</button>
<v-btn :disabled="filesIncomplete" @click="ynabImport">Importieren</v-btn> </Card>
</v-card-actions> </div>
</v-card> </div>
</v-col>
</v-row>
<v-card></v-card>
</v-container>
</template> </template>

View File

@ -54,12 +54,30 @@ export const useAccountStore = defineStore("budget/account", {
AccountsList(state) { AccountsList(state) {
return [...state.Accounts.values()]; return [...state.Accounts.values()];
}, },
CategoriesForMonth: (state) => (year: number, month: number) => { AllCategoriesForMonth: (state) => (year: number, month: number) => {
const yearMap = state.Months.get(year); const yearMap = state.Months.get(year);
const monthMap = yearMap?.get(month); const monthMap = yearMap?.get(month);
console.log("MTH", monthMap)
return [...monthMap?.values() || []]; return [...monthMap?.values() || []];
}, },
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.Group != prev)
categoryGroups.push(category.Group);
prev = category.Group;
}
return categoryGroups;
}
},
CategoriesForMonthAndGroup(state) {
return (year: number, month: number, group : string) => {
const categories = this.AllCategoriesForMonth(year, month);
return categories.filter(x => x.Group == group);
}
},
CurrentAccount(state): Account | undefined { CurrentAccount(state): Account | undefined {
if (state.CurrentAccountID == null) if (state.CurrentAccountID == null)
return undefined; return undefined;
@ -102,6 +120,8 @@ export const useAccountStore = defineStore("budget/account", {
async FetchMonthBudget(budgetid: string, year: number, month: number) { async FetchMonthBudget(budgetid: string, year: number, month: number) {
const result = await GET("/budget/" + budgetid + "/" + year + "/" + month); const result = await GET("/budget/" + budgetid + "/" + year + "/" + month);
const response = await result.json(); const response = await result.json();
if(response.Categories == undefined || response.Categories.length <= 0)
return;
this.addCategoriesForMonth(year, month, response.Categories); this.addCategoriesForMonth(year, month, response.Categories);
}, },
addCategoriesForMonth(year: number, month: number, categories: Category[]): void { addCategoriesForMonth(year: number, month: number, categories: Category[]): void {