Use spaces

This commit is contained in:
Jan Bader 2022-03-15 12:52:23 +00:00
parent 61a534610f
commit d717ef1b4d
43 changed files with 1400 additions and 1400 deletions

View File

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

View File

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title> <title>Vite App</title>
</head> </head>
<body <body
class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full"> class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View File

@ -1,43 +1,43 @@
{ {
"name": "web", "name": "web",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"serve": "vite preview", "serve": "vite preview",
"build": "vite build", "build": "vite build",
"dev": "vite", "dev": "vite",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "5.9.55", "@mdi/font": "5.9.55",
"@vueuse/core": "^7.6.1", "@vueuse/core": "^7.6.1",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"tailwindcss": "^3.0.18", "tailwindcss": "^3.0.18",
"vue": "^3.2.25", "vue": "^3.2.25",
"vue-router": "^4.0.12" "vue-router": "^4.0.12"
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.5", "@types/file-saver": "^2.0.5",
"@typescript-eslint/parser": "^5.13.0", "@typescript-eslint/parser": "^5.13.0",
"@vitejs/plugin-vue": "^2.0.0", "@vitejs/plugin-vue": "^2.0.0",
"@vue/cli-plugin-babel": "5.0.0-beta.7", "@vue/cli-plugin-babel": "5.0.0-beta.7",
"@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "5.0.0-beta.7", "@vue/cli-service": "5.0.0-beta.7",
"eslint": "^8.10.0", "eslint": "^8.10.0",
"eslint-plugin-vue": "^8.5.0", "eslint-plugin-vue": "^8.5.0",
"prettier": "2.5.1", "prettier": "2.5.1",
"sass": "^1.38.0", "sass": "^1.38.0",
"sass-loader": "^10.0.0", "sass-loader": "^10.0.0",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"vite": "^2.7.2", "vite": "^2.7.2",
"vue-tsc": "^0.32.0" "vue-tsc": "^0.32.0"
}, },
"prettier": { "prettier": {
"bracketSameLine": true, "bracketSameLine": true,
"embeddedLanguageFormatting": "off", "embeddedLanguageFormatting": "off",
"tabWidth": 4, "tabWidth": 4,
"useTabs": true "useTabs": false
} }
} }

View File

@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}; };

View File

