Run prettier
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
Jan Bader 2022-03-15 12:49:53 +00:00
parent d8d713f841
commit 61a534610f
42 changed files with 1485 additions and 1187 deletions

View File

@ -2,16 +2,16 @@ module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
'plugin:vue/vue3-recommended',
"plugin:vue/vue3-recommended",
// 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
},
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module"
}
}
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
sourceType: "module",
},
};

View File

@ -6,7 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
<body
class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@ -30,18 +30,28 @@ export default defineComponent({
<router-view name="sidebar"></router-view>
<div class="flex-1 overflow-auto">
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
<div
class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
<span
class="flex-1 font-bold text-5xl -my-3 hidden md:inline"
@click="toggleMenuSize"
></span>
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu"></span>
></span
>
<span
class="flex-1 font-bold text-5xl -my-3 md:hidden"
@click="toggleMenu"
></span
>
<span class="flex-1">{{ CurrentBudgetName }}</span>
<div class="flex flex-1 flex-row justify-end -mx-4">
<router-link class="mx-4" v-if="LoggedIn" to="/dashboard">Dashboard</router-link>
<router-link class="mx-4" v-if="!LoggedIn" to="/login">Login</router-link>
<router-link class="mx-4" v-if="LoggedIn" to="/dashboard"
>Dashboard</router-link
>
<router-link class="mx-4" v-if="!LoggedIn" to="/login"
>Login</router-link
>
<a class="mx-4" v-if="LoggedIn" @click="logout">Logout</a>
</div>
</div>

View File

@ -1,13 +1,13 @@
import { useSessionStore } from "./stores/session";
export const BASE_URL = "/api/v1"
export const BASE_URL = "/api/v1";
export function GET(path: string) {
const sessionStore = useSessionStore();
return fetch(BASE_URL + path, {
headers: sessionStore.AuthHeaders,
})
};
});
}
export function POST(path: string, body: FormData | string | null) {
const sessionStore = useSessionStore();
@ -15,12 +15,12 @@ export function POST(path: string, body: FormData | string | null) {
method: "POST",
headers: sessionStore.AuthHeaders,
body: body,
})
});
}
export function DELETE(path: string) {
const sessionStore = useSessionStore();
return fetch(BASE_URL + path, {
method: "DELETE",
headers: sessionStore.AuthHeaders,
})
});
}

View File

