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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user