@ -1,5 +1,5 @@
declare module "*.vue" { declare module "*.vue" {
import { defineComponent } from "vue"; import { defineComponent } from "vue";
const component: ReturnType<typeof defineComponent>; const component: ReturnType<typeof defineComponent>;
export default component; export default component;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<script lang="ts" setup></script> <script lang="ts" setup></script>
<template> <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> <slot></slot>
</button> </button>
</template> </template>

View File

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

View File

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

View File

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

View File

@ -26,10 +26,10 @@ function selectAll(event: FocusEvent) {
</script> </script>
<template> <template>
<Input <Input
type="date" type="date"
ref="input" ref="input"
v-bind:value="dateToYYYYMMDD(modelValue)" v-bind:value="dateToYYYYMMDD(modelValue)"
@input="updateValue" @input="updateValue"
@focus="selectAll" /> @focus="selectAll" />
</template> </template>

View File

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

View File

@ -30,38 +30,38 @@ function submitDialog() {
</script> </script>
<template> <template>
<button @click="openDialog"> <button @click="openDialog">
<slot name="placeholder"> <slot name="placeholder">
<Card> <Card>
<p class="w-24 text-center text-6xl">+</p> <p class="w-24 text-center text-6xl">+</p>
<span class="text-lg" dark>{{ buttonText }}</span> <span class="text-lg" dark>{{ buttonText }}</span>
</Card> </Card>
</slot> </slot>
</button> </button>
<div <div
v-if="visible" v-if="visible"
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"> class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full">
<div <div
class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black"> 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"> <div class="mt-3 text-center">
<h3 <h3
class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100"> class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
{{ buttonText }} {{ buttonText }}
</h3> </h3>
<slot></slot> <slot></slot>
<div class="grid grid-cols-2 gap-6"> <div class="grid grid-cols-2 gap-6">
<button <button
@click="closeDialog" @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"> 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 Close
</button> </button>
<button <button
@click="submitDialog" @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"> 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 Save
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -38,38 +38,38 @@ function saveTransaction(e: MouseEvent) {
</script> </script>
<template> <template>
<tr> <tr>
<td class="text-sm"> <td class="text-sm">
<DateInput class="border-b-2 border-black" v-model="TX.Date" /> <DateInput class="border-b-2 border-black" v-model="TX.Date" />
</td> </td>
<td> <td>
<Autocomplete <Autocomplete
v-model:text="TX.Payee" v-model:text="TX.Payee"
v-model:id="TX.PayeeID" v-model:id="TX.PayeeID"
v-model:type="payeeType" v-model:type="payeeType"
model="payees" /> model="payees" />
</td> </td>
<td> <td>
<Autocomplete <Autocomplete
v-model:text="TX.Category" v-model:text="TX.Category"
v-model:id="TX.CategoryID" v-model:id="TX.CategoryID"
model="categories" /> model="categories" />
</td> </td>
<td> <td>
<Input <Input
class="block w-full border-b-2 border-black" class="block w-full border-b-2 border-black"
type="text" type="text"
v-model="TX.Memo" /> v-model="TX.Memo" />
</td> </td>
<td class="text-right"> <td class="text-right">
<Input <Input
class="text-right block w-full border-b-2 border-black" class="text-right block w-full border-b-2 border-black"
type="currency" type="currency"
v-model="TX.Amount" /> v-model="TX.Amount" />
</td> </td>
<td> <td>
<Button class="bg-blue-500" @click="saveTransaction">Save</Button> <Button class="bg-blue-500" @click="saveTransaction">Save</Button>
</td> </td>
<td></td> <td></td>
</tr> </tr>
</template> </template>

View File

@ -52,41 +52,41 @@ function saveTransaction(e: MouseEvent) {
</script> </script>
<template> <template>
<tr> <tr>
<label class="md:hidden">Date</label> <label class="md:hidden">Date</label>
<td class="text-sm"> <td class="text-sm">
<DateInput class="border-b-2 border-black" v-model="TX.Date" /> <DateInput class="border-b-2 border-black" v-model="TX.Date" />
</td> </td>
<label class="md:hidden">Payee</label> <label class="md:hidden">Payee</label>
<td> <td>
<Autocomplete <Autocomplete
v-model:text="TX.Payee" v-model:text="TX.Payee"
v-model:id="TX.PayeeID" v-model:id="TX.PayeeID"
v-model:type="payeeType" v-model:type="payeeType"
model="payees" /> model="payees" />
</td> </td>
<label class="md:hidden">Category</label> <label class="md:hidden">Category</label>
<td> <td>
<Autocomplete <Autocomplete
v-model:text="TX.Category" v-model:text="TX.Category"
v-model:id="TX.CategoryID" v-model:id="TX.CategoryID"
model="categories" /> model="categories" />
</td> </td>
<td class="col-span-2"> <td class="col-span-2">
<Input <Input
class="block w-full border-b-2 border-black" class="block w-full border-b-2 border-black"
type="text" type="text"
v-model="TX.Memo" /> v-model="TX.Memo" />
</td> </td>
<label class="md:hidden">Amount</label> <label class="md:hidden">Amount</label>
<td class="text-right"> <td class="text-right">
<Input <Input
class="text-right block w-full border-b-2 border-black" class="text-right block w-full border-b-2 border-black"
type="currency" type="currency"
v-model="TX.Amount" /> v-model="TX.Amount" />
</td> </td>
<td class="hidden md:table-cell"> <td class="hidden md:table-cell">
<Button class="bg-blue-500" @click="saveTransaction">Save</Button> <Button class="bg-blue-500" @click="saveTransaction">Save</Button>
</td> </td>
</tr> </tr>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

@ -3,15 +3,15 @@
@tailwind utilities; @tailwind utilities;
h1 { h1 {
font-size: 200%; font-size: 200%;
} }
a { a {
text-decoration: underline; text-decoration: underline;
} }
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

View File

@ -13,66 +13,66 @@ app.use(router);
const pinia = createPinia(); const pinia = createPinia();
pinia.use( pinia.use(
PiniaLogger({ PiniaLogger({
expanded: false, expanded: false,
showDuration: true, showDuration: true,
}) })
); );
app.use(pinia); app.use(pinia);
app.mount("#app"); app.mount("#app");
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const budgetStore = useBudgetsStore(); const budgetStore = useBudgetsStore();
await budgetStore.SetCurrentBudget(<string>to.params.budgetid); await budgetStore.SetCurrentBudget(<string>to.params.budgetid);
const accountStore = useAccountStore(); const accountStore = useAccountStore();
await accountStore.SetCurrentAccount( await accountStore.SetCurrentAccount(
<string>to.params.budgetid, <string>to.params.budgetid,
<string>to.params.accountid <string>to.params.accountid
); );
next(); next();
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const token = sessionStore.Session?.Token; const token = sessionStore.Session?.Token;
let loggedIn = false; let loggedIn = false;
if (token != null) { if (token != null) {
const jwt = parseJwt(token); 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) { if (!loggedIn) {
next({ path: "/login" }); next({ path: "/login" });
} else { } else {
next(); next();
} }
} else if (to.matched.some((record) => record.meta.hideForAuth)) { } else if (to.matched.some((record) => record.meta.hideForAuth)) {
if (loggedIn) { if (loggedIn) {
next({ path: "/dashboard" }); next({ path: "/dashboard" });
} else { } else {
next(); next();
} }
} else { } else {
next(); next();
} }
}); });
function parseJwt(token: string) { function parseJwt(token: string) {
var base64Url = token.split(".")[1]; var base64Url = token.split(".")[1];
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
var jsonPayload = decodeURIComponent( var jsonPayload = decodeURIComponent(
atob(base64) atob(base64)
.split("") .split("")
.map(function (c) { .map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
}) })
.join("") .join("")
); );
return JSON.parse(jsonPayload); return JSON.parse(jsonPayload);
} }
1646426130; 1646426130;

