Improve mobile usability #30
@@ -1,6 +1,7 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
@@ -29,11 +30,14 @@ func main() {
 | 
			
		||||
		panic("couldn't open static files")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokenVerifier, err := jwt.NewTokenVerifier(cfg.SessionSecret)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(fmt.Errorf("couldn't create token verifier: %w", err))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	handler := &server.Handler{
 | 
			
		||||
		Service: queries,
 | 
			
		||||
		TokenVerifier: &jwt.TokenVerifier{
 | 
			
		||||
			Secret: cfg.SessionSecret,
 | 
			
		||||
		},
 | 
			
		||||
		Service:             queries,
 | 
			
		||||
		TokenVerifier:       tokenVerifier,
 | 
			
		||||
		CredentialsVerifier: &bcrypt.Verifier{},
 | 
			
		||||
		StaticFS:            http.FS(static),
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								jwt/login.go
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								jwt/login.go
									
									
									
									
									
								
							@@ -12,7 +12,19 @@ import (
 | 
			
		||||
 | 
			
		||||
// TokenVerifier verifies Tokens.
 | 
			
		||||
type TokenVerifier struct {
 | 
			
		||||
	Secret string
 | 
			
		||||
	secret string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ErrEmptySecret = fmt.Errorf("secret is required")
 | 
			
		||||
 | 
			
		||||
func NewTokenVerifier(secret string) (*TokenVerifier, error) {
 | 
			
		||||
	if secret == "" {
 | 
			
		||||
		return nil, ErrEmptySecret
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &TokenVerifier{
 | 
			
		||||
		secret: secret,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Token contains everything to authenticate a user.
 | 
			
		||||
@@ -29,6 +41,10 @@ const (
 | 
			
		||||
 | 
			
		||||
// CreateToken creates a new token from username and name.
 | 
			
		||||
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
 | 
			
		||||
	if tv.secret == "" {
 | 
			
		||||
		return "", ErrEmptySecret
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"usr":  user.Email,
 | 
			
		||||
		"name": user.Name,
 | 
			
		||||
@@ -37,7 +53,7 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Generate encoded token and send it as response.
 | 
			
		||||
	t, err := token.SignedString([]byte(tv.Secret))
 | 
			
		||||
	t, err := token.SignedString([]byte(tv.secret))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("create token: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -53,11 +69,15 @@ var (
 | 
			
		||||
 | 
			
		||||
// VerifyToken verifys a given string-token.
 | 
			
		||||
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
 | 
			
		||||
	if tv.secret == "" {
 | 
			
		||||
		return nil, ErrEmptySecret
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
 | 
			
		||||
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
 | 
			
		||||
			return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
 | 
			
		||||
		}
 | 
			
		||||
		return []byte(tv.Secret), nil
 | 
			
		||||
		return []byte(tv.secret), nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("parse jwt: %w", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -28,11 +28,10 @@ func TestRegisterUser(t *testing.T) { //nolint:funlen
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokenVerifier, _ := jwt.NewTokenVerifier("this_is_my_demo_secret_for_unit_tests")
 | 
			
		||||
	h := Handler{
 | 
			
		||||
		Service: database,
 | 
			
		||||
		TokenVerifier: &jwt.TokenVerifier{
 | 
			
		||||
			Secret: "this_is_my_demo_secret_for_unit_tests",
 | 
			
		||||
		},
 | 
			
		||||
		Service:             database,
 | 
			
		||||
		TokenVerifier:       tokenVerifier,
 | 
			
		||||
		CredentialsVerifier: &bcrypt.Verifier{},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <button
 | 
			
		||||
        class="px-4 py-2 text-base font-medium rounded-md shadow-sm focus:outline-none focus:ring-2"
 | 
			
		||||
        class="px-4 rounded-md shadow-sm focus:outline-none focus:ring-2"
 | 
			
		||||
    >
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
    </button>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="flex flex-row items-center bg-gray-300 h-32 rounded-lg">
 | 
			
		||||
    <div class="flex flex-row items-center bg-gray-300 rounded-lg">
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -3,7 +3,7 @@ import Card from '../components/Card.vue';
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    buttonText: string,
 | 
			
		||||
    buttonText?: string,
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { computed, ref } from "vue";
 | 
			
		||||
import Autocomplete from '../components/Autocomplete.vue'
 | 
			
		||||
import { Transaction, useTransactionsStore } from "../stores/transactions";
 | 
			
		||||
import DateInput from "./DateInput.vue";
 | 
			
		||||
import Button from "./Button.vue";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    budgetid: string
 | 
			
		||||
@@ -51,28 +52,31 @@ function saveTransaction(e: MouseEvent) {
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <tr>
 | 
			
		||||
        <td style="width: 90px;" class="text-sm">
 | 
			
		||||
        <label class="md:hidden">Date</label>
 | 
			
		||||
        <td class="text-sm">
 | 
			
		||||
            <DateInput class="border-b-2 border-black" v-model="TX.Date" />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td style="max-width: 150px;">
 | 
			
		||||
        <label class="md:hidden">Payee</label>
 | 
			
		||||
        <td>
 | 
			
		||||
            <Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td style="max-width: 200px;">
 | 
			
		||||
        <label class="md:hidden">Category</label>
 | 
			
		||||
        <td>
 | 
			
		||||
            <Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>
 | 
			
		||||
        <td class="col-span-2">
 | 
			
		||||
            <input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td style="width: 80px;" class="text-right">
 | 
			
		||||
        <label class="md:hidden">Amount</label>
 | 
			
		||||
        <td class="text-right">
 | 
			
		||||
            <input
 | 
			
		||||
                class="text-right block w-full border-b-2 border-black"
 | 
			
		||||
                type="currency"
 | 
			
		||||
                v-model="TX.Amount"
 | 
			
		||||
            />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td style="width: 20px;">
 | 
			
		||||
            <input type="submit" @click="saveTransaction" value="Save" />
 | 
			
		||||
        <td class="hidden md:table-cell">
 | 
			
		||||
            <Button class="bg-blue-500" @click="saveTransaction">Save</Button>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td style="width: 20px;"></td>
 | 
			
		||||
    </tr>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -5,6 +5,7 @@ import { useTransactionsStore } from "../stores/transactions";
 | 
			
		||||
import Currency from "./Currency.vue";
 | 
			
		||||
import TransactionEditRow from "./TransactionEditRow.vue";
 | 
			
		||||
import { formatDate } from "../date";
 | 
			
		||||
import { useAccountStore } from "../stores/budget-account";
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
    transactionid: string,
 | 
			
		||||
@@ -18,17 +19,43 @@ const Reconciling = computed(() => useTransactionsStore().Reconciling);
 | 
			
		||||
 | 
			
		||||
const transactionsStore = useTransactionsStore();
 | 
			
		||||
const TX = transactionsStore.Transactions.get(props.transactionid)!;
 | 
			
		||||
 | 
			
		||||
function dateChanged() {
 | 
			
		||||
    const currentAccount = useAccountStore().CurrentAccount;
 | 
			
		||||
    if (currentAccount == null)
 | 
			
		||||
        return true;
 | 
			
		||||
    const transactionIndex = currentAccount.Transactions.indexOf(props.transactionid);
 | 
			
		||||
    if(transactionIndex<=0)
 | 
			
		||||
        return true;
 | 
			
		||||
 | 
			
		||||
    const previousTransactionId = currentAccount.Transactions[transactionIndex-1];
 | 
			
		||||
    const previousTransaction = transactionsStore.Transactions.get(previousTransactionId);
 | 
			
		||||
    return TX.Date.getTime() != previousTransaction?.Date.getTime();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getStatusSymbol() {
 | 
			
		||||
    if(TX.Status == "Reconciled")
 | 
			
		||||
        return "✔";
 | 
			
		||||
 | 
			
		||||
    if(TX.Status == "Uncleared")
 | 
			
		||||
        return "*";
 | 
			
		||||
 | 
			
		||||
    return "✘";
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <tr v-if="dateChanged()" class="table-row md:hidden">
 | 
			
		||||
        <td class="bg-gray-200 rounded-lg p-2" colspan="5">{{ formatDate(TX.Date) }}</td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr
 | 
			
		||||
        v-if="!edit"
 | 
			
		||||
        class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
 | 
			
		||||
        :class="[index % 6 < 3 ? 'bg-gray-300' : 'bg-gray-100']"
 | 
			
		||||
        :class="[index % 6 < 3 ? 'md:bg-gray-300' : 'md:bg-gray-100']"
 | 
			
		||||
    >
 | 
			
		||||
        <!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
 | 
			
		||||
        <td>{{ formatDate(TX.Date) }}</td>
 | 
			
		||||
        <td>{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}</td>
 | 
			
		||||
        <td class="hidden md:block">{{ formatDate(TX.Date) }}</td>
 | 
			
		||||
        <td class="pl-2 md:pl-0">{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}</td>
 | 
			
		||||
        <td>{{ TX.CategoryGroup ? TX.CategoryGroup + " : " + TX.Category : "" }}</td>
 | 
			
		||||
        <td>
 | 
			
		||||
            <a
 | 
			
		||||
@@ -38,13 +65,11 @@ const TX = transactionsStore.Transactions.get(props.transactionid)!;
 | 
			
		||||
        <td>
 | 
			
		||||
            <Currency class="block" :value="TX.Amount" />
 | 
			
		||||
        </td>
 | 
			
		||||
        <td>{{ TX.Status == "Reconciled" ? "✔" : (TX.Status == "Uncleared" ? "" : "*") }}</td>
 | 
			
		||||
        <td class="text-right">
 | 
			
		||||
            {{ TX.GroupID ? "☀" : "" }}
 | 
			
		||||
            {{ getStatusSymbol() }}
 | 
			
		||||
            <a @click="edit = true;">✎</a>
 | 
			
		||||
        </td>
 | 
			
		||||
        <td v-if="Reconciling && TX.Status != 'Reconciled'">
 | 
			
		||||
            <input type="checkbox" v-model="TX.Reconciled" />
 | 
			
		||||
            <input v-if="Reconciling && TX.Status != 'Reconciled'" type="checkbox" v-model="TX.Reconciled" />
 | 
			
		||||
        </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <TransactionEditRow v-if="edit" :transactionid="TX.ID" @save="edit = false" />
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import { useAccountStore } from "../stores/budget-account";
 | 
			
		||||
import EditAccount from "../dialogs/EditAccount.vue";
 | 
			
		||||
import Button from "../components/Button.vue";
 | 
			
		||||
import { useTransactionsStore } from "../stores/transactions";
 | 
			
		||||
import Modal from "../components/Modal.vue";
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
    budgetid: string
 | 
			
		||||
@@ -47,18 +48,18 @@ function createReconcilationTransaction() {
 | 
			
		||||
        </h1>
 | 
			
		||||
 | 
			
		||||
        <div class="text-right">
 | 
			
		||||
            <span class="border-2 rounded-lg p-1">
 | 
			
		||||
            <span class="border-2 rounded-lg p-1 whitespace-nowrap">
 | 
			
		||||
                Working:
 | 
			
		||||
                <Currency :value="accounts.CurrentAccount?.WorkingBalance" />
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <span class="border-2 rounded-lg p-1 ml-2">
 | 
			
		||||
            <span class="border-2 rounded-lg p-1 ml-2 whitespace-nowrap">
 | 
			
		||||
                Cleared:
 | 
			
		||||
                <Currency :value="accounts.CurrentAccount?.ClearedBalance" />
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <span
 | 
			
		||||
                class="border-2 border-blue-500 rounded-lg bg-blue-500 ml-2 p-1"
 | 
			
		||||
                class="border-2 border-blue-500 rounded-lg bg-blue-500 ml-2 p-1 whitespace-nowrap"
 | 
			
		||||
                v-if="!transactions.Reconciling"
 | 
			
		||||
                @click="transactions.Reconciling = true"
 | 
			
		||||
            >
 | 
			
		||||
@@ -70,34 +71,32 @@ function createReconcilationTransaction() {
 | 
			
		||||
        <span v-if="transactions.Reconciling" class="border-2 block bg-gray-200 rounded-lg p-2">
 | 
			
		||||
            Is
 | 
			
		||||
            <Currency :value="transactions.ReconcilingBalance" />your current balance?
 | 
			
		||||
            <Button class="bg-blue-500 mx-3" @click="submitReconcilation">Yes!</Button>
 | 
			
		||||
            <Button class="bg-blue-500 mx-3 py-2" @click="submitReconcilation">Yes!</Button>
 | 
			
		||||
            <br />No, it's:
 | 
			
		||||
            <input class="text-right" type="number" v-model="TargetReconcilingBalance" />
 | 
			
		||||
            Difference:
 | 
			
		||||
            <Currency :value="transactions.ReconcilingBalance - TargetReconcilingBalance" />
 | 
			
		||||
            <Button
 | 
			
		||||
                class="bg-orange-500 mx-3"
 | 
			
		||||
                class="bg-orange-500 mx-3 py-2"
 | 
			
		||||
                v-if="Math.abs(transactions.ReconcilingBalance - TargetReconcilingBalance) > 0.01"
 | 
			
		||||
                @click="createReconcilationTransaction"
 | 
			
		||||
            >Create reconciling Transaction</Button>
 | 
			
		||||
            <Button class="bg-red-500 mx-3" @click="cancelReconcilation">Cancel</Button>
 | 
			
		||||
            <Button class="bg-red-500 mx-3 py-2" @click="cancelReconcilation">Cancel</Button>
 | 
			
		||||
        </span>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <table>
 | 
			
		||||
        <tr class="font-bold">
 | 
			
		||||
            <td style="width: 90px;">Date</td>
 | 
			
		||||
            <td class="hidden md:block" style="width: 90px;">Date</td>
 | 
			
		||||
            <td style="max-width: 150px;">Payee</td>
 | 
			
		||||
            <td style="max-width: 200px;">Category</td>
 | 
			
		||||
            <td>Memo</td>
 | 
			
		||||
            <td class="text-right">Amount</td>
 | 
			
		||||
            <td style="width: 20px;"></td>
 | 
			
		||||
            <td style="width: 40px;"></td>
 | 
			
		||||
            <td style="width: 20px;" v-if="transactions.Reconciling">
 | 
			
		||||
                <input type="checkbox" @input="setReconciled" />
 | 
			
		||||
            <td style="width: 80px;">
 | 
			
		||||
                <input v-if="transactions.Reconciling" type="checkbox" @input="setReconciled" />
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
 | 
			
		||||
        <TransactionInputRow class="hidden md:table-row" :budgetid="budgetid" :accountid="accountid" />
 | 
			
		||||
        <TransactionRow
 | 
			
		||||
            v-for="(transaction, index) in transactions.TransactionsList"
 | 
			
		||||
            :key="transaction.ID"
 | 
			
		||||
@@ -105,6 +104,14 @@ function createReconcilationTransaction() {
 | 
			
		||||
            :index="index"
 | 
			
		||||
        />
 | 
			
		||||
    </table>
 | 
			
		||||
    <div class="md:hidden">
 | 
			
		||||
        <Modal>
 | 
			
		||||
            <template v-slot:placeholder>
 | 
			
		||||
                <Button class="fixed right-4 bottom-4 font-bold text-lg bg-blue-500 py-2">+</Button>
 | 
			
		||||
            </template>
 | 
			
		||||
            <TransactionInputRow class="grid grid-cols-2" :budgetid="budgetid" :accountid="accountid" />
 | 
			
		||||
        </Modal>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID);
 | 
			
		||||
const accountStore = useAccountStore();
 | 
			
		||||
const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
 | 
			
		||||
 | 
			
		||||
function GetCategories(group : string) {
 | 
			
		||||
function GetCategories(group: string) {
 | 
			
		||||
    return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -28,20 +28,20 @@ const GroupsForMonth = computed(() => {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const previous = computed(() => ({
 | 
			
		||||
        Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
 | 
			
		||||
        Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
 | 
			
		||||
    Year: new Date(selected.value.Year, selected.value.Month - 1, 1).getFullYear(),
 | 
			
		||||
    Month: new Date(selected.value.Year, selected.value.Month - 1, 1).getMonth(),
 | 
			
		||||
}));
 | 
			
		||||
const current = computed(() => ({
 | 
			
		||||
        Year: new Date().getFullYear(),
 | 
			
		||||
        Month: new Date().getMonth(),
 | 
			
		||||
    Year: new Date().getFullYear(),
 | 
			
		||||
    Month: new Date().getMonth(),
 | 
			
		||||
}));
 | 
			
		||||
const selected = computed(() => ({
 | 
			
		||||
        Year: Number(props.year) ?? current.value.Year,
 | 
			
		||||
        Month: Number(props.month ?? current.value.Month)
 | 
			
		||||
    Year: Number(props.year) ?? current.value.Year,
 | 
			
		||||
    Month: Number(props.month ?? current.value.Month)
 | 
			
		||||
}));
 | 
			
		||||
const next = computed(() => ({
 | 
			
		||||
        Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
 | 
			
		||||
        Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
 | 
			
		||||
    Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
 | 
			
		||||
    Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
@@ -56,12 +56,12 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
const expandedGroups = ref<Map<string, boolean>>(new Map<string, boolean>())
 | 
			
		||||
 | 
			
		||||
function toggleGroup(group : {Name : string, Expand: boolean}) {
 | 
			
		||||
function toggleGroup(group: { Name: string, Expand: boolean }) {
 | 
			
		||||
    console.log(expandedGroups.value);
 | 
			
		||||
    expandedGroups.value.set(group.Name, !(expandedGroups.value.get(group.Name) ?? group.Expand))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
 | 
			
		||||
function getGroupState(group: { Name: string, Expand: boolean }): boolean {
 | 
			
		||||
    return expandedGroups.value.get(group.Name) ?? group.Expand;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@@ -71,43 +71,32 @@ function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
 | 
			
		||||
    <div>
 | 
			
		||||
        <router-link
 | 
			
		||||
            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
 | 
			
		||||
        >Previous Month</router-link>-
 | 
			
		||||
        ><<</router-link> 
 | 
			
		||||
        <router-link
 | 
			
		||||
            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
 | 
			
		||||
        >Current Month</router-link>-
 | 
			
		||||
        >Current Month</router-link> 
 | 
			
		||||
        <router-link
 | 
			
		||||
            :to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
 | 
			
		||||
        >Next Month</router-link>
 | 
			
		||||
        >>></router-link>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" id="content">
 | 
			
		||||
        <span class="hidden sm:block"></span>
 | 
			
		||||
        <span class="hidden lg:block text-right">Leftover</span>
 | 
			
		||||
        <span class="hidden sm:block text-right">Assigned</span>
 | 
			
		||||
        <span class="hidden sm:block text-right">Activity</span>
 | 
			
		||||
        <span class="hidden sm:block text-right">Available</span>
 | 
			
		||||
        <template v-for="group in GroupsForMonth">
 | 
			
		||||
            <a
 | 
			
		||||
                class="text-lg font-bold col-span-2 sm:col-span-4 lg:col-span-5"
 | 
			
		||||
                @click="toggleGroup(group)"
 | 
			
		||||
            >{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</a>
 | 
			
		||||
            <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" />
 | 
			
		||||
                <Currency :value="category.Activity" class="hidden sm:block" />
 | 
			
		||||
                <Currency :value="category.Available" />
 | 
			
		||||
            </template>
 | 
			
		||||
        </template>
 | 
			
		||||
    </div>
 | 
			
		||||
    <table class="container col-lg-12" id="content">
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th>Category</th>
 | 
			
		||||
            <th></th>
 | 
			
		||||
            <th></th>
 | 
			
		||||
            <th>Leftover</th>
 | 
			
		||||
            <th>Assigned</th>
 | 
			
		||||
            <th>Activity</th>
 | 
			
		||||
            <th>Available</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
        <tbody v-for="group in GroupsForMonth">
 | 
			
		||||
            <a class="text-lg font-bold" @click="toggleGroup(group)">{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</a>
 | 
			
		||||
            <tr v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
 | 
			
		||||
                <td>{{ category.Name }}</td>
 | 
			
		||||
                <td></td>
 | 
			
		||||
                <td></td>
 | 
			
		||||
                <td class="text-right">
 | 
			
		||||
                    <Currency :value="category.AvailableLastMonth" />
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right">
 | 
			
		||||
                    <Currency :value="category.Assigned" />
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right">
 | 
			
		||||
                    <Currency :value="category.Activity" />
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="text-right">
 | 
			
		||||
                    <Currency :value="category.Available" />
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -82,42 +82,40 @@ function ynabExport() {
 | 
			
		||||
                <h2 class="text-lg font-bold">Clear Budget</h2>
 | 
			
		||||
                <p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
 | 
			
		||||
 | 
			
		||||
                <Button class="bg-red-500" @click="clearBudget">Clear budget</Button>
 | 
			
		||||
                <Button class="bg-red-500 py-2" @click="clearBudget">Clear budget</Button>
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Card class="flex-col p-3">
 | 
			
		||||
                <h2 class="text-lg font-bold">Delete Budget</h2>
 | 
			
		||||
                <p>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</p>
 | 
			
		||||
                <Button class="bg-red-500" @click="deleteBudget">Delete budget</button>
 | 
			
		||||
                <Button class="bg-red-500 py-2" @click="deleteBudget">Delete budget</button>
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Card class="flex-col p-3">
 | 
			
		||||
                <h2 class="text-lg font-bold">Fix all historic negative category-balances</h2>
 | 
			
		||||
                <p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
 | 
			
		||||
                <Button class="bg-orange-500" @click="cleanNegative">Fix negative</button>
 | 
			
		||||
                <Button class="bg-orange-500 py-2" @click="cleanNegative">Fix negative</button>
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Card class="flex-col p-3">
 | 
			
		||||
                <h2 class="text-lg font-bold">Import YNAB Budget</h2>
 | 
			
		||||
                
 | 
			
		||||
                <div class="flex flex-row">
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <label for="transactions_file">
 | 
			
		||||
                            Transaktionen:
 | 
			
		||||
                            <input type="file" @change="gotTransactions" accept="text/*" />
 | 
			
		||||
                        </label>
 | 
			
		||||
                        <br />
 | 
			
		||||
                        <label for="assignments_file">
 | 
			
		||||
                            Budget:
 | 
			
		||||
                            <input type="file" @change="gotAssignments" accept="text/*" />
 | 
			
		||||
                        </label>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <label for="transactions_file">
 | 
			
		||||
                        Transaktionen:
 | 
			
		||||
                        <input type="file" @change="gotTransactions" accept="text/*" />
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <br />
 | 
			
		||||
                    <label for="assignments_file">
 | 
			
		||||
                        Budget:
 | 
			
		||||
                        <input type="file" @change="gotAssignments" accept="text/*" />
 | 
			
		||||
                    </label>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <Button class="bg-blue-500 py-2" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Card class="flex-col p-3">
 | 
			
		||||
                <h2 class="text-lg font-bold">Export as YNAB TSV</h2>
 | 
			
		||||
 | 
			
		||||
                <div class="flex flex-row">
 | 
			
		||||
                    <Button class="bg-blue-500" @click="ynabExport">Export</Button>
 | 
			
		||||
                    <Button class="bg-blue-500 py-2" @click="ynabExport">Export</Button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </Card>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user