Improve mobile usability #30

Merged
jacob1123 merged 14 commits from mobile-usability into master 2022-03-01 21:33:07 +01:00
11 changed files with 150 additions and 104 deletions

View File

@ -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),
}

View File

@ -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)

View File

@ -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{},
}

View File

@ -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>

View File

@ -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>

View File

@ -3,7 +3,7 @@ import Card from '../components/Card.vue';
import { ref } from "vue";
const props = defineProps<{
buttonText: string,
buttonText?: string,
}>();
const emit = defineEmits<{

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>-
>&lt;&lt;</router-link>&nbsp;
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
>Current Month</router-link>-
>Current Month</router-link>&nbsp;
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
>Next Month</router-link>
>&gt;&gt;</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>

View File

@ -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>