Use spaces
This commit is contained in:
parent
61a534610f
commit
d717ef1b4d
@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
6
web/src/@types/shims-vue.d.ts
vendored
6
web/src/@types/shims-vue.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
My current balance is
|
||||||
<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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
><<</router-link
|
><<</router-link
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
|
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
|
||||||
>Current Month</router-link
|
>Current Month</router-link
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
|
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
|
||||||
>>></router-link
|
>>></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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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: [],
|
||||||
};
|
};
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user