@ -24,7 +24,9 @@ function daysSinceLastReconciled() {
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID">
{{account.Name}}
</router-link>
<span v-if="props.account.LastReconciled.Valid && daysSinceLastReconciled() > 7" class="font-bold bg-gray-500 rounded-md text-sm px-2 mx-2 py-1 no-underline">
<span
v-if="props.account.LastReconciled.Valid && daysSinceLastReconciled() > 7"
class="font-bold bg-gray-500 rounded-md text-sm px-2 mx-2 py-1 no-underline">
{{daysSinceLastReconciled()}}
</span>
</span>

View File

@ -87,16 +87,23 @@ function clear() {
class="border-b-2 border-black"
@keypress="keypress"
v-if="id == undefined"
v-model="SearchQuery"
/>
<span @click="clear" v-if="id != undefined" class="bg-gray-300 dark:bg-gray-700">{{ text }}</span>
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 dark:bg-gray-600 w-64 p-2">
v-model="SearchQuery" />
<span
@click="clear"
v-if="id != undefined"
class="bg-gray-300 dark:bg-gray-700"
>{{ text }}</span
>
<div
v-if="Suggestions.length > 0"
class="absolute bg-gray-400 dark:bg-gray-600 w-64 p-2">
<span
v-for="suggestion in Suggestions"
class="block"
@click="select"
:value="suggestion.ID"
>{{ suggestion.Name }}</span>
>{{ suggestion.Name }}</span
>
</div>
</div>
</template>

View File

@ -1,10 +1,7 @@
<script lang="ts" setup>
</script>
<script lang="ts" setup></script>
<template>
<button
class="px-4 rounded-md shadow-sm focus:outline-none focus:ring-2"
>
<button class="px-4 rounded-md shadow-sm focus:outline-none focus:ring-2">
<slot></slot>
</button>
</template>

View File

@ -1,8 +1,8 @@
<script lang="ts" setup>
</script>
<script lang="ts" setup></script>
<template>
<div class="flex flex-row items-center bg-gray-300 dark:bg-gray-700 rounded-lg">
<div
class="flex flex-row items-center bg-gray-300 dark:bg-gray-700 rounded-lg">
<slot></slot>
</div>
</template>

View File

@ -7,5 +7,5 @@ const props = defineProps(["modelValue"]);
type="checkbox"
:checked="modelValue"
@change="$emit('update:modelValue', ($event.target as HTMLInputElement)?.checked)"
class="dark:bg-slate-900">
class="dark:bg-slate-900" />
</template>

View File

@ -15,5 +15,9 @@ const formattedValue = computed(() => internalValue.value.toLocaleString(undefin
</script>
<template>
<span class="text-right" :class="internalValue < 0 ? (negativeClass ?? 'negative') : positiveClass">{{ formattedValue }} </span>
<span
class="text-right"
:class="internalValue < 0 ? (negativeClass ?? 'negative') : positiveClass"
>{{ formattedValue }} </span
>
</template>

View File

@ -31,6 +31,5 @@ function selectAll(event: FocusEvent) {
ref="input"
v-bind:value="dateToYYYYMMDD(modelValue)"
@input="updateValue"
@focus="selectAll"
/>
@focus="selectAll" />
</template>

View File

@ -6,5 +6,5 @@ const props = defineProps(["modelValue"]);
<input
:value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement)?.value)"
class="dark:bg-slate-900">
class="dark:bg-slate-900" />
</template>

View File

@ -40,21 +40,26 @@ function submitDialog() {
</button>
<div
v-if="visible"
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
>
<div class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black">
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full">
<div
class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black">
<div class="mt-3 text-center">
<h3 class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{{ buttonText }}</h3>
<h3
class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
{{ buttonText }}
</h3>
<slot></slot>
<div class="grid grid-cols-2 gap-6">
<button
@click="closeDialog"
class="px-4 py-2 bg-red-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
>Close</button>
class="px-4 py-2 bg-red-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300">
Close
</button>
<button
@click="submitDialog"
class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
>Save</button>
class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300">
Save
</button>
</div>
</div>
</div>

View File

@ -43,20 +43,29 @@ function saveTransaction(e: MouseEvent) {
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
</td>
<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>
<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>
<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 class="text-right">
<Input
class="text-right block w-full border-b-2 border-black"
type="currency"
v-model="TX.Amount"
/>
v-model="TX.Amount" />
</td>
<td>
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>

View File

@ -59,22 +59,31 @@ function saveTransaction(e: MouseEvent) {
</td>
<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>
<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 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>
<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"
/>
v-model="TX.Amount" />
</td>
<td class="hidden md:table-cell">
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>

View File

@ -48,21 +48,27 @@ function getStatusSymbol() {
<template>
<tr v-if="dateChanged()" class="table-row md:hidden">
<td class="bg-gray-200 dark:bg-gray-800 rounded-lg p-2" colspan="5">{{ formatDate(TX.Date) }}</td>
<td class="bg-gray-200 dark:bg-gray-800 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 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']"
>
:class="[index % 6 < 3 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']">
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
<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 class="pl-2 md:pl-0">
{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}
</td>
<td>
{{ TX.CategoryGroup ? TX.CategoryGroup + " : " + TX.Category : "" }}
</td>
<td>
<a
:href="'/budget/' + CurrentBudgetID + '/transaction/' + TX.ID"
>{{ TX.Memo }}</a>
>{{ TX.Memo }}</a
>
</td>
<td>
<Currency class="block" :value="TX.Amount" />
@ -71,10 +77,15 @@ function getStatusSymbol() {
{{ TX.GroupID ? "☀" : "" }}
{{ getStatusSymbol() }}
<a @click="edit = true;"></a>
<Checkbox v-if="Reconciling && TX.Status != 'Reconciled'" v-model="TX.Reconciled" />
<Checkbox
v-if="Reconciling && TX.Status != 'Reconciled'"
v-model="TX.Reconciled" />
</td>
</tr>
<TransactionEditRow v-if="edit" :transactionid="TX.ID" @save="edit = false" />
<TransactionEditRow
v-if="edit"
:transactionid="TX.ID"
@save="edit = false" />
</template>
<style>

View File

@ -1,5 +1,6 @@
export function formatDate(date: Date): string {
return date.toLocaleDateString(undefined, { // you can use undefined as first argument
return date.toLocaleDateString(undefined, {
// you can use undefined as first argument
year: "numeric",
month: "2-digit",
day: "2-digit",

View File

@ -42,7 +42,10 @@ function openEditAccount(e : any) {
</script>
<template>
<Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
<Modal
button-text="Edit Account"
@open="openEditAccount"
@submit="editAccount">
<template v-slot:placeholder><span class="ml-2"></span></template>
<div class="mt-2 px-7 py-3">
<Input
@ -50,23 +53,14 @@ function openEditAccount(e : any) {
type="text"
v-model="accountName"
placeholder="Account name"
required
/>
required />
</div>
<div class="mt-2 px-7 py-3">
<Checkbox
class="border-2"
v-model="accountOnBudget"
required
/>
<Checkbox class="border-2" v-model="accountOnBudget" required />
<label>On Budget</label>
</div>
<div class="mt-2 px-7 py-3">
<Checkbox
class="border-2"
v-model="accountOpen"
required
/>
<Checkbox class="border-2" v-model="accountOpen" required />
<label>Open</label>
</div>
<div v-if="error != ''" class="dark:text-red-300 text-red-700">

View File

@ -13,7 +13,12 @@ function saveBudget() {
<template>
<Modal button-text="New Budget" @submit="saveBudget">
<div class="mt-2 px-7 py-3">
<Input class="border-2" type="text" v-model="budgetName" placeholder="Budget name" required />
<Input
class="border-2"
type="text"
v-model="budgetName"
placeholder="Budget name"
required />
</div>
</Modal>
</template>

View File

@ -1,32 +1,37 @@
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from './router'
import { createPinia } from 'pinia'
import { useBudgetsStore } from './stores/budget';
import { useAccountStore } from './stores/budget-account'
import PiniaLogger from './pinia-logger'
import { useSessionStore } from './stores/session'
import { createApp } from "vue";
import App from "./App.vue";
import "./index.css";
import router from "./router";
import { createPinia } from "pinia";
import { useBudgetsStore } from "./stores/budget";
import { useAccountStore } from "./stores/budget-account";
import PiniaLogger from "./pinia-logger";
import { useSessionStore } from "./stores/session";
const app = createApp(App)
app.use(router)
const app = createApp(App);
app.use(router);
const pinia = createPinia()
pinia.use(PiniaLogger({
const pinia = createPinia();
pinia.use(
PiniaLogger({
expanded: false,
showDuration: true
}))
app.use(pinia)
app.mount('#app')
showDuration: true,
})
);
app.use(pinia);
app.mount("#app");
router.beforeEach(async (to, from, next) => {
const budgetStore = useBudgetsStore();
await budgetStore.SetCurrentBudget((<string>to.params.budgetid));
await budgetStore.SetCurrentBudget(<string>to.params.budgetid);
const accountStore = useAccountStore();
await accountStore.SetCurrentAccount((<string>to.params.budgetid), (<string>to.params.accountid));
await accountStore.SetCurrentAccount(
<string>to.params.budgetid,
<string>to.params.accountid
);
next();
})
});
router.beforeEach((to, from, next) => {
const sessionStore = useSessionStore();
@ -35,20 +40,18 @@ router.beforeEach((to, from, next) => {
if (token != null) {
const jwt = parseJwt(token);
if (jwt.exp > Date.now() / 1000)
loggedIn = true;
if (jwt.exp > Date.now() / 1000) loggedIn = true;
}
if (to.matched.some(record => record.meta.requiresAuth)) {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!loggedIn) {
next({ path: '/login' });
next({ path: "/login" });
} else {
next();
}
} else if (to.matched.some(record => record.meta.hideForAuth)) {
} else if (to.matched.some((record) => record.meta.hideForAuth)) {
if (loggedIn) {
next({ path: '/dashboard' });
next({ path: "/dashboard" });
} else {
next();
}
@ -58,15 +61,19 @@ router.beforeEach((to, from, next) => {
});
function parseJwt(token: string) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
var base64Url = token.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
var jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
return JSON.parse(jsonPayload);
};
}
1646426130
1646512855755
1646426130;
1646512855755;

View File

@ -50,7 +50,8 @@ function createReconcilationTransaction() {
<EditAccount />
</div>
<div class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm">
<div
class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm">
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
Working:
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
@ -64,33 +65,36 @@ function createReconcilationTransaction() {
<span
class="rounded-lg bg-blue-500 p-1 whitespace-nowrap flex-1"
v-if="!transactions.Reconciling"
@click="transactions.Reconciling = true"
>
@click="transactions.Reconciling = true">
Reconciled:
<Currency :value="accounts.CurrentAccount?.ReconciledBalance" />
</span>
<span v-if="transactions.Reconciling" class="contents">
<Button @click="submitReconcilation"
<Button
@click="submitReconcilation"
class="bg-blue-500 p-1 whitespace-nowrap flex-1">
My current balance is&nbsp;
<Currency :value="transactions.ReconcilingBalance" />
</Button>
<Button @click="createReconcilationTransaction"
<Button
@click="createReconcilationTransaction"
class="bg-orange-500 p-1 whitespace-nowrap flex-1">
No, it's:
<Input
class="text-right w-20 bg-transparent dark:bg-transparent border-b-2"
type="number"
v-model="TargetReconcilingBalance"
/>
v-model="TargetReconcilingBalance" />
(Difference
<Currency
:value="transactions.ReconcilingBalance - TargetReconcilingBalance"
/>)
:value="transactions.ReconcilingBalance - TargetReconcilingBalance" />)
</Button>
<Button class="bg-red-500 p-1 flex-1" @click="cancelReconcilation">Cancel</Button>
<Button
class="bg-red-500 p-1 flex-1"
@click="cancelReconcilation"
>Cancel</Button
>
</span>
</div>
</div>
@ -103,31 +107,34 @@ function createReconcilationTransaction() {
<td>Memo</td>
<td class="text-right">Amount</td>
<td style="width: 80px;">
<Input v-if="transactions.Reconciling" type="checkbox" @input="setReconciled" />
<Input
v-if="transactions.Reconciling"
type="checkbox"
@input="setReconciled" />
</td>
</tr>
<TransactionInputRow
class="hidden md:table-row"
:budgetid="budgetid"
:accountid="accountid"
/>
:accountid="accountid" />
<TransactionRow
v-for="(transaction, index) in transactions.TransactionsList"
:key="transaction.ID"
:transactionid="transaction.ID"
:index="index"
/>
: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>
<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"
/>
:accountid="accountid" />
</Modal>
</div>
</template>

View File

@ -24,18 +24,20 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
<template>
<div
:class="[ExpandMenu ? 'md:w-72' : 'md:w-36', ShowMenu ? '' : 'hidden']"
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black"
>
class="md:block flex-shrink-0 w-full bg-gray-500 border-r-4 border-black">
<div class="flex flex-col mt-14 md:mt-0">
<span
class="m-2 p-1 px-3 h-10 overflow-hidden"
:class="[ExpandMenu ? 'text-2xl' : 'text-md']"
:class="[ExpandMenu ? 'text-2xl' : 'text-md']">
<router-link to="/dashboard" style="font-size:150%"
></router-link
>
<router-link to="/dashboard" style="font-size:150%"></router-link>
{{ CurrentBudgetName }}
</span>
<span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col">
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'"
>Budget</router-link
>
<br />
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
@ -43,21 +45,33 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold">
<span>On-Budget Accounts</span>
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" />
<Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="OnBudgetAccountsBalance" />
</div>
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
<div
v-for="account in OnBudgetAccounts"
class="flex flex-row justify-between">
<AccountWithReconciled :account="account" />
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
<Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="account.ClearedBalance" />
</div>
</li>
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold">
<span>Off-Budget Accounts</span>
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" />
<Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="OffBudgetAccountsBalance" />
</div>
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
<div
v-for="account in OffBudgetAccounts"
class="flex flex-row justify-between">
<AccountWithReconciled :account="account" />
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
<Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="account.ClearedBalance" />
</div>
</li>
<!--
@ -72,7 +86,9 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
</li>-->
<li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3">
<router-link :to="'/budget/' + CurrentBudgetID + '/settings'">Budget-Settings</router-link>
<router-link :to="'/budget/' + CurrentBudgetID + '/settings'"
>Budget-Settings</router-link
>
</li>
<!--<li><router-link to="/admin">Admin</router-link></li>-->
</div>

View File

@ -76,19 +76,28 @@ function assignedChanged(e : Event, category : Category){
<template>
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
<span>Available balance: <Currency :value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)" /></span>
<span
>Available balance:
<Currency
:value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)"
/></span>
<div>
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
>&lt;&lt;</router-link>&nbsp;
>&lt;&lt;</router-link
>&nbsp;
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
>Current Month</router-link>&nbsp;
>Current Month</router-link
>&nbsp;
<router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
>&gt;&gt;</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">
<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>
@ -98,17 +107,46 @@ function assignedChanged(e : Event, category : Category){
<span
class="text-lg font-bold mt-2"
@click="toggleGroup(group)"
>{{ (getGroupState(group) ? "" : "+") + " " + group.Name }}</span>
<Currency :value="group.AvailableLastMonth" class="hidden lg:block mt-2" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
<Currency :value="group.Assigned" class="hidden sm:block mx-2 mt-2 text-right" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
<Currency :value="group.Activity" class="hidden sm:block mt-2" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
<Currency :value="group.Available" class="mt-2" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
<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" />
<Input type="number" v-model="category.Assigned" @input="(evt) => assignedChanged(evt, category)" class="hidden sm:block mx-2 text-right" />
>{{ (getGroupState(group) ? "" : "+") + " " + group.Name }}</span
>
<Currency
:value="group.AvailableLastMonth"
class="hidden lg:block mt-2"
positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" />
<Currency
:value="group.Assigned"
class="hidden sm:block mx-2 mt-2 text-right"
positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" />
<Currency
:value="group.Activity"
class="hidden sm:block mt-2"
positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" />
<Currency
:value="group.Available"
class="mt-2"
positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" />
<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" />
<Input
type="number"
v-model="category.Assigned"
@input="(evt) => assignedChanged(evt, category)"
class="hidden sm:block mx-2 text-right" />
<Currency :value="category.Activity" class="hidden sm:block" />
<Currency :value="accountStore.GetCategoryAvailable(category)" />
<Currency
:value="accountStore.GetCategoryAvailable(category)" />
</template>
</template>
</div>

View File

@ -14,10 +14,14 @@ const BudgetsList = useSessionStore().BudgetsList;
<h1>Budgets</h1>
<div class="grid md:grid-cols-2 gap-6">
<Card v-for="budget in BudgetsList">
<router-link v-bind:to="'/budget/'+budget.ID+'/budgeting'" class="contents">
<router-link
v-bind:to="'/budget/'+budget.ID+'/budgeting'"
class="contents">
<!--<svg class="w-24"></svg>-->
<p class="w-24 text-center text-6xl"></p>
<span class="text-lg">{{budget.Name}}{{budget.ID == budgetid ? " *" : ""}}</span>
<span class="text-lg"
>{{budget.Name}}{{budget.ID == budgetid ? " *" : ""}}</span
>
</router-link>
</Card>
<NewBudget />

View File

@ -1,5 +1,4 @@
<script lang="ts" setup>
</script>
<script lang="ts" setup></script>
<template>
<div>
@ -7,7 +6,8 @@
Willkommen bei Budgeteer, der neuen App für's Budget!
</div>
<div class="container col-md-4" id="login">
<router-link to="/login">Login</router-link> or <router-link to="/login">register</router-link>
<router-link to="/login">Login</router-link> or
<router-link to="/login">register</router-link>
</div>
</div>
</template>

View File

@ -29,15 +29,24 @@ function formSubmit(e: MouseEvent) {
<template>
<div>
<Input type="text" v-model="login.user"
<Input
type="text"
v-model="login.user"
placeholder="Username"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
<Input type="password" v-model="login.password"
<Input
type="password"
v-model="login.password"
placeholder="Password"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
</div>
<div>{{ error }}</div>
<button type="submit" @click="formSubmit" class="bg-blue-300 rounded-lg p-2 w-48">Login</button>
<button
type="submit"
@click="formSubmit"
class="bg-blue-300 rounded-lg p-2 w-48">
Login
</button>
<p>
New user?
<router-link to="/register">Register</router-link> instead!

View File

@ -29,18 +29,29 @@ function formSubmit(e: MouseEvent) {
<template>
<div>
<Input type="text" v-model="login.name"
<Input
type="text"
v-model="login.name"
placeholder="Name"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
<Input type="text" v-model="login.email"
<Input
type="text"
v-model="login.email"
placeholder="Email"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
<Input type="password" v-model="login.password"
<Input
type="password"
v-model="login.password"
placeholder="Password"
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
</div>
<div>{{ error }}</div>
<button type="submit" @click="formSubmit" class="bg-blue-300 rounded-lg p-2 w-48">Register</button>
<button
type="submit"
@click="formSubmit"
class="bg-blue-300 rounded-lg p-2 w-48">
Register
</button>
<p>
Existing user?
<router-link to="/login">Login</router-link> instead!

View File

@ -72,7 +72,6 @@ function ynabExport() {
saveAs(blob, timeStamp + " " + CurrentBudgetName.value + " - Transactions.tsv");
})
}
</script>
<template>
@ -81,19 +80,36 @@ function ynabExport() {
<div class="grid md:grid-cols-2 gap-6">
<Card class="flex-col p-3">
<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 py-2" @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 py-2" @click="deleteBudget">Delete budget</button>
<p>
This deletes the whole bugdet including all transactions,
assignments, accounts and categories. Not undoable!
</p>
<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 py-2" @click="cleanNegative">Fix negative</button>
<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 py-2" @click="cleanNegative"
>Fix negative</Button
>
</Card>
<Card class="flex-col p-3">
<h2 class="text-lg font-bold">Import YNAB Budget</h2>
@ -101,22 +117,35 @@ function ynabExport() {
<div>
<label for="transactions_file">
Transaktionen:
<input type="file" @change="gotTransactions" accept="text/*" />
<input
type="file"
@change="gotTransactions"
accept="text/*" />
</label>
<br />
<label for="assignments_file">
Budget:
<input type="file" @change="gotAssignments" accept="text/*" />
<input
type="file"
@change="gotAssignments"
accept="text/*" />
</label>
</div>
<Button class="bg-blue-500 py-2" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
<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 py-2" @click="ynabExport">Export</Button>
<Button class="bg-blue-500 py-2" @click="ynabExport"
>Export</Button
>
</div>
</Card>
</div>

View File

@ -1,4 +1,9 @@
import { PiniaPluginContext, StoreGeneric, _ActionsTree, _StoreOnActionListenerContext } from 'pinia';
import {
PiniaPluginContext,
StoreGeneric,
_ActionsTree,
_StoreOnActionListenerContext,
} from "pinia";
const cloneDeep = <T>(obj: T): T => {
try {
@ -8,9 +13,9 @@ const cloneDeep = <T>(obj: T): T => {
}
};
const formatTime = (date = new Date()) => {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
};
@ -18,12 +23,16 @@ const formatTime = (date = new Date()) => {
export interface PiniaLoggerOptions {
disabled?: boolean;
expanded?: boolean;
showDuration?: boolean
showDuration?: boolean;
showStoreName?: boolean;
logErrors?: boolean;
}
export type PiniaActionListenerContext = _StoreOnActionListenerContext<StoreGeneric, string, _ActionsTree>;
export type PiniaActionListenerContext = _StoreOnActionListenerContext<
StoreGeneric,
string,
_ActionsTree
>;
const defaultOptions: PiniaLoggerOptions = {
logErrors: true,
@ -33,7 +42,9 @@ const defaultOptions: PiniaLoggerOptions = {
showDuration: false,
};
export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext) => {
export const PiniaLogger =
(config = defaultOptions) =>
(ctx: PiniaPluginContext) => {
const options = {
...defaultOptions,
...config,
@ -41,28 +52,43 @@ export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext
if (options.disabled) return;
ctx.store.$onAction((action: PiniaActionListenerContext) => {
const startTime = Date.now();
const prevState = cloneDeep(ctx.store.$state);
const log = (isError?: boolean, error?: any) => {
const endTime = Date.now();
const duration = endTime - startTime + 'ms';
const duration = endTime - startTime + "ms";
const nextState = cloneDeep(ctx.store.$state);
const storeName = action.store.$id;
const title = `${formatTime()} action 🍍 ${options.showStoreName ? `[${storeName}] ` : ''}${action.name} ${isError ? `failed after ` : ''}in ${duration}`;
const title = `${formatTime()} action 🍍 ${
options.showStoreName ? `[${storeName}] ` : ""
}${action.name} ${
isError ? `failed after ` : ""
}in ${duration}`;
console[options.expanded ? 'group' : 'groupCollapsed'](`%c${title}`, `font-weight: bold; ${isError ? 'color: #ed4981;' : ''}`);
console.log('%cprev state', 'font-weight: bold; color: grey;', prevState);
console.log('%caction', 'font-weight: bold; color: #69B7FF;', {
console[options.expanded ? "group" : "groupCollapsed"](
`%c${title}`,
`font-weight: bold; ${isError ? "color: #ed4981;" : ""}`
);
console.log(
"%cprev state",
"font-weight: bold; color: grey;",
prevState
);
console.log("%caction", "font-weight: bold; color: #69B7FF;", {
type: action.name,
args: action.args.length > 0 ? { ...action.args } : undefined,
args:
action.args.length > 0 ? { ...action.args } : undefined,
...(options.showStoreName && { store: action.store.$id }),
...(options.showDuration && { duration }),
...(isError && { error }),
});
console.log('%cnext state', 'font-weight: bold; color: #4caf50;', nextState);
console.log(
"%cnext state",
"font-weight: bold; color: #4caf50;",
nextState
);
console.groupEnd();
};
@ -76,6 +102,6 @@ export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext
});
}
});
};
};
export default PiniaLogger;

View File

@ -1,30 +1,75 @@
import { createRouter, createWebHistory, RouteLocationNormalized } from 'vue-router'
import Dashboard from '../pages/Dashboard.vue';
import Login from '../pages/Login.vue';
import Index from '../pages/Index.vue';
import Register from '../pages/Register.vue';
import Account from '@/pages/Account.vue';
import Settings from '../pages/Settings.vue';
import Budgeting from '../pages/Budgeting.vue';
import BudgetSidebar from '../pages/BudgetSidebar.vue';
import {
createRouter,
createWebHistory,
RouteLocationNormalized,
} from "vue-router";
import Dashboard from "../pages/Dashboard.vue";
import Login from "../pages/Login.vue";
import Index from "../pages/Index.vue";
import Register from "../pages/Register.vue";
import Account from "@/pages/Account.vue";
import Settings from "../pages/Settings.vue";
import Budgeting from "../pages/Budgeting.vue";
import BudgetSidebar from "../pages/BudgetSidebar.vue";
const routes = [
{ path: "/", name: "Index", component: Index },
{ path: "/dashboard", name: "Dashboard", component: Dashboard, meta: { requiresAuth: true } },
{ path: "/login", name: "Login", component: Login, meta: { hideForAuth: true } },
{ path: "/register", name: "Register", component: Register, meta: { hideForAuth: true } },
{ path: "/budget/:budgetid/budgeting", name: "Budget", redirect: (to : RouteLocationNormalized) =>
'/budget/' + to.params.budgetid + '/budgeting/' + new Date().getFullYear() + '/' + new Date().getMonth(),
meta: { requiresAuth: true }
{
path: "/dashboard",
name: "Dashboard",
component: Dashboard,
meta: { requiresAuth: true },
},
{ path: "/budget/:budgetid/budgeting/:year/:month", name: "Budget with date", components: { default: Budgeting, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
{ path: "/budget/:budgetid/Settings", name: "Budget Settings", components: { default: Settings, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
{ path: "/budget/:budgetid/account/:accountid", name: "Account", components: { default: Account, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
]
{
path: "/login",
name: "Login",
component: Login,
meta: { hideForAuth: true },
},
{
path: "/register",
name: "Register",
component: Register,
meta: { hideForAuth: true },
},
{
path: "/budget/:budgetid/budgeting",
name: "Budget",
redirect: (to: RouteLocationNormalized) =>
"/budget/" +
to.params.budgetid +
"/budgeting/" +
new Date().getFullYear() +
"/" +
new Date().getMonth(),
meta: { requiresAuth: true },
},
{
path: "/budget/:budgetid/budgeting/:year/:month",
name: "Budget with date",
components: { default: Budgeting, sidebar: BudgetSidebar },
props: true,
meta: { requiresAuth: true },
},
{
path: "/budget/:budgetid/Settings",
name: "Budget Settings",
components: { default: Settings, sidebar: BudgetSidebar },
props: true,
meta: { requiresAuth: true },
},
{
path: "/budget/:budgetid/account/:accountid",
name: "Account",
components: { default: Account, sidebar: BudgetSidebar },
props: true,
meta: { requiresAuth: true },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
})
});
export default router
export default router;

View File

@ -1,41 +1,41 @@
import { defineStore } from "pinia"
import { defineStore } from "pinia";
import { GET, POST } from "../api";
import { useBudgetsStore } from "./budget";
import { useSessionStore } from "./session";
import { useTransactionsStore } from "./transactions";
interface State {
Accounts: Map<string, Account>
CurrentAccountID: string | null
Categories: Map<string, Category>
Months: Map<number, Map<number, Map<string, Category>>>
Assignments: []
Accounts: Map<string, Account>;
CurrentAccountID: string | null;
Categories: Map<string, Category>;
Months: Map<number, Map<number, Map<string, Category>>>;
Assignments: [];
}
export interface Account {
ID: string
Name: string
OnBudget: boolean
IsOpen: boolean
ClearedBalance: number
WorkingBalance: number
ReconciledBalance: number
Transactions: string[]
LastReconciled: NullDate
ID: string;
Name: string;
OnBudget: boolean;
IsOpen: boolean;
ClearedBalance: number;
WorkingBalance: number;
ReconciledBalance: number;
Transactions: string[];
LastReconciled: NullDate;
}
interface NullDate {
Valid: boolean
Time: Date
Valid: boolean;
Time: Date;
}
export interface Category {
ID: string
Group: string
Name: string
AvailableLastMonth: number
Assigned: number
Activity: number
ID: string;
Group: string;
Name: string;
AvailableLastMonth: number;
Assigned: number;
Activity: number;
}
export const useAccountStore = defineStore("budget/account", {
@ -53,12 +53,16 @@ export const useAccountStore = defineStore("budget/account", {
AllCategoriesForMonth: (state) => (year: number, month: number) => {
const yearMap = state.Months.get(year);
const monthMap = yearMap?.get(month);
return [...monthMap?.values() || []];
return [...(monthMap?.values() || [])];
},
GetCategoryAvailable(state) {
return (category: Category): number => {
return category.AvailableLastMonth + Number(category.Assigned) + category.Activity;
}
return (
category.AvailableLastMonth +
Number(category.Assigned) +
category.Activity
);
};
},
GetIncomeCategoryID(state) {
const budget = useBudgetsStore();
@ -67,15 +71,15 @@ export const useAccountStore = defineStore("budget/account", {
GetIncomeAvailable(state) {
return (year: number, month: number) => {
const IncomeCategoryID = this.GetIncomeCategoryID;
if (IncomeCategoryID == null)
return 0;
if (IncomeCategoryID == null) return 0;
const categories = this.AllCategoriesForMonth(year, month);
const category = categories.filter(x => x.ID == IncomeCategoryID)[0];
if (category == null)
return 0;
const category = categories.filter(
(x) => x.ID == IncomeCategoryID
)[0];
if (category == null) return 0;
return category.AvailableLastMonth;
}
};
},
CategoryGroupsForMonth(state) {
return (year: number, month: number) => {
@ -83,8 +87,7 @@ export const useAccountStore = defineStore("budget/account", {
const categoryGroups = [];
let prev = undefined;
for (const category of categories) {
if (category.ID == this.GetIncomeCategoryID)
continue;
if (category.ID == this.GetIncomeCategoryID) continue;
if (prev == undefined || category.Group != prev.Name) {
prev = {
@ -93,81 +96,110 @@ export const useAccountStore = defineStore("budget/account", {
AvailableLastMonth: category.AvailableLastMonth,
Activity: category.Activity,
Assigned: category.Assigned,
}
};
categoryGroups.push({
...prev,
Expand: prev.Name != "Hidden Categories",
});
} else {
categoryGroups[categoryGroups.length-1].Available += this.GetCategoryAvailable(category);
categoryGroups[categoryGroups.length-1].AvailableLastMonth += category.AvailableLastMonth;
categoryGroups[categoryGroups.length-1].Activity += category.Activity;
categoryGroups[categoryGroups.length-1].Assigned += category.Assigned;
categoryGroups[categoryGroups.length - 1].Available +=
this.GetCategoryAvailable(category);
categoryGroups[
categoryGroups.length - 1
].AvailableLastMonth += category.AvailableLastMonth;
categoryGroups[categoryGroups.length - 1].Activity +=
category.Activity;
categoryGroups[categoryGroups.length - 1].Assigned +=
category.Assigned;
continue;
}
}
return categoryGroups;
}
};
},
CategoriesForMonthAndGroup(state) {
return (year: number, month: number, group: string) => {
const categories = this.AllCategoriesForMonth(year, month);
return categories.filter(x => x.Group == group);
}
return categories.filter((x) => x.Group == group);
};
},
GetAccount(state) {
return (accountid: string) => {
return this.Accounts.get(accountid);
}
};
},
CurrentAccount(state): Account | undefined {
if (state.CurrentAccountID == null)
return undefined;
if (state.CurrentAccountID == null) return undefined;
return this.GetAccount(state.CurrentAccountID);
},
OnBudgetAccounts(state) {
return [...state.Accounts.values()].filter(x => x.OnBudget);
return [...state.Accounts.values()].filter((x) => x.OnBudget);
},
OnBudgetAccountsBalance(state): number {
return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0);
return this.OnBudgetAccounts.reduce(
(prev, curr) => prev + Number(curr.ClearedBalance),
0
);
},
OffBudgetAccounts(state) {
return [...state.Accounts.values()].filter(x => !x.OnBudget);
return [...state.Accounts.values()].filter((x) => !x.OnBudget);
},
OffBudgetAccountsBalance(state): number {
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0);
return this.OffBudgetAccounts.reduce(
(prev, curr) => prev + Number(curr.ClearedBalance),
0
);
},
},
actions: {
async SetCurrentAccount(budgetid: string, accountid: string) {
if (budgetid == null)
return;
if (budgetid == null) return;
this.CurrentAccountID = accountid;
if (accountid == null)
return;
if (accountid == null) return;
const account = this.GetAccount(accountid)!;
useSessionStore().setTitle(account.Name);
await this.FetchAccount(account);
},
async FetchAccount(account: Account) {
const result = await GET("/account/" + account.ID + "/transactions");
const result = await GET(
"/account/" + account.ID + "/transactions"
);
const response = await result.json();
const transactionsStore = useTransactionsStore()
const transactions = transactionsStore.AddTransactions(response.Transactions);
const transactionsStore = useTransactionsStore();
const transactions = transactionsStore.AddTransactions(
response.Transactions
);
account.Transactions = transactions;
},
async FetchMonthBudget(budgetid: string, year: number, month: number) {
const result = await GET("/budget/" + budgetid + "/" + year + "/" + (month + 1));
const result = await GET(
"/budget/" + budgetid + "/" + year + "/" + (month + 1)
);
const response = await result.json();
if (response.Categories == undefined || response.Categories.length <= 0)
if (
response.Categories == undefined ||
response.Categories.length <= 0
)
return;
this.addCategoriesForMonth(year, month, response.Categories);
},
async EditAccount(accountid: string, name: string, onBudget: boolean, isOpen: boolean) {
const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget, isOpen: isOpen }));
async EditAccount(
accountid: string,
name: string,
onBudget: boolean,
isOpen: boolean
) {
const result = await POST(
"/account/" + accountid,
JSON.stringify({
name: name,
onBudget: onBudget,
isOpen: isOpen,
})
);
const response = await result.json();
useBudgetsStore().MergeBudgetingData(response);
@ -175,10 +207,17 @@ export const useAccountStore = defineStore("budget/account", {
this.Accounts.delete(accountid);
}
},
addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
addCategoriesForMonth(
year: number,
month: number,
categories: Category[]
): void {
this.$patch((state) => {
const yearMap = state.Months.get(year) || new Map<number, Map<string, Category>>();
const monthMap = yearMap.get(month) || new Map<string, Category>();
const yearMap =
state.Months.get(year) ||
new Map<number, Map<string, Category>>();
const monthMap =
yearMap.get(month) || new Map<string, Category>();
for (const category of categories) {
monthMap.set(category.ID, category);
}
@ -188,8 +227,7 @@ export const useAccountStore = defineStore("budget/account", {
});
},
logout() {
this.$reset()
this.$reset();
},
}
})
},
});

View File

@ -4,17 +4,16 @@ import { useAccountStore } from "./budget-account";
import { Budget, useSessionStore } from "./session";
interface State {
CurrentBudgetID: string | null,
CurrentBudgetID: string | null;
}
export const useBudgetsStore = defineStore('budget', {
export const useBudgetsStore = defineStore("budget", {
state: (): State => ({
CurrentBudgetID: null,
}),
getters: {
CurrentBudget(): Budget | undefined {
if (this.CurrentBudgetID == null)
return undefined;
if (this.CurrentBudgetID == null) return undefined;
const sessionStore = useSessionStore();
return sessionStore.Budgets.get(this.CurrentBudgetID);
@ -27,7 +26,7 @@ export const useBudgetsStore = defineStore('budget', {
ImportYNAB(formData: FormData) {
return POST(
"/budget/" + this.CurrentBudgetID + "/import/ynab",
formData,
formData
);
},
async NewBudget(budgetName: string): Promise<void> {
@ -43,8 +42,7 @@ export const useBudgetsStore = defineStore('budget', {
async SetCurrentBudget(budgetid: string): Promise<void> {
this.CurrentBudgetID = budgetid;
if (budgetid == null)
return
if (budgetid == null) return;
await this.FetchBudget(budgetid);
},
@ -58,13 +56,15 @@ export const useBudgetsStore = defineStore('budget', {
for (const account of response.Accounts || []) {
const existingAccount = accounts.Accounts.get(account.ID);
account.Transactions = existingAccount?.Transactions ?? [];
if(account.LastReconciled.Valid)
account.LastReconciled.Time = new Date(account.LastReconciled.Time);
if (account.LastReconciled.Valid)
account.LastReconciled.Time = new Date(
account.LastReconciled.Time
);
accounts.Accounts.set(account.ID, account);
}
for (const category of response.Categories || []) {
accounts.Categories.set(category.ID, category);
}
},
}
})
},
});

View File

@ -1,43 +1,52 @@
import { StorageSerializers, useStorage } from '@vueuse/core';
import { defineStore } from 'pinia'
import { POST } from '../api';
import { StorageSerializers, useStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { POST } from "../api";
interface State {
Session: Session | null
Budgets: Map<string, Budget>,
Session: Session | null;
Budgets: Map<string, Budget>;
}
interface Session {
Token: string
User: string
Token: string;
User: string;
}
export interface Budget {
ID: string
Name: string
AvailableBalance: number
IncomeCategoryID: string
ID: string;
Name: string;
AvailableBalance: number;
IncomeCategoryID: string;
}
export const useSessionStore = defineStore('session', {
export const useSessionStore = defineStore("session", {
state: () => ({
Session: useStorage<Session | null>('session', null, undefined, { serializer: StorageSerializers.object }),
Budgets: useStorage<Map<string, Budget>>('budgets', new Map<string, Budget>(), undefined, { serializer: StorageSerializers.map }),
Session: useStorage<Session | null>("session", null, undefined, {
serializer: StorageSerializers.object,
}),
Budgets: useStorage<Map<string, Budget>>(
"budgets",
new Map<string, Budget>(),
undefined,
{ serializer: StorageSerializers.map }
),
}),
getters: {
BudgetsList: (state) => [ ...state.Budgets.values() ],
AuthHeaders: (state) => ({'Authorization': 'Bearer ' + state.Session?.Token}),
BudgetsList: (state) => [...state.Budgets.values()],
AuthHeaders: (state) => ({
Authorization: "Bearer " + state.Session?.Token,
}),
LoggedIn: (state) => state.Session != null,
},
actions: {
setTitle(title : string) {
setTitle(title: string) {
document.title = "Budgeteer - " + title;
},
loginSuccess(x : any) {
loginSuccess(x: any) {
this.Session = {
User: x.User,
Token: x.Token,
}
};
for (const budget of x.Budgets) {
this.Budgets.set(budget.ID, budget);
}
@ -48,8 +57,11 @@ export const useSessionStore = defineStore('session', {
this.loginSuccess(result);
return result;
},
async register(login : any) {
const response = await POST("/user/register", JSON.stringify(login));
async register(login: any) {
const response = await POST(
"/user/register",
JSON.stringify(login)
);
const result = await response.json();
this.loginSuccess(result);
return result;
@ -58,5 +70,5 @@ export const useSessionStore = defineStore('session', {
this.Session = null;
this.Budgets.clear();
},
}
})
},
});

View File

@ -2,17 +2,17 @@ import { useStorage } from "@vueuse/core";
import { defineStore } from "pinia";
interface State {
Menu: MenuSettings
Menu: MenuSettings;
}
interface MenuSettings {
Show: boolean | null,
Expand: boolean | null,
Show: boolean | null;
Expand: boolean | null;
}
export const useSettingsStore = defineStore('settings', {
export const useSettingsStore = defineStore("settings", {
state: () => ({
Menu: useStorage<MenuSettings>('settings', {
Menu: useStorage<MenuSettings>("settings", {
Show: null,
Expand: false,
}),
@ -24,5 +24,5 @@ export const useSettingsStore = defineStore('settings', {
toggleMenuSize() {
this.Menu.Expand = !this.Menu.Expand;
},
}
},
});

View File

@ -1,26 +1,26 @@
import { defineStore } from "pinia"
import { defineStore } from "pinia";
import { POST } from "../api";
import { useAccountStore } from "./budget-account";
interface State {
Transactions: Map<string, Transaction>
Reconciling: boolean
Transactions: Map<string, Transaction>;
Reconciling: boolean;
}
export interface Transaction {
ID: string
Date: Date
TransferAccount: string
CategoryGroup: string
Category: string
CategoryID: string | undefined
Memo: string
Status: string
GroupID: string
Payee: string
PayeeID: string | undefined
Amount: number
Reconciled: boolean
ID: string;
Date: Date;
TransferAccount: string;
CategoryGroup: string;
Category: string;
CategoryID: string | undefined;
Memo: string;
Status: string;
GroupID: string;
Payee: string;
PayeeID: string | undefined;
Amount: number;
Reconciled: boolean;
}
export const useTransactionsStore = defineStore("budget/transactions", {
@ -30,8 +30,9 @@ export const useTransactionsStore = defineStore("budget/transactions", {
}),
getters: {
ReconcilingBalance(state): number {
const accountsStore = useAccountStore()
let reconciledBalance = accountsStore.CurrentAccount!.ReconciledBalance;
const accountsStore = useAccountStore();
let reconciledBalance =
accountsStore.CurrentAccount!.ReconciledBalance;
for (const transaction of this.TransactionsList) {
if (transaction.Reconciled)
reconciledBalance += transaction.Amount;
@ -39,9 +40,9 @@ export const useTransactionsStore = defineStore("budget/transactions", {
return reconciledBalance;
},
TransactionsList(state): Transaction[] {
const accountsStore = useAccountStore()
return accountsStore.CurrentAccount!.Transactions.map(x => {
return this.Transactions.get(x)!
const accountsStore = useAccountStore();
return accountsStore.CurrentAccount!.Transactions.map((x) => {
return this.Transactions.get(x)!;
});
},
},
@ -59,25 +60,30 @@ export const useTransactionsStore = defineStore("budget/transactions", {
},
SetReconciledForAllTransactions(value: boolean) {
for (const transaction of this.TransactionsList) {
if (transaction.Status == "Reconciled")
continue;
if (transaction.Status == "Reconciled") continue;
transaction.Reconciled = value;
}
},
async SubmitReconcilation(reconciliationTransactionAmount: number) {
const accountsStore = useAccountStore()
const accountsStore = useAccountStore();
const account = accountsStore.CurrentAccount!;
const reconciledTransactions = this.TransactionsList.filter(x => x.Reconciled);
const reconciledTransactions = this.TransactionsList.filter(
(x) => x.Reconciled
);
for (const transaction of reconciledTransactions) {
account.ReconciledBalance += transaction.Amount;
transaction.Status = "Reconciled";
transaction.Reconciled = false;
}
const result = await POST("/account/" + accountsStore.CurrentAccountID + "/reconcile", JSON.stringify({
transactionIDs: reconciledTransactions.map(x => x.ID),
reconciliationTransactionAmount: reconciliationTransactionAmount.toString(),
}));
const result = await POST(
"/account/" + accountsStore.CurrentAccountID + "/reconcile",
JSON.stringify({
transactionIDs: reconciledTransactions.map((x) => x.ID),
reconciliationTransactionAmount:
reconciliationTransactionAmount.toString(),
})
);
const response = await result.json();
const recTrans = response.ReconciliationTransaction;
if (recTrans) {
@ -86,20 +92,19 @@ export const useTransactionsStore = defineStore("budget/transactions", {
}
},
logout() {
this.$reset()
this.$reset();
},
async saveTransaction(payload: string) {
const accountsStore = useAccountStore()
const accountsStore = useAccountStore();
const result = await POST("/transaction/new", payload);
const response = await result.json() as Transaction;
const response = (await result.json()) as Transaction;
this.AddTransactions([response]);
accountsStore.CurrentAccount?.Transactions.unshift(response.ID);
},
async editTransaction(transactionid: string, payload: string) {
const result = await POST("/transaction/" + transactionid, payload);
const response = await result.json() as Transaction;
const response = (await result.json()) as Transaction;
this.AddTransactions([response]);
}
}
})
},
},
});

View File

@ -1,10 +1,7 @@
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}"
],
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
};

View File

@ -1,7 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from 'path'
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
@ -9,21 +9,21 @@ export default defineConfig({
vue(),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
],
define: { 'process.env': {} },
define: { "process.env": {} },
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
"@": path.resolve(__dirname, "src"),
},
},
server: {
host: '0.0.0.0',
host: "0.0.0.0",
proxy: {
'/api': {
target: 'http://10.0.0.162:1323/',
changeOrigin: true
}
}
}
"/api": {
target: "http://10.0.0.162:1323/",
changeOrigin: true,
},
},
},
/* remove the need to specify .vue files https://vitejs.dev/config/#resolve-extensions
resolve: {
extensions: [
@ -37,4 +37,4 @@ export default defineConfig({
]
},
*/
})
});