View File

@ -42,101 +42,101 @@ function createReconcilationTransaction() {
</script> </script>
<template> <template>
<div class="grid grid-cols-[1fr_auto]"> <div class="grid grid-cols-[1fr_auto]">
<div> <div>
<h1 class="inline"> <h1 class="inline">
{{ accounts.CurrentAccount?.Name }} {{ accounts.CurrentAccount?.Name }}
</h1> </h1>
<EditAccount /> <EditAccount />
</div> </div>
<div <div
class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm"> 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"> <span class="rounded-lg p-1 whitespace-nowrap flex-1">
Working: Working:
<Currency :value="accounts.CurrentAccount?.WorkingBalance" /> <Currency :value="accounts.CurrentAccount?.WorkingBalance" />
</span> </span>
<span class="rounded-lg p-1 whitespace-nowrap flex-1"> <span class="rounded-lg p-1 whitespace-nowrap flex-1">
Cleared: Cleared:
<Currency :value="accounts.CurrentAccount?.ClearedBalance" /> <Currency :value="accounts.CurrentAccount?.ClearedBalance" />
</span> </span>
<span <span
class="rounded-lg bg-blue-500 p-1 whitespace-nowrap flex-1" class="rounded-lg bg-blue-500 p-1 whitespace-nowrap flex-1"
v-if="!transactions.Reconciling" v-if="!transactions.Reconciling"
@click="transactions.Reconciling = true"> @click="transactions.Reconciling = true">
Reconciled: Reconciled:
<Currency :value="accounts.CurrentAccount?.ReconciledBalance" /> <Currency :value="accounts.CurrentAccount?.ReconciledBalance" />
</span> </span>
<span v-if="transactions.Reconciling" class="contents"> <span v-if="transactions.Reconciling" class="contents">
<Button <Button
@click="submitReconcilation" @click="submitReconcilation"
class="bg-blue-500 p-1 whitespace-nowrap flex-1"> class="bg-blue-500 p-1 whitespace-nowrap flex-1">
My current balance is&nbsp; My current balance is&nbsp;
<Currency :value="transactions.ReconcilingBalance" /> <Currency :value="transactions.ReconcilingBalance" />
</Button> </Button>
<Button <Button
@click="createReconcilationTransaction" @click="createReconcilationTransaction"
class="bg-orange-500 p-1 whitespace-nowrap flex-1"> class="bg-orange-500 p-1 whitespace-nowrap flex-1">
No, it's: No, it's:
<Input <Input
class="text-right w-20 bg-transparent dark:bg-transparent border-b-2" class="text-right w-20 bg-transparent dark:bg-transparent border-b-2"
type="number" type="number"
v-model="TargetReconcilingBalance" /> v-model="TargetReconcilingBalance" />
(Difference (Difference
<Currency <Currency
:value="transactions.ReconcilingBalance - TargetReconcilingBalance" />) :value="transactions.ReconcilingBalance - TargetReconcilingBalance" />)
</Button> </Button>
<Button <Button
class="bg-red-500 p-1 flex-1" class="bg-red-500 p-1 flex-1"
@click="cancelReconcilation" @click="cancelReconcilation"
>Cancel</Button >Cancel</Button
> >
</span> </span>
</div> </div>
</div> </div>
<table> <table>
<tr class="font-bold"> <tr class="font-bold">
<td class="hidden md:block" style="width: 90px;">Date</td> <td class="hidden md:block" style="width: 90px;">Date</td>
<td style="max-width: 150px;">Payee</td> <td style="max-width: 150px;">Payee</td>
<td style="max-width: 200px;">Category</td> <td style="max-width: 200px;">Category</td>
<td>Memo</td> <td>Memo</td>
<td class="text-right">Amount</td> <td class="text-right">Amount</td>
<td style="width: 80px;"> <td style="width: 80px;">
<Input <Input
v-if="transactions.Reconciling" v-if="transactions.Reconciling"
type="checkbox" type="checkbox"
@input="setReconciled" /> @input="setReconciled" />
</td> </td>
</tr> </tr>
<TransactionInputRow <TransactionInputRow
class="hidden md:table-row" class="hidden md:table-row"
:budgetid="budgetid" :budgetid="budgetid"
:accountid="accountid" /> :accountid="accountid" />
<TransactionRow <TransactionRow
v-for="(transaction, index) in transactions.TransactionsList" v-for="(transaction, index) in transactions.TransactionsList"
:key="transaction.ID" :key="transaction.ID"
:transactionid="transaction.ID" :transactionid="transaction.ID"
:index="index" /> :index="index" />
</table> </table>
<div class="md:hidden"> <div class="md:hidden">
<Modal> <Modal>
<template v-slot:placeholder> <template v-slot:placeholder>
<Button <Button
class="fixed right-4 bottom-4 font-bold text-lg bg-blue-500 py-2" class="fixed right-4 bottom-4 font-bold text-lg bg-blue-500 py-2"
>+</Button >+</Button
> >
</template> </template>
<TransactionInputRow <TransactionInputRow
class="grid grid-cols-2" class="grid grid-cols-2"
:budgetid="budgetid" :budgetid="budgetid"
:accountid="accountid" /> :accountid="accountid" />
</Modal> </Modal>
</div> </div>
</template> </template>
<style> <style>

View File

@ -8,9 +8,9 @@ onMounted(() => {
</script> </script>
<template> <template>
<h1>Danger Zone</h1> <h1>Danger Zone</h1>
<div class="budget-item"> <div class="budget-item">
<button>Clear database</button> <button>Clear database</button>
<p>This removes all data and starts from scratch. Not undoable!</p> <p>This removes all data and starts from scratch. Not undoable!</p>
</div> </div>
</template> </template>

View File

@ -22,59 +22,59 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
</script> </script>
<template> <template>
<div <div
:class="[ExpandMenu ? 'md:w-72' : 'md:w-36', ShowMenu ? '' : 'hidden']" :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"> <div class="flex flex-col mt-14 md:mt-0">
<span <span
class="m-2 p-1 px-3 h-10 overflow-hidden" 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 to="/dashboard" style="font-size:150%"
></router-link ></router-link
> >
{{ CurrentBudgetName }} {{ CurrentBudgetName }}
</span> </span>
<span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col"> <span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col">
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'" <router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'"
>Budget</router-link >Budget</router-link
> >
<br /> <br />
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>--> <!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>--> <!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
</span> </span>
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3"> <li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold"> <div class="flex flex-row justify-between font-bold">
<span>On-Budget Accounts</span> <span>On-Budget Accounts</span>
<Currency <Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'" :class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="OnBudgetAccountsBalance" /> :value="OnBudgetAccountsBalance" />
</div> </div>
<div <div
v-for="account in OnBudgetAccounts" v-for="account in OnBudgetAccounts"
class="flex flex-row justify-between"> class="flex flex-row justify-between">
<AccountWithReconciled :account="account" /> <AccountWithReconciled :account="account" />
<Currency <Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'" :class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="account.ClearedBalance" /> :value="account.ClearedBalance" />
</div> </div>
</li> </li>
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3"> <li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold"> <div class="flex flex-row justify-between font-bold">
<span>Off-Budget Accounts</span> <span>Off-Budget Accounts</span>
<Currency <Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'" :class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="OffBudgetAccountsBalance" /> :value="OffBudgetAccountsBalance" />
</div> </div>
<div <div
v-for="account in OffBudgetAccounts" v-for="account in OffBudgetAccounts"
class="flex flex-row justify-between"> class="flex flex-row justify-between">
<AccountWithReconciled :account="account" /> <AccountWithReconciled :account="account" />
<Currency <Currency
:class="ExpandMenu ? 'md:inline' : 'md:hidden'" :class="ExpandMenu ? 'md:inline' : 'md:hidden'"
:value="account.ClearedBalance" /> :value="account.ClearedBalance" />
</div> </div>
</li> </li>
<!-- <!--
<li class="bg-slate-100 dark:bg-slate-800 my-2 p-2 px-3"> <li class="bg-slate-100 dark:bg-slate-800 my-2 p-2 px-3">
<div class="flex flex-row justify-between font-bold"> <div class="flex flex-row justify-between font-bold">
<span>Closed Accounts</span> <span>Closed Accounts</span>
@ -82,15 +82,15 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
+ Add Account + Add Account
</li> </li>
--> -->
<!--<li> <!--<li>
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link> <router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
</li>--> </li>-->
<li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3"> <li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3">
<router-link :to="'/budget/' + CurrentBudgetID + '/settings'" <router-link :to="'/budget/' + CurrentBudgetID + '/settings'"
>Budget-Settings</router-link >Budget-Settings</router-link
> >
</li> </li>
<!--<li><router-link to="/admin">Admin</router-link></li>--> <!--<li><router-link to="/admin">Admin</router-link></li>-->
</div> </div>
</div> </div>
</template> </template>

View File

@ -75,79 +75,79 @@ function assignedChanged(e : Event, category : Category){
</script> </script>
<template> <template>
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1> <h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
<span <span
>Available balance: >Available balance:
<Currency <Currency
:value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)" :value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)"
/></span> /></span>
<div> <div>
<router-link <router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month" :to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
>&lt;&lt;</router-link >&lt;&lt;</router-link
>&nbsp; >&nbsp;
<router-link <router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month" :to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
>Current Month</router-link >Current Month</router-link
>&nbsp; >&nbsp;
<router-link <router-link
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month" :to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
>&gt;&gt;</router-link >&gt;&gt;</router-link
> >
</div> </div>
<div <div
class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5"
id="content"> id="content">
<span class="hidden sm:block"></span> <span class="hidden sm:block"></span>
<span class="hidden lg:block text-right">Leftover</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">Assigned</span>
<span class="hidden sm:block text-right">Activity</span> <span class="hidden sm:block text-right">Activity</span>
<span class="hidden sm:block text-right">Available</span> <span class="hidden sm:block text-right">Available</span>
<template v-for="group in GroupsForMonth"> <template v-for="group in GroupsForMonth">
<span <span
class="text-lg font-bold mt-2" class="text-lg font-bold mt-2"
@click="toggleGroup(group)" @click="toggleGroup(group)"
>{{ (getGroupState(group) ? "" : "+") + " " + group.Name }}</span >{{ (getGroupState(group) ? "" : "+") + " " + group.Name }}</span
> >
<Currency <Currency
:value="group.AvailableLastMonth" :value="group.AvailableLastMonth"
class="hidden lg:block mt-2" class="hidden lg:block mt-2"
positive-class="text-slate-500" positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" /> negative-class="text-red-700 dark:text-red-400" />
<Currency <Currency
:value="group.Assigned" :value="group.Assigned"
class="hidden sm:block mx-2 mt-2 text-right" class="hidden sm:block mx-2 mt-2 text-right"
positive-class="text-slate-500" positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" /> negative-class="text-red-700 dark:text-red-400" />
<Currency <Currency
:value="group.Activity" :value="group.Activity"
class="hidden sm:block mt-2" class="hidden sm:block mt-2"
positive-class="text-slate-500" positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" /> negative-class="text-red-700 dark:text-red-400" />
<Currency <Currency
:value="group.Available" :value="group.Available"
class="mt-2" class="mt-2"
positive-class="text-slate-500" positive-class="text-slate-500"
negative-class="text-red-700 dark:text-red-400" /> negative-class="text-red-700 dark:text-red-400" />
<template <template
v-for="category in GetCategories(group.Name)" v-for="category in GetCategories(group.Name)"
v-if="getGroupState(group)"> v-if="getGroupState(group)">
<span <span
class="whitespace-nowrap overflow-hidden" class="whitespace-nowrap overflow-hidden"
>{{ category.Name }}</span >{{ category.Name }}</span
> >
<Currency <Currency
:value="category.AvailableLastMonth" :value="category.AvailableLastMonth"
class="hidden lg:block" /> class="hidden lg:block" />
<Input <Input
type="number" type="number"
v-model="category.Assigned" v-model="category.Assigned"
@input="(evt) => assignedChanged(evt, category)" @input="(evt) => assignedChanged(evt, category)"
class="hidden sm:block mx-2 text-right" /> class="hidden sm:block mx-2 text-right" />
<Currency :value="category.Activity" class="hidden sm:block" /> <Currency :value="category.Activity" class="hidden sm:block" />
<Currency <Currency
:value="accountStore.GetCategoryAvailable(category)" /> :value="accountStore.GetCategoryAvailable(category)" />
</template> </template>
</template> </template>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

@ -75,79 +75,79 @@ function ynabExport() {
</script> </script>
<template> <template>
<div> <div>
<h1>Danger Zone</h1> <h1>Danger Zone</h1>
<div class="grid md:grid-cols-2 gap-6"> <div class="grid md:grid-cols-2 gap-6">
<Card class="flex-col p-3"> <Card class="flex-col p-3">
<h2 class="text-lg font-bold">Clear Budget</h2> <h2 class="text-lg font-bold">Clear Budget</h2>
<p> <p>
This removes transactions and assignments to start from This removes transactions and assignments to start from
scratch. Accounts and categories are kept. Not undoable! scratch. Accounts and categories are kept. Not undoable!
</p> </p>
<Button class="bg-red-500 py-2" @click="clearBudget" <Button class="bg-red-500 py-2" @click="clearBudget"
>Clear budget</Button >Clear budget</Button
> >
</Card> </Card>
<Card class="flex-col p-3"> <Card class="flex-col p-3">
<h2 class="text-lg font-bold">Delete Budget</h2> <h2 class="text-lg font-bold">Delete Budget</h2>
<p> <p>
This deletes the whole bugdet including all transactions, This deletes the whole bugdet including all transactions,
assignments, accounts and categories. Not undoable! assignments, accounts and categories. Not undoable!
</p> </p>
<Button class="bg-red-500 py-2" @click="deleteBudget" <Button class="bg-red-500 py-2" @click="deleteBudget"
>Delete budget</Button >Delete budget</Button
> >
</Card> </Card>
<Card class="flex-col p-3"> <Card class="flex-col p-3">
<h2 class="text-lg font-bold"> <h2 class="text-lg font-bold">
Fix all historic negative category-balances Fix all historic negative category-balances
</h2> </h2>
<p> <p>
This restores YNABs functionality, that would substract any This restores YNABs functionality, that would substract any
overspent categories' balances from next months inflows. overspent categories' balances from next months inflows.
</p> </p>
<Button class="bg-orange-500 py-2" @click="cleanNegative" <Button class="bg-orange-500 py-2" @click="cleanNegative"
>Fix negative</Button >Fix negative</Button
> >
</Card> </Card>
<Card class="flex-col p-3"> <Card class="flex-col p-3">
<h2 class="text-lg font-bold">Import YNAB Budget</h2> <h2 class="text-lg font-bold">Import YNAB Budget</h2>
<div> <div>
<label for="transactions_file"> <label for="transactions_file">
Transaktionen: Transaktionen:
<input <input
type="file" type="file"
@change="gotTransactions" @change="gotTransactions"
accept="text/*" /> accept="text/*" />
</label> </label>
<br /> <br />
<label for="assignments_file"> <label for="assignments_file">
Budget: Budget:
<input <input
type="file" type="file"
@change="gotAssignments" @change="gotAssignments"
accept="text/*" /> accept="text/*" />
</label> </label>
</div> </div>
<Button <Button
class="bg-blue-500 py-2" class="bg-blue-500 py-2"
:disabled="filesIncomplete" :disabled="filesIncomplete"
@click="ynabImport" @click="ynabImport"
>Importieren</Button >Importieren</Button
> >
</Card> </Card>
<Card class="flex-col p-3"> <Card class="flex-col p-3">
<h2 class="text-lg font-bold">Export as YNAB TSV</h2> <h2 class="text-lg font-bold">Export as YNAB TSV</h2>
<div class="flex flex-row"> <div class="flex flex-row">
<Button class="bg-blue-500 py-2" @click="ynabExport" <Button class="bg-blue-500 py-2" @click="ynabExport"
>Export</Button >Export</Button
> >
</div> </div>
</Card> </Card>
</div> </div>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

@ -4,67 +4,67 @@ import { useAccountStore } from "./budget-account";
import { Budget, useSessionStore } from "./session"; import { Budget, useSessionStore } from "./session";
interface State { interface State {
CurrentBudgetID: string | null; CurrentBudgetID: string | null;
} }
export const useBudgetsStore = defineStore("budget", { export const useBudgetsStore = defineStore("budget", {
state: (): State => ({ state: (): State => ({
CurrentBudgetID: null, CurrentBudgetID: null,
}), }),
getters: { getters: {
CurrentBudget(): Budget | undefined { CurrentBudget(): Budget | undefined {
if (this.CurrentBudgetID == null) return undefined; if (this.CurrentBudgetID == null) return undefined;
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
return sessionStore.Budgets.get(this.CurrentBudgetID); return sessionStore.Budgets.get(this.CurrentBudgetID);
}, },
CurrentBudgetName(state): string { CurrentBudgetName(state): string {
return this.CurrentBudget?.Name ?? ""; return this.CurrentBudget?.Name ?? "";
}, },
}, },
actions: { actions: {
ImportYNAB(formData: FormData) { ImportYNAB(formData: FormData) {
return POST( return POST(
"/budget/" + this.CurrentBudgetID + "/import/ynab", "/budget/" + this.CurrentBudgetID + "/import/ynab",
formData formData
); );
}, },
async NewBudget(budgetName: string): Promise<void> { async NewBudget(budgetName: string): Promise<void> {
const result = await POST( const result = await POST(
"/budget/new", "/budget/new",
JSON.stringify({ name: budgetName }) JSON.stringify({ name: budgetName })
); );
const response = await result.json(); const response = await result.json();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
sessionStore.Budgets.set(response.ID, response); sessionStore.Budgets.set(response.ID, response);
}, },
async SetCurrentBudget(budgetid: string): Promise<void> { async SetCurrentBudget(budgetid: string): Promise<void> {
this.CurrentBudgetID = budgetid; this.CurrentBudgetID = budgetid;
if (budgetid == null) return; if (budgetid == null) return;
await this.FetchBudget(budgetid); await this.FetchBudget(budgetid);
}, },
async FetchBudget(budgetid: string) { async FetchBudget(budgetid: string) {
const result = await GET("/budget/" + budgetid); const result = await GET("/budget/" + budgetid);
const response = await result.json(); const response = await result.json();
this.MergeBudgetingData(response); this.MergeBudgetingData(response);
}, },
MergeBudgetingData(response: any) { MergeBudgetingData(response: any) {
const accounts = useAccountStore(); const accounts = useAccountStore();
for (const account of response.Accounts || []) { for (const account of response.Accounts || []) {
const existingAccount = accounts.Accounts.get(account.ID); const existingAccount = accounts.Accounts.get(account.ID);
account.Transactions = existingAccount?.Transactions ?? []; account.Transactions = existingAccount?.Transactions ?? [];
if (account.LastReconciled.Valid) if (account.LastReconciled.Valid)
account.LastReconciled.Time = new Date( account.LastReconciled.Time = new Date(
account.LastReconciled.Time account.LastReconciled.Time
); );
accounts.Accounts.set(account.ID, account); accounts.Accounts.set(account.ID, account);
} }
for (const category of response.Categories || []) { for (const category of response.Categories || []) {
accounts.Categories.set(category.ID, category); accounts.Categories.set(category.ID, category);
} }
}, },
}, },
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
// this enables stricter inference for data properties on `this` // this enables stricter inference for data properties on `this`
"strict": true, "strict": true,
"jsx": "preserve", "jsx": "preserve",
"moduleResolution": "node" "moduleResolution": "node"
} }
} }

View File

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