Merge pull request 'Improve mobile usability' (#30) from mobile-usability into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
This commit is contained in:
commit
e8a0670a83
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -29,11 +30,14 @@ func main() {
|
|||||||
panic("couldn't open static files")
|
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{
|
handler := &server.Handler{
|
||||||
Service: queries,
|
Service: queries,
|
||||||
TokenVerifier: &jwt.TokenVerifier{
|
TokenVerifier: tokenVerifier,
|
||||||
Secret: cfg.SessionSecret,
|
|
||||||
},
|
|
||||||
CredentialsVerifier: &bcrypt.Verifier{},
|
CredentialsVerifier: &bcrypt.Verifier{},
|
||||||
StaticFS: http.FS(static),
|
StaticFS: http.FS(static),
|
||||||
}
|
}
|
||||||
|
26
jwt/login.go
26
jwt/login.go
@ -12,7 +12,19 @@ import (
|
|||||||
|
|
||||||
// TokenVerifier verifies Tokens.
|
// TokenVerifier verifies Tokens.
|
||||||
type TokenVerifier struct {
|
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.
|
// Token contains everything to authenticate a user.
|
||||||
@ -29,6 +41,10 @@ const (
|
|||||||
|
|
||||||
// CreateToken creates a new token from username and name.
|
// CreateToken creates a new token from username and name.
|
||||||
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
||||||
|
if tv.secret == "" {
|
||||||
|
return "", ErrEmptySecret
|
||||||
|
}
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
"usr": user.Email,
|
"usr": user.Email,
|
||||||
"name": user.Name,
|
"name": user.Name,
|
||||||
@ -37,7 +53,7 @@ func (tv *TokenVerifier) CreateToken(user *postgres.User) (string, error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Generate encoded token and send it as response.
|
// 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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("create token: %w", err)
|
return "", fmt.Errorf("create token: %w", err)
|
||||||
}
|
}
|
||||||
@ -53,11 +69,15 @@ var (
|
|||||||
|
|
||||||
// VerifyToken verifys a given string-token.
|
// VerifyToken verifys a given string-token.
|
||||||
func (tv *TokenVerifier) VerifyToken(tokenString string) (budgeteer.Token, error) { //nolint:ireturn
|
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) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
|
return nil, fmt.Errorf("method '%v': %w", token.Header["alg"], ErrUnexpectedSigningMethod)
|
||||||
}
|
}
|
||||||
return []byte(tv.Secret), nil
|
return []byte(tv.secret), nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse jwt: %w", err)
|
return nil, fmt.Errorf("parse jwt: %w", err)
|
||||||
|
@ -28,11 +28,10 @@ func TestRegisterUser(t *testing.T) { //nolint:funlen
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenVerifier, _ := jwt.NewTokenVerifier("this_is_my_demo_secret_for_unit_tests")
|
||||||
h := Handler{
|
h := Handler{
|
||||||
Service: database,
|
Service: database,
|
||||||
TokenVerifier: &jwt.TokenVerifier{
|
TokenVerifier: tokenVerifier,
|
||||||
Secret: "this_is_my_demo_secret_for_unit_tests",
|
|
||||||
},
|
|
||||||
CredentialsVerifier: &bcrypt.Verifier{},
|
CredentialsVerifier: &bcrypt.Verifier{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<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>
|
<slot></slot>
|
||||||
</button>
|
</button>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
@ -3,7 +3,7 @@ import Card from '../components/Card.vue';
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
buttonText: string,
|
buttonText?: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -3,6 +3,7 @@ import { computed, ref } from "vue";
|
|||||||
import Autocomplete from '../components/Autocomplete.vue'
|
import Autocomplete from '../components/Autocomplete.vue'
|
||||||
import { Transaction, useTransactionsStore } from "../stores/transactions";
|
import { Transaction, useTransactionsStore } from "../stores/transactions";
|
||||||
import DateInput from "./DateInput.vue";
|
import DateInput from "./DateInput.vue";
|
||||||
|
import Button from "./Button.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
budgetid: string
|
budgetid: string
|
||||||
@ -51,28 +52,31 @@ function saveTransaction(e: MouseEvent) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr>
|
<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" />
|
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
|
||||||
</td>
|
</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" />
|
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
|
||||||
</td>
|
</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" />
|
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="col-span-2">
|
||||||
<input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
|
<input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 80px;" class="text-right">
|
<label class="md:hidden">Amount</label>
|
||||||
|
<td class="text-right">
|
||||||
<input
|
<input
|
||||||
class="text-right block w-full border-b-2 border-black"
|
class="text-right block w-full border-b-2 border-black"
|
||||||
type="currency"
|
type="currency"
|
||||||
v-model="TX.Amount"
|
v-model="TX.Amount"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 20px;">
|
<td class="hidden md:table-cell">
|
||||||
<input type="submit" @click="saveTransaction" value="Save" />
|
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 20px;"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
@ -5,6 +5,7 @@ import { useTransactionsStore } from "../stores/transactions";
|
|||||||
import Currency from "./Currency.vue";
|
import Currency from "./Currency.vue";
|
||||||
import TransactionEditRow from "./TransactionEditRow.vue";
|
import TransactionEditRow from "./TransactionEditRow.vue";
|
||||||
import { formatDate } from "../date";
|
import { formatDate } from "../date";
|
||||||
|
import { useAccountStore } from "../stores/budget-account";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
transactionid: string,
|
transactionid: string,
|
||||||
@ -18,17 +19,43 @@ const Reconciling = computed(() => useTransactionsStore().Reconciling);
|
|||||||
|
|
||||||
const transactionsStore = useTransactionsStore();
|
const transactionsStore = useTransactionsStore();
|
||||||
const TX = transactionsStore.Transactions.get(props.transactionid)!;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
<tr
|
||||||
v-if="!edit"
|
v-if="!edit"
|
||||||
class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
|
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' : '']">-->
|
<!--: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 class="hidden md:block">{{ formatDate(TX.Date) }}</td>
|
||||||
<td>{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}</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>{{ TX.CategoryGroup ? TX.CategoryGroup + " : " + TX.Category : "" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
@ -38,13 +65,11 @@ const TX = transactionsStore.Transactions.get(props.transactionid)!;
|
|||||||
<td>
|
<td>
|
||||||
<Currency class="block" :value="TX.Amount" />
|
<Currency class="block" :value="TX.Amount" />
|
||||||
</td>
|
</td>
|
||||||
<td>{{ TX.Status == "Reconciled" ? "✔" : (TX.Status == "Uncleared" ? "" : "*") }}</td>
|
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{{ TX.GroupID ? "☀" : "" }}
|
{{ TX.GroupID ? "☀" : "" }}
|
||||||
|
{{ getStatusSymbol() }}
|
||||||
<a @click="edit = true;">✎</a>
|
<a @click="edit = true;">✎</a>
|
||||||
</td>
|
<input v-if="Reconciling && TX.Status != 'Reconciled'" type="checkbox" v-model="TX.Reconciled" />
|
||||||
<td v-if="Reconciling && TX.Status != 'Reconciled'">
|
|
||||||
<input type="checkbox" v-model="TX.Reconciled" />
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<TransactionEditRow v-if="edit" :transactionid="TX.ID" @save="edit = false" />
|
<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 EditAccount from "../dialogs/EditAccount.vue";
|
||||||
import Button from "../components/Button.vue";
|
import Button from "../components/Button.vue";
|
||||||
import { useTransactionsStore } from "../stores/transactions";
|
import { useTransactionsStore } from "../stores/transactions";
|
||||||
|
import Modal from "../components/Modal.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
budgetid: string
|
budgetid: string
|
||||||
@ -47,18 +48,18 @@ function createReconcilationTransaction() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<span class="border-2 rounded-lg p-1">
|
<span class="border-2 rounded-lg p-1 whitespace-nowrap">
|
||||||
Working:
|
Working:
|
||||||
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
|
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="border-2 rounded-lg p-1 ml-2">
|
<span class="border-2 rounded-lg p-1 ml-2 whitespace-nowrap">
|
||||||
Cleared:
|
Cleared:
|
||||||
<Currency :value="accounts.CurrentAccount?.ClearedBalance" />
|
<Currency :value="accounts.CurrentAccount?.ClearedBalance" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<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"
|
v-if="!transactions.Reconciling"
|
||||||
@click="transactions.Reconciling = true"
|
@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">
|
<span v-if="transactions.Reconciling" class="border-2 block bg-gray-200 rounded-lg p-2">
|
||||||
Is
|
Is
|
||||||
<Currency :value="transactions.ReconcilingBalance" />your current balance?
|
<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:
|
<br />No, it's:
|
||||||
<input class="text-right" type="number" v-model="TargetReconcilingBalance" />
|
<input class="text-right" type="number" v-model="TargetReconcilingBalance" />
|
||||||
Difference:
|
Difference:
|
||||||
<Currency :value="transactions.ReconcilingBalance - TargetReconcilingBalance" />
|
<Currency :value="transactions.ReconcilingBalance - TargetReconcilingBalance" />
|
||||||
<Button
|
<Button
|
||||||
class="bg-orange-500 mx-3"
|
class="bg-orange-500 mx-3 py-2"
|
||||||
v-if="Math.abs(transactions.ReconcilingBalance - TargetReconcilingBalance) > 0.01"
|
v-if="Math.abs(transactions.ReconcilingBalance - TargetReconcilingBalance) > 0.01"
|
||||||
@click="createReconcilationTransaction"
|
@click="createReconcilationTransaction"
|
||||||
>Create reconciling Transaction</Button>
|
>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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr class="font-bold">
|
<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: 150px;">Payee</td>
|
||||||
<td style="max-width: 200px;">Category</td>
|
<td style="max-width: 200px;">Category</td>
|
||||||
<td>Memo</td>
|
<td>Memo</td>
|
||||||
<td class="text-right">Amount</td>
|
<td class="text-right">Amount</td>
|
||||||
<td style="width: 20px;"></td>
|
<td style="width: 80px;">
|
||||||
<td style="width: 40px;"></td>
|
<input v-if="transactions.Reconciling" type="checkbox" @input="setReconciled" />
|
||||||
<td style="width: 20px;" v-if="transactions.Reconciling">
|
|
||||||
<input type="checkbox" @input="setReconciled" />
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<TransactionInputRow :budgetid="budgetid" :accountid="accountid" />
|
<TransactionInputRow class="hidden md:table-row" :budgetid="budgetid" :accountid="accountid" />
|
||||||
<TransactionRow
|
<TransactionRow
|
||||||
v-for="(transaction, index) in transactions.TransactionsList"
|
v-for="(transaction, index) in transactions.TransactionsList"
|
||||||
:key="transaction.ID"
|
:key="transaction.ID"
|
||||||
@ -105,6 +104,14 @@ function createReconcilationTransaction() {
|
|||||||
:index="index"
|
:index="index"
|
||||||
/>
|
/>
|
||||||
</table>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -17,7 +17,7 @@ const CurrentBudgetID = computed(() => budgetsStore.CurrentBudgetID);
|
|||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
|
const categoriesForMonth = accountStore.CategoriesForMonthAndGroup;
|
||||||
|
|
||||||
function GetCategories(group : string) {
|
function GetCategories(group: string) {
|
||||||
return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
|
return [...categoriesForMonth(selected.value.Year, selected.value.Month, group)];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,20 +28,20 @@ const GroupsForMonth = computed(() => {
|
|||||||
|
|
||||||
|
|
||||||
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(),
|
||||||
}));
|
}));
|
||||||
const current = computed(() => ({
|
const current = computed(() => ({
|
||||||
Year: new Date().getFullYear(),
|
Year: new Date().getFullYear(),
|
||||||
Month: new Date().getMonth(),
|
Month: new Date().getMonth(),
|
||||||
}));
|
}));
|
||||||
const selected = computed(() => ({
|
const selected = computed(() => ({
|
||||||
Year: Number(props.year) ?? current.value.Year,
|
Year: Number(props.year) ?? current.value.Year,
|
||||||
Month: Number(props.month ?? current.value.Month)
|
Month: Number(props.month ?? current.value.Month)
|
||||||
}));
|
}));
|
||||||
const next = computed(() => ({
|
const next = computed(() => ({
|
||||||
Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
|
Year: new Date(selected.value.Year, Number(props.month) + 1, 1).getFullYear(),
|
||||||
Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
|
Month: new Date(selected.value.Year, Number(props.month) + 1, 1).getMonth(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@ -56,12 +56,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
const expandedGroups = ref<Map<string, boolean>>(new Map<string, boolean>())
|
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);
|
console.log(expandedGroups.value);
|
||||||
expandedGroups.value.set(group.Name, !(expandedGroups.value.get(group.Name) ?? group.Expand))
|
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;
|
return expandedGroups.value.get(group.Name) ?? group.Expand;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -71,43 +71,32 @@ function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
|
|||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
||||||
>Previous Month</router-link>-
|
><<</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
|
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
|
||||||
>Current Month</router-link>-
|
>Current Month</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
|
: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>
|
</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>
|
</template>
|
||||||
|
@ -82,42 +82,40 @@ function ynabExport() {
|
|||||||
<h2 class="text-lg font-bold">Clear Budget</h2>
|
<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>
|
<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>
|
||||||
<Card class="flex-col p-3">
|
<Card class="flex-col p-3">
|
||||||
<h2 class="text-lg font-bold">Delete Budget</h2>
|
<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>
|
<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>
|
||||||
<Card class="flex-col p-3">
|
<Card class="flex-col p-3">
|
||||||
<h2 class="text-lg font-bold">Fix all historic negative category-balances</h2>
|
<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>
|
<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>
|
||||||
<Card class="flex-col p-3">
|
<Card class="flex-col p-3">
|
||||||
<h2 class="text-lg font-bold">Import YNAB Budget</h2>
|
<h2 class="text-lg font-bold">Import YNAB Budget</h2>
|
||||||
|
|
||||||
<div class="flex flex-row">
|
<div>
|
||||||
<div>
|
<label for="transactions_file">
|
||||||
<label for="transactions_file">
|
Transaktionen:
|
||||||
Transaktionen:
|
<input type="file" @change="gotTransactions" accept="text/*" />
|
||||||
<input type="file" @change="gotTransactions" accept="text/*" />
|
</label>
|
||||||
</label>
|
<br />
|
||||||
<br />
|
<label for="assignments_file">
|
||||||
<label for="assignments_file">
|
Budget:
|
||||||
Budget:
|
<input type="file" @change="gotAssignments" accept="text/*" />
|
||||||
<input type="file" @change="gotAssignments" accept="text/*" />
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button class="bg-blue-500 py-2" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
|
||||||
</Card>
|
</Card>
|
||||||
<Card class="flex-col p-3">
|
<Card class="flex-col p-3">
|
||||||
<h2 class="text-lg font-bold">Export as YNAB TSV</h2>
|
<h2 class="text-lg font-bold">Export as YNAB TSV</h2>
|
||||||
|
|
||||||
<div class="flex flex-row">
|
<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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user