Setup and enable ESLint #43
@ -78,6 +78,7 @@ tasks:
|
||||
- yarn
|
||||
- yarn build
|
||||
- yarn run vue-tsc --noEmit
|
||||
- yarn run eslint "./src/**"
|
||||
|
||||
docker:
|
||||
desc: Build budgeeter:latest
|
||||
|
17
web/.eslintrc.js
Normal file
17
web/.eslintrc.js
Normal file
@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
// add more generic rulesets here, such as:
|
||||
// 'eslint:recommended',
|
||||
"plugin:vue/vue3-recommended",
|
||||
// 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
|
||||
],
|
||||
rules: {
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
},
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
sourceType: "module",
|
||||
},
|
||||
};
|
@ -1,13 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body
|
||||
class="bg-slate-200 text-slate-800 dark:bg-slate-800 dark:text-slate-200 box-border w-full">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,33 +1,44 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"serve": "vite preview",
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "5.9.55",
|
||||
"@vueuse/core": "^7.6.1",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"pinia": "^2.0.11",
|
||||
"postcss": "^8.4.6",
|
||||
"tailwindcss": "^3.0.18",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@vitejs/plugin-vue": "^2.0.0",
|
||||
"@vue/cli-plugin-babel": "5.0.0-beta.7",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-service": "5.0.0-beta.7",
|
||||
"sass": "^1.38.0",
|
||||
"sass-loader": "^10.0.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.7.2",
|
||||
"vue-tsc": "^0.32.0"
|
||||
}
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"serve": "vite preview",
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "5.9.55",
|
||||
"@vueuse/core": "^7.6.1",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"pinia": "^2.0.11",
|
||||
"postcss": "^8.4.6",
|
||||
"tailwindcss": "^3.0.18",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"@vitejs/plugin-vue": "^2.0.0",
|
||||
"@vue/cli-plugin-babel": "5.0.0-beta.7",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-service": "5.0.0-beta.7",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"prettier": "2.5.1",
|
||||
"sass": "^1.38.0",
|
||||
"sass-loader": "^10.0.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.7.2",
|
||||
"vue-tsc": "^0.32.0"
|
||||
},
|
||||
"prettier": {
|
||||
"bracketSameLine": true,
|
||||
"embeddedLanguageFormatting": "off",
|
||||
"tabWidth": 4,
|
||||
"useTabs": false
|
||||
},
|
||||
"eslintIgnore": ["index.css"]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
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" {
|
||||
import { defineComponent } from "vue";
|
||||
const component: ReturnType<typeof defineComponent>;
|
||||
export default component;
|
||||
import { defineComponent } from "vue";
|
||||
const component: ReturnType<typeof defineComponent>;
|
||||
export default component;
|
||||
}
|
@ -26,29 +26,50 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col md:flex-row flex-1 h-screen">
|
||||
<router-view name="sidebar"></router-view>
|
||||
<div class="flex flex-col md:flex-row flex-1 h-screen">
|
||||
<router-view name="sidebar" />
|
||||
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14">
|
||||
<span
|
||||
class="flex-1 font-bold text-5xl -my-3 hidden md:inline"
|
||||
@click="toggleMenuSize"
|
||||
>≡</span>
|
||||
<span class="flex-1 font-bold text-5xl -my-3 md:hidden" @click="toggleMenu">≡</span>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div
|
||||
class="flex bg-gray-400 dark:bg-gray-600 p-4 fixed md:static top-0 left-0 w-full h-14"
|
||||
>
|
||||
<span
|
||||
class="flex-1 font-bold text-5xl -my-3 hidden md:inline"
|
||||
@click="toggleMenuSize"
|
||||
>≡</span>
|
||||
<span
|
||||
class="flex-1 font-bold text-5xl -my-3 md:hidden"
|
||||
@click="toggleMenu"
|
||||
>≡</span>
|
||||
|
||||
<span class="flex-1">{{ CurrentBudgetName }}</span>
|
||||
<span class="flex-1">{{ CurrentBudgetName }}</span>
|
||||
|
||||
<div class="flex flex-1 flex-row justify-end -mx-4">
|
||||
<router-link class="mx-4" v-if="LoggedIn" to="/dashboard">Dashboard</router-link>
|
||||
<router-link class="mx-4" v-if="!LoggedIn" to="/login">Login</router-link>
|
||||
<a class="mx-4" v-if="LoggedIn" @click="logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 pl-6">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-row justify-end -mx-4">
|
||||
<router-link
|
||||
v-if="LoggedIn"
|
||||
class="mx-4"
|
||||
to="/dashboard"
|
||||
>
|
||||
Dashboard
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="!LoggedIn"
|
||||
class="mx-4"
|
||||
to="/login"
|
||||
>
|
||||
Login
|
||||
</router-link>
|
||||
<a
|
||||
v-if="LoggedIn"
|
||||
class="mx-4"
|
||||
@click="logout"
|
||||
>Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 pl-6">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,13 +1,13 @@
|
||||
import { useSessionStore } from "./stores/session";
|
||||
|
||||
export const BASE_URL = "/api/v1"
|
||||
export const BASE_URL = "/api/v1";
|
||||
|
||||
export function GET(path: string) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch(BASE_URL + path, {
|
||||
headers: sessionStore.AuthHeaders,
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function POST(path: string, body: FormData | string | null) {
|
||||
const sessionStore = useSessionStore();
|
||||
@ -15,12 +15,12 @@ export function POST(path: string, body: FormData | string | null) {
|
||||
method: "POST",
|
||||
headers: sessionStore.AuthHeaders,
|
||||
body: body,
|
||||
})
|
||||
});
|
||||
}
|
||||
export function DELETE(path: string) {
|
||||
const sessionStore = useSessionStore();
|
||||
return fetch(BASE_URL + path, {
|
||||
method: "DELETE",
|
||||
headers: sessionStore.AuthHeaders,
|
||||
})
|
||||
});
|
||||
}
|
@ -20,12 +20,16 @@ function daysSinceLastReconciled() {
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID">
|
||||
{{account.Name}}
|
||||
</router-link>
|
||||
<span v-if="props.account.LastReconciled.Valid && daysSinceLastReconciled() > 7" class="font-bold bg-gray-500 rounded-md text-sm px-2 mx-2 py-1 no-underline">
|
||||
{{daysSinceLastReconciled()}}
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/account/' + account.ID"
|
||||
>
|
||||
{{ account.Name }}
|
||||
</router-link>
|
||||
<span
|
||||
v-if="props.account.LastReconciled.Valid && daysSinceLastReconciled() > 7"
|
||||
class="font-bold bg-gray-500 rounded-md text-sm px-2 mx-2 py-1 no-underline"
|
||||
>
|
||||
{{ daysSinceLastReconciled() }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
@ -11,9 +11,9 @@ export interface Suggestion {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
text: String,
|
||||
id: String | undefined,
|
||||
model: String,
|
||||
text: string,
|
||||
id: string | undefined,
|
||||
model: string,
|
||||
type?: string | undefined,
|
||||
}>();
|
||||
|
||||
@ -81,22 +81,30 @@ function clear() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
class="border-b-2 border-black"
|
||||
@keypress="keypress"
|
||||
v-if="id == undefined"
|
||||
v-model="SearchQuery"
|
||||
/>
|
||||
<span @click="clear" v-if="id != undefined" class="bg-gray-300 dark:bg-gray-700">{{ text }}</span>
|
||||
<div v-if="Suggestions.length > 0" class="absolute bg-gray-400 dark:bg-gray-600 w-64 p-2">
|
||||
<span
|
||||
v-for="suggestion in Suggestions"
|
||||
class="block"
|
||||
@click="select"
|
||||
:value="suggestion.ID"
|
||||
>{{ suggestion.Name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
v-if="id == undefined"
|
||||
v-model="SearchQuery"
|
||||
type="text"
|
||||
class="border-b-2 border-black"
|
||||
@keypress="keypress"
|
||||
/>
|
||||
<span
|
||||
v-if="id != undefined"
|
||||
class="bg-gray-300 dark:bg-gray-700"
|
||||
@click="clear"
|
||||
>{{ text }}</span>
|
||||
<div
|
||||
v-if="Suggestions.length > 0"
|
||||
class="absolute bg-gray-400 dark:bg-gray-600 w-64 p-2"
|
||||
>
|
||||
<span
|
||||
v-for="suggestion in Suggestions"
|
||||
:key="suggestion.ID"
|
||||
class="block"
|
||||
:value="suggestion.ID"
|
||||
@click="select"
|
||||
>{{ suggestion.Name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,10 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="px-4 rounded-md shadow-sm focus:outline-none focus:ring-2"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
@ -1,8 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row items-center bg-gray-300 dark:bg-gray-700 rounded-lg">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
@ -1,11 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(["modelValue"]);
|
||||
const props = defineProps<{modelValue?: boolean}>();
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", value: boolean): void
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue"
|
||||
@change="$emit('update:modelValue', ($event.target as HTMLInputElement)?.checked)"
|
||||
class="dark:bg-slate-900">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue"
|
||||
class="dark:bg-slate-900"
|
||||
@change="emits('update:modelValue', ($event.target as HTMLInputElement)?.checked)"
|
||||
>
|
||||
</template>
|
@ -15,5 +15,8 @@ const formattedValue = computed(() => internalValue.value.toLocaleString(undefin
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="text-right" :class="internalValue < 0 ? (negativeClass ?? 'negative') : positiveClass">{{ formattedValue }} €</span>
|
||||
<span
|
||||
class="text-right"
|
||||
:class="internalValue < 0 ? (negativeClass ?? 'negative') : positiveClass"
|
||||
>{{ formattedValue }} €</span>
|
||||
</template>
|
@ -1,9 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import Input from './Input.vue';
|
||||
const props = defineProps(["modelValue"]);
|
||||
const props = defineProps<{
|
||||
modelValue?: Date
|
||||
}>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function dateToYYYYMMDD(d: Date) : string {
|
||||
function dateToYYYYMMDD(d: Date | undefined) : string {
|
||||
if(d == null)
|
||||
return "";
|
||||
// alternative implementations in https://stackoverflow.com/q/23593052/1850609
|
||||
@ -27,9 +29,9 @@ function selectAll(event: FocusEvent) {
|
||||
|
||||
<template>
|
||||
<Input
|
||||
type="date"
|
||||
ref="input"
|
||||
v-bind:value="dateToYYYYMMDD(modelValue)"
|
||||
type="date"
|
||||
:value="dateToYYYYMMDD(modelValue)"
|
||||
@input="updateValue"
|
||||
@focus="selectAll"
|
||||
/>
|
||||
|
@ -1,10 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(["modelValue"]);
|
||||
const props = defineProps<{
|
||||
modelValue?: number | string
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", value: number | string): void
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement)?.value)"
|
||||
class="dark:bg-slate-900">
|
||||
<input
|
||||
:value="modelValue"
|
||||
class="dark:bg-slate-900"
|
||||
@input="emits('update:modelValue', ($event.target as HTMLInputElement)?.value)"
|
||||
>
|
||||
</template>
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import Card from '../components/Card.vue';
|
||||
import RowCard from './RowCard.vue';
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
@ -30,33 +30,48 @@ function submitDialog() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="openDialog">
|
||||
<slot name="placeholder">
|
||||
<Card>
|
||||
<p class="w-24 text-center text-6xl">+</p>
|
||||
<span class="text-lg" dark>{{ buttonText }}</span>
|
||||
</Card>
|
||||
</slot>
|
||||
</button>
|
||||
<button @click="openDialog">
|
||||
<slot name="placeholder">
|
||||
<RowCard>
|
||||
<p class="w-24 text-center text-6xl">
|
||||
+
|
||||
</p>
|
||||
<span
|
||||
class="text-lg"
|
||||
dark
|
||||
>{{ buttonText }}</span>
|
||||
</RowCard>
|
||||
</slot>
|
||||
</button>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
|
||||
>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
|
||||
class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black"
|
||||
>
|
||||
<div class="relative top-20 mx-auto p-5 w-96 shadow-lg rounded-md bg-white dark:bg-black">
|
||||
<div class="mt-3 text-center">
|
||||
<h3 class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{{ buttonText }}</h3>
|
||||
<slot></slot>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<button
|
||||
@click="closeDialog"
|
||||
class="px-4 py-2 bg-red-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
|
||||
>Close</button>
|
||||
<button
|
||||
@click="submitDialog"
|
||||
class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
|
||||
>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<h3
|
||||
class="mt-3 text-lg leading-6 font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</h3>
|
||||
<slot />
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<button
|
||||
class="px-4 py-2 bg-red-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
|
||||
@click="closeDialog"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 bg-green-500 text-white text-base font-medium rounded-md shadow-sm hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-300"
|
||||
@click="submitDialog"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
9
web/src/components/RowCard.vue
Normal file
9
web/src/components/RowCard.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-row items-center bg-gray-300 dark:bg-gray-700 rounded-lg"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
7
web/src/components/SimpleButton.vue
Normal file
7
web/src/components/SimpleButton.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<button class="px-4 rounded-md shadow-sm focus:outline-none focus:ring-2">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
@ -5,7 +5,7 @@ import { useAccountStore } from '../stores/budget-account'
|
||||
import DateInput from "./DateInput.vue";
|
||||
import { useTransactionsStore } from "../stores/transactions";
|
||||
import Input from "./Input.vue";
|
||||
import Button from "./Button.vue";
|
||||
import Button from "./SimpleButton.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
transactionid: string
|
||||
@ -38,29 +38,50 @@ function saveTransaction(e: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td class="text-sm">
|
||||
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
|
||||
</td>
|
||||
<td>
|
||||
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
|
||||
</td>
|
||||
<td>
|
||||
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
|
||||
</td>
|
||||
<td>
|
||||
<Input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<Input
|
||||
class="text-right block w-full border-b-2 border-black"
|
||||
type="currency"
|
||||
v-model="TX.Amount"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-sm">
|
||||
<DateInput
|
||||
v-model="TX.Date"
|
||||
class="border-b-2 border-black"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Autocomplete
|
||||
v-model:text="TX.Payee"
|
||||
v-model:id="TX.PayeeID"
|
||||
v-model:type="payeeType"
|
||||
model="payees"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Autocomplete
|
||||
v-model:text="TX.Category"
|
||||
v-model:id="TX.CategoryID"
|
||||
model="categories"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
v-model="TX.Memo"
|
||||
class="block w-full border-b-2 border-black"
|
||||
type="text"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<Input
|
||||
v-model="TX.Amount"
|
||||
class="text-right block w-full border-b-2 border-black"
|
||||
type="currency"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
class="bg-blue-500"
|
||||
@click="saveTransaction"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
</template>
|
@ -3,7 +3,7 @@ import { computed, ref } from "vue";
|
||||
import Autocomplete from '../components/Autocomplete.vue'
|
||||
import { Transaction, useTransactionsStore } from "../stores/transactions";
|
||||
import DateInput from "./DateInput.vue";
|
||||
import Button from "./Button.vue";
|
||||
import Button from "./SimpleButton.vue";
|
||||
import Input from "./Input.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
@ -52,32 +52,53 @@ function saveTransaction(e: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<label class="md:hidden">Date</label>
|
||||
<td class="text-sm">
|
||||
<DateInput class="border-b-2 border-black" v-model="TX.Date" />
|
||||
</td>
|
||||
<label class="md:hidden">Payee</label>
|
||||
<td>
|
||||
<Autocomplete v-model:text="TX.Payee" v-model:id="TX.PayeeID" v-model:type="payeeType" model="payees" />
|
||||
</td>
|
||||
<label class="md:hidden">Category</label>
|
||||
<td>
|
||||
<Autocomplete v-model:text="TX.Category" v-model:id="TX.CategoryID" model="categories" />
|
||||
</td>
|
||||
<td class="col-span-2">
|
||||
<Input class="block w-full border-b-2 border-black" type="text" v-model="TX.Memo" />
|
||||
</td>
|
||||
<label class="md:hidden">Amount</label>
|
||||
<td class="text-right">
|
||||
<Input
|
||||
class="text-right block w-full border-b-2 border-black"
|
||||
type="currency"
|
||||
v-model="TX.Amount"
|
||||
/>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<Button class="bg-blue-500" @click="saveTransaction">Save</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<label class="md:hidden">Date</label>
|
||||
<td class="text-sm">
|
||||
<DateInput
|
||||
v-model="TX.Date"
|
||||
class="border-b-2 border-black"
|
||||
/>
|
||||
</td>
|
||||
<label class="md:hidden">Payee</label>
|
||||
<td>
|
||||
<Autocomplete
|
||||
v-model:text="TX.Payee"
|
||||
v-model:id="TX.PayeeID"
|
||||
v-model:type="payeeType"
|
||||
model="payees"
|
||||
/>
|
||||
</td>
|
||||
<label class="md:hidden">Category</label>
|
||||
<td>
|
||||
<Autocomplete
|
||||
v-model:text="TX.Category"
|
||||
v-model:id="TX.CategoryID"
|
||||
model="categories"
|
||||
/>
|
||||
</td>
|
||||
<td class="col-span-2">
|
||||
<Input
|
||||
v-model="TX.Memo"
|
||||
class="block w-full border-b-2 border-black"
|
||||
type="text"
|
||||
/>
|
||||
</td>
|
||||
<label class="md:hidden">Amount</label>
|
||||
<td class="text-right">
|
||||
<Input
|
||||
v-model="TX.Amount"
|
||||
class="text-right block w-full border-b-2 border-black"
|
||||
type="currency"
|
||||
/>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<Button
|
||||
class="bg-blue-500"
|
||||
@click="saveTransaction"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
@ -47,34 +47,58 @@ function getStatusSymbol() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr v-if="dateChanged()" class="table-row md:hidden">
|
||||
<td class="bg-gray-200 dark:bg-gray-800 rounded-lg p-2" colspan="5">{{ formatDate(TX.Date) }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="!edit"
|
||||
class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
|
||||
:class="[index % 6 < 3 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']"
|
||||
<tr
|
||||
v-if="dateChanged()"
|
||||
class="table-row md:hidden"
|
||||
>
|
||||
<td
|
||||
class="bg-gray-200 dark:bg-gray-800 rounded-lg p-2"
|
||||
colspan="5"
|
||||
>
|
||||
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
|
||||
<td class="hidden md:block">{{ formatDate(TX.Date) }}</td>
|
||||
<td class="pl-2 md:pl-0">{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}</td>
|
||||
<td>{{ TX.CategoryGroup ? TX.CategoryGroup + " : " + TX.Category : "" }}</td>
|
||||
<td>
|
||||
<a
|
||||
:href="'/budget/' + CurrentBudgetID + '/transaction/' + TX.ID"
|
||||
>{{ TX.Memo }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<Currency class="block" :value="TX.Amount" />
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ TX.GroupID ? "☀" : "" }}
|
||||
{{ getStatusSymbol() }}
|
||||
<a @click="edit = true;">✎</a>
|
||||
<Checkbox v-if="Reconciling && TX.Status != 'Reconciled'" v-model="TX.Reconciled" />
|
||||
</td>
|
||||
</tr>
|
||||
<TransactionEditRow v-if="edit" :transactionid="TX.ID" @save="edit = false" />
|
||||
{{ formatDate(TX.Date) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-if="!edit"
|
||||
class="{{new Date(TX.Date) > new Date() ? 'future' : ''}}"
|
||||
:class="[index % 6 < 3 ? 'md:bg-gray-300 dark:md:bg-gray-700' : 'md:bg-gray-100 dark:md:bg-gray-900']"
|
||||
>
|
||||
<!--:class="[index % 6 < 3 ? index % 6 === 1 ? 'bg-gray-400' : 'bg-gray-300' : index % 6 !== 4 ? 'bg-gray-100' : '']">-->
|
||||
<td class="hidden md:block">
|
||||
{{ formatDate(TX.Date) }}
|
||||
</td>
|
||||
<td class="pl-2 md:pl-0">
|
||||
{{ TX.TransferAccount ? "Transfer : " + TX.TransferAccount : TX.Payee }}
|
||||
</td>
|
||||
<td>
|
||||
{{ TX.CategoryGroup ? TX.CategoryGroup + " : " + TX.Category : "" }}
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
:href="'/budget/' + CurrentBudgetID + '/transaction/' + TX.ID"
|
||||
>{{ TX.Memo }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<Currency
|
||||
class="block"
|
||||
:value="TX.Amount"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ TX.GroupID ? "☀" : "" }}
|
||||
{{ getStatusSymbol() }}
|
||||
<a @click="edit = true;">✎</a>
|
||||
<Checkbox
|
||||
v-if="Reconciling && TX.Status != 'Reconciled'"
|
||||
v-model="TX.Reconciled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<TransactionEditRow
|
||||
v-if="edit"
|
||||
:transactionid="TX.ID"
|
||||
@save="edit = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
@ -1,5 +1,6 @@
|
||||
export function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString(undefined, { // you can use undefined as first argument
|
||||
return date.toLocaleDateString(undefined, {
|
||||
// you can use undefined as first argument
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
|
@ -42,35 +42,44 @@ function openEditAccount(e : any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal button-text="Edit Account" @open="openEditAccount" @submit="editAccount">
|
||||
<template v-slot:placeholder><span class="ml-2">✎</span></template>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Input
|
||||
class="border-2 dark:border-gray-700"
|
||||
type="text"
|
||||
v-model="accountName"
|
||||
placeholder="Account name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Checkbox
|
||||
class="border-2"
|
||||
v-model="accountOnBudget"
|
||||
required
|
||||
/>
|
||||
<label>On Budget</label>
|
||||
</div>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Checkbox
|
||||
class="border-2"
|
||||
v-model="accountOpen"
|
||||
required
|
||||
/>
|
||||
<label>Open</label>
|
||||
</div>
|
||||
<div v-if="error != ''" class="dark:text-red-300 text-red-700">
|
||||
{{ error }}
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
button-text="Edit Account"
|
||||
@open="openEditAccount"
|
||||
@submit="editAccount"
|
||||
>
|
||||
<template #placeholder>
|
||||
<span class="ml-2">✎</span>
|
||||
</template>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Input
|
||||
v-model="accountName"
|
||||
class="border-2 dark:border-gray-700"
|
||||
type="text"
|
||||
placeholder="Account name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Checkbox
|
||||
v-model="accountOnBudget"
|
||||
class="border-2"
|
||||
required
|
||||
/>
|
||||
<label>On Budget</label>
|
||||
</div>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Checkbox
|
||||
v-model="accountOpen"
|
||||
class="border-2"
|
||||
required
|
||||
/>
|
||||
<label>Open</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="error != ''"
|
||||
class="dark:text-red-300 text-red-700"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
@ -11,9 +11,18 @@ function saveBudget() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal button-text="New Budget" @submit="saveBudget">
|
||||
<Modal
|
||||
button-text="New Budget"
|
||||
@submit="saveBudget"
|
||||
>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<Input class="border-2" type="text" v-model="budgetName" placeholder="Budget name" required />
|
||||
<Input
|
||||
v-model="budgetName"
|
||||
class="border-2"
|
||||
type="text"
|
||||
placeholder="Budget name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
@ -3,11 +3,11 @@
|
||||
@tailwind utilities;
|
||||
|
||||
h1 {
|
||||
font-size: 200%;
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#app {
|
||||
|
117
web/src/main.ts
117
web/src/main.ts
@ -1,72 +1,79 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './index.css'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import { useBudgetsStore } from './stores/budget';
|
||||
import { useAccountStore } from './stores/budget-account'
|
||||
import PiniaLogger from './pinia-logger'
|
||||
import { useSessionStore } from './stores/session'
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./index.css";
|
||||
import router from "./router";
|
||||
import { createPinia } from "pinia";
|
||||
import { useBudgetsStore } from "./stores/budget";
|
||||
import { useAccountStore } from "./stores/budget-account";
|
||||
import PiniaLogger from "./pinia-logger";
|
||||
import { useSessionStore } from "./stores/session";
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(PiniaLogger({
|
||||
expanded: false,
|
||||
showDuration: true
|
||||
}))
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
const pinia = createPinia();
|
||||
pinia.use(
|
||||
PiniaLogger({
|
||||
expanded: false,
|
||||
showDuration: true,
|
||||
})
|
||||
);
|
||||
app.use(pinia);
|
||||
app.mount("#app");
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const budgetStore = useBudgetsStore();
|
||||
await budgetStore.SetCurrentBudget((<string>to.params.budgetid));
|
||||
const budgetStore = useBudgetsStore();
|
||||
await budgetStore.SetCurrentBudget(<string>to.params.budgetid);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
await accountStore.SetCurrentAccount((<string>to.params.budgetid), (<string>to.params.accountid));
|
||||
next();
|
||||
})
|
||||
const accountStore = useAccountStore();
|
||||
await accountStore.SetCurrentAccount(
|
||||
<string>to.params.budgetid,
|
||||
<string>to.params.accountid
|
||||
);
|
||||
next();
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const sessionStore = useSessionStore();
|
||||
const token = sessionStore.Session?.Token;
|
||||
let loggedIn = false;
|
||||
const sessionStore = useSessionStore();
|
||||
const token = sessionStore.Session?.Token;
|
||||
let loggedIn = false;
|
||||
|
||||
if (token != null) {
|
||||
const jwt = parseJwt(token);
|
||||
if (jwt.exp > Date.now() / 1000)
|
||||
loggedIn = true;
|
||||
}
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (!loggedIn) {
|
||||
next({ path: '/login' });
|
||||
} else {
|
||||
next();
|
||||
if (token != null) {
|
||||
const jwt = parseJwt(token);
|
||||
if (jwt.exp > Date.now() / 1000) loggedIn = true;
|
||||
}
|
||||
|
||||
} else if (to.matched.some(record => record.meta.hideForAuth)) {
|
||||
if (loggedIn) {
|
||||
next({ path: '/dashboard' });
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
if (!loggedIn) {
|
||||
next({ path: "/login" });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else if (to.matched.some((record) => record.meta.hideForAuth)) {
|
||||
if (loggedIn) {
|
||||
next({ path: "/dashboard" });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
function parseJwt(token: string) {
|
||||
var base64Url = token.split('.')[1];
|
||||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
var base64Url = token.split(".")[1];
|
||||
var base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
var jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map(function (c) {
|
||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
};
|
||||
return JSON.parse(jsonPayload);
|
||||
}
|
||||
|
||||
|
||||
1646426130
|
||||
1646512855755
|
||||
1646426130;
|
||||
1646512855755;
|
||||
|
@ -5,10 +5,11 @@ import TransactionRow from "../components/TransactionRow.vue";
|
||||
import TransactionInputRow from "../components/TransactionInputRow.vue";
|
||||
import { useAccountStore } from "../stores/budget-account";
|
||||
import EditAccount from "../dialogs/EditAccount.vue";
|
||||
import Button from "../components/Button.vue";
|
||||
import Button from "../components/SimpleButton.vue";
|
||||
import { useTransactionsStore } from "../stores/transactions";
|
||||
import Modal from "../components/Modal.vue";
|
||||
import Input from "../components/Input.vue";
|
||||
import Checkbox from "../components/Checkbox.vue";
|
||||
|
||||
defineProps<{
|
||||
budgetid: string
|
||||
@ -42,94 +43,124 @@ function createReconcilationTransaction() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<h1 class="inline">
|
||||
{{ accounts.CurrentAccount?.Name }}
|
||||
</h1>
|
||||
<EditAccount />
|
||||
</div>
|
||||
|
||||
<div class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm">
|
||||
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
|
||||
Working:
|
||||
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
|
||||
Cleared:
|
||||
<Currency :value="accounts.CurrentAccount?.ClearedBalance" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="rounded-lg bg-blue-500 p-1 whitespace-nowrap flex-1"
|
||||
v-if="!transactions.Reconciling"
|
||||
@click="transactions.Reconciling = true"
|
||||
>
|
||||
Reconciled:
|
||||
<Currency :value="accounts.CurrentAccount?.ReconciledBalance" />
|
||||
</span>
|
||||
|
||||
<span v-if="transactions.Reconciling" class="contents">
|
||||
<Button @click="submitReconcilation"
|
||||
class="bg-blue-500 p-1 whitespace-nowrap flex-1">
|
||||
My current balance is
|
||||
<Currency :value="transactions.ReconcilingBalance" />
|
||||
</Button>
|
||||
|
||||
<Button @click="createReconcilationTransaction"
|
||||
class="bg-orange-500 p-1 whitespace-nowrap flex-1">
|
||||
No, it's:
|
||||
<Input
|
||||
class="text-right w-20 bg-transparent dark:bg-transparent border-b-2"
|
||||
type="number"
|
||||
v-model="TargetReconcilingBalance"
|
||||
/>
|
||||
(Difference
|
||||
<Currency
|
||||
:value="transactions.ReconcilingBalance - TargetReconcilingBalance"
|
||||
/>)
|
||||
</Button>
|
||||
<Button class="bg-red-500 p-1 flex-1" @click="cancelReconcilation">Cancel</Button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<h1 class="inline">
|
||||
{{ accounts.CurrentAccount?.Name }}
|
||||
</h1>
|
||||
<EditAccount />
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr class="font-bold">
|
||||
<td class="hidden md:block" style="width: 90px;">Date</td>
|
||||
<td style="max-width: 150px;">Payee</td>
|
||||
<td style="max-width: 200px;">Category</td>
|
||||
<td>Memo</td>
|
||||
<td class="text-right">Amount</td>
|
||||
<td style="width: 80px;">
|
||||
<Input v-if="transactions.Reconciling" type="checkbox" @input="setReconciled" />
|
||||
</td>
|
||||
</tr>
|
||||
<TransactionInputRow
|
||||
class="hidden md:table-row"
|
||||
:budgetid="budgetid"
|
||||
:accountid="accountid"
|
||||
/>
|
||||
<TransactionRow
|
||||
v-for="(transaction, index) in transactions.TransactionsList"
|
||||
:key="transaction.ID"
|
||||
:transactionid="transaction.ID"
|
||||
:index="index"
|
||||
/>
|
||||
</table>
|
||||
<div class="md:hidden">
|
||||
<Modal>
|
||||
<template v-slot:placeholder>
|
||||
<Button class="fixed right-4 bottom-4 font-bold text-lg bg-blue-500 py-2">+</Button>
|
||||
</template>
|
||||
<TransactionInputRow
|
||||
class="grid grid-cols-2"
|
||||
:budgetid="budgetid"
|
||||
:accountid="accountid"
|
||||
/>
|
||||
</Modal>
|
||||
<div
|
||||
class="text-right flex flex-wrap flex-col md:flex-row justify-end gap-2 max-w-sm"
|
||||
>
|
||||
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
|
||||
Working:
|
||||
<Currency :value="accounts.CurrentAccount?.WorkingBalance" />
|
||||
</span>
|
||||
|
||||
<span class="rounded-lg p-1 whitespace-nowrap flex-1">
|
||||
Cleared:
|
||||
<Currency :value="accounts.CurrentAccount?.ClearedBalance" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!transactions.Reconciling"
|
||||
class="rounded-lg bg-blue-500 p-1 whitespace-nowrap flex-1"
|
||||
@click="transactions.Reconciling = true"
|
||||
>
|
||||
Reconciled:
|
||||
<Currency :value="accounts.CurrentAccount?.ReconciledBalance" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="transactions.Reconciling"
|
||||
class="contents"
|
||||
>
|
||||
<Button
|
||||
class="bg-blue-500 p-1 whitespace-nowrap flex-1"
|
||||
@click="submitReconcilation"
|
||||
>
|
||||
My current balance is
|
||||
<Currency :value="transactions.ReconcilingBalance" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
class="bg-orange-500 p-1 whitespace-nowrap flex-1"
|
||||
@click="createReconcilationTransaction"
|
||||
>
|
||||
No, it's:
|
||||
<Input
|
||||
v-model="TargetReconcilingBalance"
|
||||
class="text-right w-20 bg-transparent dark:bg-transparent border-b-2"
|
||||
type="number"
|
||||
/>
|
||||
(Difference
|
||||
<Currency
|
||||
:value="transactions.ReconcilingBalance - TargetReconcilingBalance"
|
||||
/>)
|
||||
</Button>
|
||||
<Button
|
||||
class="bg-red-500 p-1 flex-1"
|
||||
@click="cancelReconcilation"
|
||||
>Cancel</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr class="font-bold">
|
||||
<td
|
||||
class="hidden md:block"
|
||||
style="width: 90px;"
|
||||
>
|
||||
Date
|
||||
</td>
|
||||
<td style="max-width: 150px;">
|
||||
Payee
|
||||
</td>
|
||||
<td style="max-width: 200px;">
|
||||
Category
|
||||
</td>
|
||||
<td>Memo</td>
|
||||
<td class="text-right">
|
||||
Amount
|
||||
</td>
|
||||
<td style="width: 80px;">
|
||||
<Checkbox
|
||||
v-if="transactions.Reconciling"
|
||||
@input="setReconciled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<TransactionInputRow
|
||||
class="hidden md:table-row"
|
||||
:budgetid="budgetid"
|
||||
:accountid="accountid"
|
||||
/>
|
||||
<TransactionRow
|
||||
v-for="(transaction, index) in transactions.TransactionsList"
|
||||
:key="transaction.ID"
|
||||
:transactionid="transaction.ID"
|
||||
:index="index"
|
||||
/>
|
||||
</table>
|
||||
<div class="md:hidden">
|
||||
<Modal>
|
||||
<template #placeholder>
|
||||
<Button
|
||||
class="fixed right-4 bottom-4 font-bold text-lg bg-blue-500 py-2"
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</template>
|
||||
<TransactionInputRow
|
||||
class="grid grid-cols-2"
|
||||
:budgetid="budgetid"
|
||||
:accountid="accountid"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
@ -8,9 +8,9 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Danger Zone</h1>
|
||||
<div class="budget-item">
|
||||
<button>Clear database</button>
|
||||
<p>This removes all data and starts from scratch. Not undoable!</p>
|
||||
</div>
|
||||
<h1>Danger Zone</h1>
|
||||
<div class="budget-item">
|
||||
<button>Clear database</button>
|
||||
<p>This removes all data and starts from scratch. Not undoable!</p>
|
||||
</div>
|
||||
</template>
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import Currency from "../components/Currency.vue"
|
||||
import { useBudgetsStore } from "../stores/budget"
|
||||
import { Account, useAccountStore } from "../stores/budget-account"
|
||||
import { useSettingsStore } from "../stores/settings"
|
||||
import Currency from "../components/Currency.vue";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
import { Account, useAccountStore } from "../stores/budget-account";
|
||||
import { useSettingsStore } from "../stores/settings";
|
||||
import AccountWithReconciled from "../components/AccountWithReconciled.vue";
|
||||
|
||||
const settings = useSettingsStore();
|
||||
@ -31,33 +31,56 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
|
||||
class="m-2 p-1 px-3 h-10 overflow-hidden"
|
||||
:class="[ExpandMenu ? 'text-2xl' : 'text-md']"
|
||||
>
|
||||
<router-link to="/dashboard" style="font-size:150%">⌂</router-link>
|
||||
<router-link
|
||||
to="/dashboard"
|
||||
style="font-size: 150%"
|
||||
>⌂</router-link>
|
||||
{{ CurrentBudgetName }}
|
||||
</span>
|
||||
<span class="bg-gray-100 dark:bg-gray-700 p-2 px-3 flex flex-col">
|
||||
<router-link :to="'/budget/' + CurrentBudgetID + '/budgeting'">Budget</router-link>
|
||||
<br />
|
||||
<br>
|
||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/reports'">Reports</router-link>-->
|
||||
<!--<router-link :to="'/budget/'+CurrentBudgetID+'/all-accounts'">All Accounts</router-link>-->
|
||||
</span>
|
||||
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
||||
<div class="flex flex-row justify-between font-bold">
|
||||
<span>On-Budget Accounts</span>
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OnBudgetAccountsBalance" />
|
||||
<Currency
|
||||
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
|
||||
:value="OnBudgetAccountsBalance"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="account in OnBudgetAccounts" class="flex flex-row justify-between">
|
||||
<div
|
||||
v-for="account in OnBudgetAccounts"
|
||||
:key="account.ID"
|
||||
class="flex flex-row justify-between"
|
||||
>
|
||||
<AccountWithReconciled :account="account" />
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
|
||||
<Currency
|
||||
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
|
||||
:value="account.ClearedBalance"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li class="bg-slate-200 dark:bg-slate-700 my-2 p-2 px-3">
|
||||
<div class="flex flex-row justify-between font-bold">
|
||||
<span>Off-Budget Accounts</span>
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="OffBudgetAccountsBalance" />
|
||||
<Currency
|
||||
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
|
||||
:value="OffBudgetAccountsBalance"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="account in OffBudgetAccounts" class="flex flex-row justify-between">
|
||||
<div
|
||||
v-for="account in OffBudgetAccounts"
|
||||
:key="account.ID"
|
||||
class="flex flex-row justify-between"
|
||||
>
|
||||
<AccountWithReconciled :account="account" />
|
||||
<Currency :class="ExpandMenu ? 'md:inline' : 'md:hidden'" :value="account.ClearedBalance" />
|
||||
<Currency
|
||||
:class="ExpandMenu ? 'md:inline' : 'md:hidden'"
|
||||
:value="account.ClearedBalance"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<!--
|
||||
@ -72,7 +95,9 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa
|
||||
<router-link :to="'/budget/'+CurrentBudgetID+'/accounts'">Edit accounts</router-link>
|
||||
</li>-->
|
||||
<li class="bg-red-100 dark:bg-slate-600 my-2 p-2 px-3">
|
||||
<router-link :to="'/budget/' + CurrentBudgetID + '/settings'">Budget-Settings</router-link>
|
||||
<router-link :to="'/budget/' + CurrentBudgetID + '/settings'">
|
||||
Budget-Settings
|
||||
</router-link>
|
||||
</li>
|
||||
<!--<li><router-link to="/admin">Admin</router-link></li>-->
|
||||
</div>
|
||||
|
@ -75,41 +75,99 @@ function assignedChanged(e : Event, category : Category){
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
|
||||
<span>Available balance: <Currency :value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)" /></span>
|
||||
<div>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
||||
><<</router-link>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
|
||||
>Current Month</router-link>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
|
||||
>>></router-link>
|
||||
</div>
|
||||
<div class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5" id="content">
|
||||
<span class="hidden sm:block"></span>
|
||||
<span class="hidden lg:block text-right">Leftover</span>
|
||||
<span class="hidden sm:block text-right">Assigned</span>
|
||||
<span class="hidden sm:block text-right">Activity</span>
|
||||
<span class="hidden sm:block text-right">Available</span>
|
||||
<template v-for="group in GroupsForMonth">
|
||||
<span
|
||||
class="text-lg font-bold mt-2"
|
||||
@click="toggleGroup(group)"
|
||||
>{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</span>
|
||||
<Currency :value="group.AvailableLastMonth" class="hidden lg:block mt-2" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
|
||||
<Currency :value="group.Assigned" class="hidden sm:block mx-2 mt-2 text-right" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
|
||||
<Currency :value="group.Activity" class="hidden sm:block mt-2" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
|
||||
<Currency :value="group.Available" class="mt-2" positive-class="text-slate-500" negative-class="text-red-700 dark:text-red-400" />
|
||||
<template v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
|
||||
<span class="whitespace-nowrap overflow-hidden">{{ category.Name }}</span>
|
||||
<Currency :value="category.AvailableLastMonth" class="hidden lg:block" />
|
||||
<Input type="number" v-model="category.Assigned" @input="(evt) => assignedChanged(evt, category)" class="hidden sm:block mx-2 text-right" />
|
||||
<Currency :value="category.Activity" class="hidden sm:block" />
|
||||
<Currency :value="accountStore.GetCategoryAvailable(category)" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<h1>Budget for {{ selected.Month + 1 }}/{{ selected.Year }}</h1>
|
||||
<span>Available balance:
|
||||
<Currency
|
||||
:value="accountStore.GetIncomeAvailable(selected.Year, selected.Month)"
|
||||
/></span>
|
||||
<div>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + previous.Year + '/' + previous.Month"
|
||||
>
|
||||
<<
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + current.Year + '/' + current.Month"
|
||||
>
|
||||
Current Month
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="'/budget/' + CurrentBudgetID + '/budgeting/' + next.Year + '/' + next.Month"
|
||||
>
|
||||
>>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
id="content"
|
||||
class="container col-lg-12 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-5"
|
||||
>
|
||||
<span class="hidden sm:block" />
|
||||
<span class="hidden lg:block text-right">Leftover</span>
|
||||
<span class="hidden sm:block text-right">Assigned</span>
|
||||
<span class="hidden sm:block text-right">Activity</span>
|
||||
<span class="hidden sm:block text-right">Available</span>
|
||||
<template
|
||||
v-for="group in GroupsForMonth"
|
||||
:key="group.Name"
|
||||
>
|
||||
<span
|
||||
class="text-lg font-bold mt-2"
|
||||
@click="toggleGroup(group)"
|
||||
>{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</span>
|
||||
<Currency
|
||||
:value="group.AvailableLastMonth"
|
||||
class="hidden lg:block mt-2"
|
||||
positive-class="text-slate-500"
|
||||
negative-class="text-red-700 dark:text-red-400"
|
||||
/>
|
||||
<Currency
|
||||
:value="group.Assigned"
|
||||
class="hidden sm:block mx-2 mt-2 text-right"
|
||||
positive-class="text-slate-500"
|
||||
negative-class="text-red-700 dark:text-red-400"
|
||||
/>
|
||||
<Currency
|
||||
:value="group.Activity"
|
||||
class="hidden sm:block mt-2"
|
||||
positive-class="text-slate-500"
|
||||
negative-class="text-red-700 dark:text-red-400"
|
||||
/>
|
||||
<Currency
|
||||
:value="group.Available"
|
||||
class="mt-2"
|
||||
positive-class="text-slate-500"
|
||||
negative-class="text-red-700 dark:text-red-400"
|
||||
/>
|
||||
<template
|
||||
v-for="category in GetCategories(group.Name)"
|
||||
:key="category.ID"
|
||||
>
|
||||
<div
|
||||
v-if="getGroupState(group)"
|
||||
class="contents"
|
||||
>
|
||||
<span
|
||||
class="whitespace-nowrap overflow-hidden"
|
||||
>{{ category.Name }}</span>
|
||||
<Currency
|
||||
:value="category.AvailableLastMonth"
|
||||
class="hidden lg:block"
|
||||
/>
|
||||
<Input
|
||||
v-model="category.Assigned"
|
||||
type="number"
|
||||
class="hidden sm:block mx-2 text-right"
|
||||
@input="(evt) => assignedChanged(evt, category)"
|
||||
/>
|
||||
<Currency
|
||||
:value="category.Activity"
|
||||
class="hidden sm:block"
|
||||
/>
|
||||
<Currency
|
||||
:value="accountStore.GetCategoryAvailable(category)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import NewBudget from '../dialogs/NewBudget.vue';
|
||||
import Card from '../components/Card.vue';
|
||||
import RowCard from '../components/RowCard.vue';
|
||||
import { useSessionStore } from '../stores/session';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -11,15 +11,22 @@ const BudgetsList = useSessionStore().BudgetsList;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Budgets</h1>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<Card v-for="budget in BudgetsList">
|
||||
<router-link v-bind:to="'/budget/'+budget.ID+'/budgeting'" class="contents">
|
||||
<!--<svg class="w-24"></svg>-->
|
||||
<p class="w-24 text-center text-6xl"></p>
|
||||
<span class="text-lg">{{budget.Name}}{{budget.ID == budgetid ? " *" : ""}}</span>
|
||||
</router-link>
|
||||
</Card>
|
||||
<NewBudget />
|
||||
</div>
|
||||
<h1>Budgets</h1>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<RowCard
|
||||
v-for="budget in BudgetsList"
|
||||
:key="budget.ID"
|
||||
>
|
||||
<router-link
|
||||
:to="'/budget/'+budget.ID+'/budgeting'"
|
||||
class="contents"
|
||||
>
|
||||
<!--<svg class="w-24"></svg>-->
|
||||
<p class="w-24 text-center text-6xl" />
|
||||
<span class="text-lg">{{ budget.Name
|
||||
}}{{ budget.ID == budgetid ? " *" : "" }}</span>
|
||||
</router-link>
|
||||
</RowCard>
|
||||
<NewBudget />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="font-bold" id="content">
|
||||
Willkommen bei Budgeteer, der neuen App für's Budget!
|
||||
</div>
|
||||
<div class="container col-md-4" id="login">
|
||||
<router-link to="/login">Login</router-link> or <router-link to="/login">register</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -28,18 +28,32 @@ function formSubmit(e: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Input type="text" v-model="login.user"
|
||||
placeholder="Username"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
<Input type="password" v-model="login.password"
|
||||
placeholder="Password"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
</div>
|
||||
<div>{{ error }}</div>
|
||||
<button type="submit" @click="formSubmit" class="bg-blue-300 rounded-lg p-2 w-48">Login</button>
|
||||
<p>
|
||||
New user?
|
||||
<router-link to="/register">Register</router-link> instead!
|
||||
</p>
|
||||
<div>
|
||||
<Input
|
||||
v-model="login.user"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
<Input
|
||||
v-model="login.password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ error }}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-blue-300 rounded-lg p-2 w-48"
|
||||
@click="formSubmit"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<p>
|
||||
New user?
|
||||
<router-link to="/register">
|
||||
Register
|
||||
</router-link> instead!
|
||||
</p>
|
||||
</template>
|
@ -28,21 +28,38 @@ function formSubmit(e: MouseEvent) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Input type="text" v-model="login.name"
|
||||
placeholder="Name"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
<Input type="text" v-model="login.email"
|
||||
placeholder="Email"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
<Input type="password" v-model="login.password"
|
||||
placeholder="Password"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48" />
|
||||
</div>
|
||||
<div>{{ error }}</div>
|
||||
<button type="submit" @click="formSubmit" class="bg-blue-300 rounded-lg p-2 w-48">Register</button>
|
||||
<p>
|
||||
Existing user?
|
||||
<router-link to="/login">Login</router-link> instead!
|
||||
</p>
|
||||
<div>
|
||||
<Input
|
||||
v-model="login.name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
<Input
|
||||
v-model="login.email"
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
<Input
|
||||
v-model="login.password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
class="border-2 border-black rounded-lg block px-2 my-2 w-48"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ error }}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-blue-300 rounded-lg p-2 w-48"
|
||||
@click="formSubmit"
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
<p>
|
||||
Existing user?
|
||||
<router-link to="/login">
|
||||
Login
|
||||
</router-link> instead!
|
||||
</p>
|
||||
</template>
|
@ -4,8 +4,8 @@ import { useRouter } from "vue-router";
|
||||
import { DELETE, POST } from "../api";
|
||||
import { useBudgetsStore } from "../stores/budget";
|
||||
import { useSessionStore } from "../stores/session";
|
||||
import Card from "../components/Card.vue";
|
||||
import Button from "../components/Button.vue";
|
||||
import RowCard from "../components/RowCard.vue";
|
||||
import Button from "../components/SimpleButton.vue";
|
||||
import { saveAs } from 'file-saver';
|
||||
import Input from "../components/Input.vue";
|
||||
|
||||
@ -72,53 +72,105 @@ function ynabExport() {
|
||||
saveAs(blob, timeStamp + " " + CurrentBudgetName.value + " - Transactions.tsv");
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Danger Zone</h1>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<Card class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">Clear Budget</h2>
|
||||
<p>This removes transactions and assignments to start from scratch. Accounts and categories are kept. Not undoable!</p>
|
||||
<div>
|
||||
<h1>Danger Zone</h1>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<RowCard class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">
|
||||
Clear Budget
|
||||
</h2>
|
||||
<p>
|
||||
This removes transactions and assignments to start from
|
||||
scratch. Accounts and categories are kept. Not undoable!
|
||||
</p>
|
||||
|
||||
<Button class="bg-red-500 py-2" @click="clearBudget">Clear budget</Button>
|
||||
</Card>
|
||||
<Card class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">Delete Budget</h2>
|
||||
<p>This deletes the whole bugdet including all transactions, assignments, accounts and categories. Not undoable!</p>
|
||||
<Button class="bg-red-500 py-2" @click="deleteBudget">Delete budget</button>
|
||||
</Card>
|
||||
<Card class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">Fix all historic negative category-balances</h2>
|
||||
<p>This restores YNABs functionality, that would substract any overspent categories' balances from next months inflows.</p>
|
||||
<Button class="bg-orange-500 py-2" @click="cleanNegative">Fix negative</button>
|
||||
</Card>
|
||||
<Card class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">Import YNAB Budget</h2>
|
||||
<Button
|
||||
class="bg-red-500 py-2"
|
||||
@click="clearBudget"
|
||||
>
|
||||
Clear budget
|
||||
</Button>
|
||||
</RowCard>
|
||||
<RowCard class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">
|
||||
Delete Budget
|
||||
</h2>
|
||||
<p>
|
||||
This deletes the whole bugdet including all transactions,
|
||||
assignments, accounts and categories. Not undoable!
|
||||
</p>
|
||||
<Button
|
||||
class="bg-red-500 py-2"
|
||||
@click="deleteBudget"
|
||||
>
|
||||
Delete budget
|
||||
</Button>
|
||||
</RowCard>
|
||||
<RowCard class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">
|
||||
Fix all historic negative category-balances
|
||||
</h2>
|
||||
<p>
|
||||
This restores YNABs functionality, that would substract any
|
||||
overspent categories' balances from next months inflows.
|
||||
</p>
|
||||
<Button
|
||||
class="bg-orange-500 py-2"
|
||||
@click="cleanNegative"
|
||||
>
|
||||
Fix negative
|
||||
</Button>
|
||||
</RowCard>
|
||||
<RowCard class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">
|
||||
Import YNAB Budget
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<label for="transactions_file">
|
||||
Transaktionen:
|
||||
<input type="file" @change="gotTransactions" accept="text/*" />
|
||||
</label>
|
||||
<br />
|
||||
<label for="assignments_file">
|
||||
Budget:
|
||||
<input type="file" @change="gotAssignments" accept="text/*" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button class="bg-blue-500 py-2" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
|
||||
</Card>
|
||||
<Card class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">Export as YNAB TSV</h2>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<Button class="bg-blue-500 py-2" @click="ynabExport">Export</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
<label for="transactions_file">
|
||||
Transaktionen:
|
||||
<input
|
||||
type="file"
|
||||
accept="text/*"
|
||||
@change="gotTransactions"
|
||||
>
|
||||
</label>
|
||||
<br>
|
||||
<label for="assignments_file">
|
||||
Budget:
|
||||
<input
|
||||
type="file"
|
||||
accept="text/*"
|
||||
@change="gotAssignments"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="bg-blue-500 py-2"
|
||||
:disabled="filesIncomplete"
|
||||
@click="ynabImport"
|
||||
>
|
||||
Importieren
|
||||
</Button>
|
||||
</RowCard>
|
||||
<RowCard class="flex-col p-3">
|
||||
<h2 class="text-lg font-bold">
|
||||
Export as YNAB TSV
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<Button
|
||||
class="bg-blue-500 py-2"
|
||||
@click="ynabExport"
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</RowCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
24
web/src/pages/WelcomePage.vue
Normal file
24
web/src/pages/WelcomePage.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
id="content"
|
||||
class="font-bold"
|
||||
>
|
||||
Willkommen bei Budgeteer, der neuen App für's Budget!
|
||||
</div>
|
||||
<div
|
||||
id="login"
|
||||
class="container col-md-4"
|
||||
>
|
||||
<router-link to="/login">
|
||||
Login
|
||||
</router-link> or
|
||||
<router-link to="/login">
|
||||
register
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,81 +1,107 @@
|
||||
import { PiniaPluginContext, StoreGeneric, _ActionsTree, _StoreOnActionListenerContext } from 'pinia';
|
||||
import {
|
||||
PiniaPluginContext,
|
||||
StoreGeneric,
|
||||
_ActionsTree,
|
||||
_StoreOnActionListenerContext,
|
||||
} from "pinia";
|
||||
|
||||
const cloneDeep = <T>(obj: T): T => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
} catch {
|
||||
return { ...obj };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
} catch {
|
||||
return { ...obj };
|
||||
}
|
||||
};
|
||||
const formatTime = (date = new Date()) => {
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
const seconds = date.getSeconds().toString().padStart(2, "0");
|
||||
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
export interface PiniaLoggerOptions {
|
||||
disabled?: boolean;
|
||||
expanded?: boolean;
|
||||
showDuration?: boolean
|
||||
showStoreName?: boolean;
|
||||
logErrors?: boolean;
|
||||
disabled?: boolean;
|
||||
expanded?: boolean;
|
||||
showDuration?: boolean;
|
||||
showStoreName?: boolean;
|
||||
logErrors?: boolean;
|
||||
}
|
||||
|
||||
export type PiniaActionListenerContext = _StoreOnActionListenerContext<StoreGeneric, string, _ActionsTree>;
|
||||
export type PiniaActionListenerContext = _StoreOnActionListenerContext<
|
||||
StoreGeneric,
|
||||
string,
|
||||
_ActionsTree
|
||||
>;
|
||||
|
||||
const defaultOptions: PiniaLoggerOptions = {
|
||||
logErrors: true,
|
||||
disabled: false,
|
||||
expanded: true,
|
||||
showStoreName: true,
|
||||
showDuration: false,
|
||||
logErrors: true,
|
||||
disabled: false,
|
||||
expanded: true,
|
||||
showStoreName: true,
|
||||
showDuration: false,
|
||||
};
|
||||
|
||||
export const PiniaLogger = (config = defaultOptions) => (ctx: PiniaPluginContext) => {
|
||||
const options = {
|
||||
...defaultOptions,
|
||||
...config,
|
||||
};
|
||||
export const PiniaLogger =
|
||||
(config = defaultOptions) =>
|
||||
(ctx: PiniaPluginContext) => {
|
||||
const options = {
|
||||
...defaultOptions,
|
||||
...config,
|
||||
};
|
||||
|
||||
if (options.disabled) return;
|
||||
if (options.disabled) return;
|
||||
|
||||
ctx.store.$onAction((action: PiniaActionListenerContext) => {
|
||||
const startTime = Date.now();
|
||||
const prevState = cloneDeep(ctx.store.$state);
|
||||
|
||||
ctx.store.$onAction((action: PiniaActionListenerContext) => {
|
||||
const startTime = Date.now();
|
||||
const prevState = cloneDeep(ctx.store.$state);
|
||||
const log = (isError?: boolean, error?: any) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime + "ms";
|
||||
const nextState = cloneDeep(ctx.store.$state);
|
||||
const storeName = action.store.$id;
|
||||
const title = `${formatTime()} action 🍍 ${
|
||||
options.showStoreName ? `[${storeName}] ` : ""
|
||||
}${action.name} ${
|
||||
isError ? `failed after ` : ""
|
||||
}in ${duration}`;
|
||||
|
||||
const log = (isError?: boolean, error?: any) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime + 'ms';
|
||||
const nextState = cloneDeep(ctx.store.$state);
|
||||
const storeName = action.store.$id;
|
||||
const title = `${formatTime()} action 🍍 ${options.showStoreName ? `[${storeName}] ` : ''}${action.name} ${isError ? `failed after ` : ''}in ${duration}`;
|
||||
console[options.expanded ? "group" : "groupCollapsed"](
|
||||
`%c${title}`,
|
||||
`font-weight: bold; ${isError ? "color: #ed4981;" : ""}`
|
||||
);
|
||||
console.log(
|
||||
"%cprev state",
|
||||
"font-weight: bold; color: grey;",
|
||||
prevState
|
||||
);
|
||||
console.log("%caction", "font-weight: bold; color: #69B7FF;", {
|
||||
type: action.name,
|
||||
args:
|
||||
action.args.length > 0 ? { ...action.args } : undefined,
|
||||
...(options.showStoreName && { store: action.store.$id }),
|
||||
...(options.showDuration && { duration }),
|
||||
...(isError && { error }),
|
||||
});
|
||||
console.log(
|
||||
"%cnext state",
|
||||
"font-weight: bold; color: #4caf50;",
|
||||
nextState
|
||||
);
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
console[options.expanded ? 'group' : 'groupCollapsed'](`%c${title}`, `font-weight: bold; ${isError ? 'color: #ed4981;' : ''}`);
|
||||
console.log('%cprev state', 'font-weight: bold; color: grey;', prevState);
|
||||
console.log('%caction', 'font-weight: bold; color: #69B7FF;', {
|
||||
type: action.name,
|
||||
args: action.args.length > 0 ? { ...action.args } : undefined,
|
||||
...(options.showStoreName && { store: action.store.$id }),
|
||||
...(options.showDuration && { duration }),
|
||||
...(isError && { error }),
|
||||
});
|
||||
console.log('%cnext state', 'font-weight: bold; color: #4caf50;', nextState);
|
||||
console.groupEnd();
|
||||
action.after(() => {
|
||||
log();
|
||||
});
|
||||
|
||||
if (options.logErrors) {
|
||||
action.onError((error) => {
|
||||
log(true, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
action.after(() => {
|
||||
log();
|
||||
});
|
||||
|
||||
if (options.logErrors) {
|
||||
action.onError((error) => {
|
||||
log(true, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default PiniaLogger;
|
@ -1,30 +1,75 @@
|
||||
import { createRouter, createWebHistory, RouteLocationNormalized } from 'vue-router'
|
||||
import Dashboard from '../pages/Dashboard.vue';
|
||||
import Login from '../pages/Login.vue';
|
||||
import Index from '../pages/Index.vue';
|
||||
import Register from '../pages/Register.vue';
|
||||
import Account from '@/pages/Account.vue';
|
||||
import Settings from '../pages/Settings.vue';
|
||||
import Budgeting from '../pages/Budgeting.vue';
|
||||
import BudgetSidebar from '../pages/BudgetSidebar.vue';
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
RouteLocationNormalized,
|
||||
} from "vue-router";
|
||||
import Account from "@/pages/Account.vue";
|
||||
import Budgeting from "../pages/Budgeting.vue";
|
||||
import BudgetSidebar from "../pages/BudgetSidebar.vue";
|
||||
import Dashboard from "../pages/Dashboard.vue";
|
||||
import Login from "../pages/Login.vue";
|
||||
import Register from "../pages/Register.vue";
|
||||
import Settings from "../pages/Settings.vue";
|
||||
import WelcomePage from "../pages/WelcomePage.vue";
|
||||
|
||||
const routes = [
|
||||
{ path: "/", name: "Index", component: Index },
|
||||
{ path: "/dashboard", name: "Dashboard", component: Dashboard, meta: { requiresAuth: true } },
|
||||
{ path: "/login", name: "Login", component: Login, meta: { hideForAuth: true } },
|
||||
{ path: "/register", name: "Register", component: Register, meta: { hideForAuth: true } },
|
||||
{ path: "/budget/:budgetid/budgeting", name: "Budget", redirect: (to : RouteLocationNormalized) =>
|
||||
'/budget/' + to.params.budgetid + '/budgeting/' + new Date().getFullYear() + '/' + new Date().getMonth(),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{ path: "/budget/:budgetid/budgeting/:year/:month", name: "Budget with date", components: { default: Budgeting, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
|
||||
{ path: "/budget/:budgetid/Settings", name: "Budget Settings", components: { default: Settings, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
|
||||
{ path: "/budget/:budgetid/account/:accountid", name: "Account", components: { default: Account, sidebar: BudgetSidebar }, props: true, meta: { requiresAuth: true } },
|
||||
]
|
||||
{ path: "/", name: "Index", component: WelcomePage },
|
||||
{
|
||||
path: "/dashboard",
|
||||
name: "Dashboard",
|
||||
component: Dashboard,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
meta: { hideForAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "Register",
|
||||
component: Register,
|
||||
meta: { hideForAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/budget/:budgetid/budgeting",
|
||||
name: "Budget",
|
||||
redirect: (to: RouteLocationNormalized) =>
|
||||
"/budget/" +
|
||||
to.params.budgetid +
|
||||
"/budgeting/" +
|
||||
new Date().getFullYear() +
|
||||
"/" +
|
||||
new Date().getMonth(),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/budget/:budgetid/budgeting/:year/:month",
|
||||
name: "Budget with date",
|
||||
components: { default: Budgeting, sidebar: BudgetSidebar },
|
||||
props: true,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/budget/:budgetid/Settings",
|
||||
name: "Budget Settings",
|
||||
components: { default: Settings, sidebar: BudgetSidebar },
|
||||
props: true,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/budget/:budgetid/account/:accountid",
|
||||
name: "Account",
|
||||
components: { default: Account, sidebar: BudgetSidebar },
|
||||
props: true,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
@ -1,41 +1,41 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { defineStore } from "pinia";
|
||||
import { GET, POST } from "../api";
|
||||
import { useBudgetsStore } from "./budget";
|
||||
import { useSessionStore } from "./session";
|
||||
import { useTransactionsStore } from "./transactions";
|
||||
|
||||
interface State {
|
||||
Accounts: Map<string, Account>
|
||||
CurrentAccountID: string | null
|
||||
Categories: Map<string, Category>
|
||||
Months: Map<number, Map<number, Map<string, Category>>>
|
||||
Assignments: []
|
||||
Accounts: Map<string, Account>;
|
||||
CurrentAccountID: string | null;
|
||||
Categories: Map<string, Category>;
|
||||
Months: Map<number, Map<number, Map<string, Category>>>;
|
||||
Assignments: [];
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
ID: string
|
||||
Name: string
|
||||
OnBudget: boolean
|
||||
IsOpen: boolean
|
||||
ClearedBalance: number
|
||||
WorkingBalance: number
|
||||
ReconciledBalance: number
|
||||
Transactions: string[]
|
||||
LastReconciled: NullDate
|
||||
ID: string;
|
||||
Name: string;
|
||||
OnBudget: boolean;
|
||||
IsOpen: boolean;
|
||||
ClearedBalance: number;
|
||||
WorkingBalance: number;
|
||||
ReconciledBalance: number;
|
||||
Transactions: string[];
|
||||
LastReconciled: NullDate;
|
||||
}
|
||||
|
||||
interface NullDate {
|
||||
Valid: boolean
|
||||
Time: Date
|
||||
Valid: boolean;
|
||||
Time: Date;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
ID: string
|
||||
Group: string
|
||||
Name: string
|
||||
AvailableLastMonth: number
|
||||
Assigned: number
|
||||
Activity: number
|
||||
ID: string;
|
||||
Group: string;
|
||||
Name: string;
|
||||
AvailableLastMonth: number;
|
||||
Assigned: number;
|
||||
Activity: number;
|
||||
}
|
||||
|
||||
export const useAccountStore = defineStore("budget/account", {
|
||||
@ -53,12 +53,16 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
AllCategoriesForMonth: (state) => (year: number, month: number) => {
|
||||
const yearMap = state.Months.get(year);
|
||||
const monthMap = yearMap?.get(month);
|
||||
return [...monthMap?.values() || []];
|
||||
return [...(monthMap?.values() || [])];
|
||||
},
|
||||
GetCategoryAvailable(state) {
|
||||
return (category: Category): number => {
|
||||
return category.AvailableLastMonth + Number(category.Assigned) + category.Activity;
|
||||
}
|
||||
return (
|
||||
category.AvailableLastMonth +
|
||||
Number(category.Assigned) +
|
||||
category.Activity
|
||||
);
|
||||
};
|
||||
},
|
||||
GetIncomeCategoryID(state) {
|
||||
const budget = useBudgetsStore();
|
||||
@ -67,15 +71,15 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
GetIncomeAvailable(state) {
|
||||
return (year: number, month: number) => {
|
||||
const IncomeCategoryID = this.GetIncomeCategoryID;
|
||||
if (IncomeCategoryID == null)
|
||||
return 0;
|
||||
if (IncomeCategoryID == null) return 0;
|
||||
|
||||
const categories = this.AllCategoriesForMonth(year, month);
|
||||
const category = categories.filter(x => x.ID == IncomeCategoryID)[0];
|
||||
if (category == null)
|
||||
return 0;
|
||||
const category = categories.filter(
|
||||
(x) => x.ID == IncomeCategoryID
|
||||
)[0];
|
||||
if (category == null) return 0;
|
||||
return category.AvailableLastMonth;
|
||||
}
|
||||
};
|
||||
},
|
||||
CategoryGroupsForMonth(state) {
|
||||
return (year: number, month: number) => {
|
||||
@ -83,8 +87,7 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
const categoryGroups = [];
|
||||
let prev = undefined;
|
||||
for (const category of categories) {
|
||||
if (category.ID == this.GetIncomeCategoryID)
|
||||
continue;
|
||||
if (category.ID == this.GetIncomeCategoryID) continue;
|
||||
|
||||
if (prev == undefined || category.Group != prev.Name) {
|
||||
prev = {
|
||||
@ -93,81 +96,110 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
AvailableLastMonth: category.AvailableLastMonth,
|
||||
Activity: category.Activity,
|
||||
Assigned: category.Assigned,
|
||||
}
|
||||
};
|
||||
categoryGroups.push({
|
||||
...prev,
|
||||
Expand: prev.Name != "Hidden Categories",
|
||||
});
|
||||
} else {
|
||||
categoryGroups[categoryGroups.length-1].Available += this.GetCategoryAvailable(category);
|
||||
categoryGroups[categoryGroups.length-1].AvailableLastMonth += category.AvailableLastMonth;
|
||||
categoryGroups[categoryGroups.length-1].Activity += category.Activity;
|
||||
categoryGroups[categoryGroups.length-1].Assigned += category.Assigned;
|
||||
categoryGroups[categoryGroups.length - 1].Available +=
|
||||
this.GetCategoryAvailable(category);
|
||||
categoryGroups[
|
||||
categoryGroups.length - 1
|
||||
].AvailableLastMonth += category.AvailableLastMonth;
|
||||
categoryGroups[categoryGroups.length - 1].Activity +=
|
||||
category.Activity;
|
||||
categoryGroups[categoryGroups.length - 1].Assigned +=
|
||||
category.Assigned;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return categoryGroups;
|
||||
}
|
||||
};
|
||||
},
|
||||
CategoriesForMonthAndGroup(state) {
|
||||
return (year: number, month: number, group: string) => {
|
||||
const categories = this.AllCategoriesForMonth(year, month);
|
||||
return categories.filter(x => x.Group == group);
|
||||
}
|
||||
return categories.filter((x) => x.Group == group);
|
||||
};
|
||||
},
|
||||
GetAccount(state) {
|
||||
return (accountid: string) => {
|
||||
return this.Accounts.get(accountid);
|
||||
}
|
||||
};
|
||||
},
|
||||
CurrentAccount(state): Account | undefined {
|
||||
if (state.CurrentAccountID == null)
|
||||
return undefined;
|
||||
if (state.CurrentAccountID == null) return undefined;
|
||||
|
||||
return this.GetAccount(state.CurrentAccountID);
|
||||
},
|
||||
OnBudgetAccounts(state) {
|
||||
return [...state.Accounts.values()].filter(x => x.OnBudget);
|
||||
return [...state.Accounts.values()].filter((x) => x.OnBudget);
|
||||
},
|
||||
OnBudgetAccountsBalance(state): number {
|
||||
return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0);
|
||||
return this.OnBudgetAccounts.reduce(
|
||||
(prev, curr) => prev + Number(curr.ClearedBalance),
|
||||
0
|
||||
);
|
||||
},
|
||||
OffBudgetAccounts(state) {
|
||||
return [...state.Accounts.values()].filter(x => !x.OnBudget);
|
||||
return [...state.Accounts.values()].filter((x) => !x.OnBudget);
|
||||
},
|
||||
OffBudgetAccountsBalance(state): number {
|
||||
return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0);
|
||||
return this.OffBudgetAccounts.reduce(
|
||||
(prev, curr) => prev + Number(curr.ClearedBalance),
|
||||
0
|
||||
);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async SetCurrentAccount(budgetid: string, accountid: string) {
|
||||
if (budgetid == null)
|
||||
return;
|
||||
if (budgetid == null) return;
|
||||
|
||||
this.CurrentAccountID = accountid;
|
||||
|
||||
if (accountid == null)
|
||||
return;
|
||||
if (accountid == null) return;
|
||||
const account = this.GetAccount(accountid)!;
|
||||
useSessionStore().setTitle(account.Name);
|
||||
await this.FetchAccount(account);
|
||||
},
|
||||
async FetchAccount(account: Account) {
|
||||
const result = await GET("/account/" + account.ID + "/transactions");
|
||||
const result = await GET(
|
||||
"/account/" + account.ID + "/transactions"
|
||||
);
|
||||
const response = await result.json();
|
||||
const transactionsStore = useTransactionsStore()
|
||||
const transactions = transactionsStore.AddTransactions(response.Transactions);
|
||||
const transactionsStore = useTransactionsStore();
|
||||
const transactions = transactionsStore.AddTransactions(
|
||||
response.Transactions
|
||||
);
|
||||
account.Transactions = transactions;
|
||||
},
|
||||
async FetchMonthBudget(budgetid: string, year: number, month: number) {
|
||||
const result = await GET("/budget/" + budgetid + "/" + year + "/" + (month + 1));
|
||||
const result = await GET(
|
||||
"/budget/" + budgetid + "/" + year + "/" + (month + 1)
|
||||
);
|
||||
const response = await result.json();
|
||||
if (response.Categories == undefined || response.Categories.length <= 0)
|
||||
if (
|
||||
response.Categories == undefined ||
|
||||
response.Categories.length <= 0
|
||||
)
|
||||
return;
|
||||
this.addCategoriesForMonth(year, month, response.Categories);
|
||||
},
|
||||
async EditAccount(accountid: string, name: string, onBudget: boolean, isOpen: boolean) {
|
||||
const result = await POST("/account/" + accountid, JSON.stringify({ name: name, onBudget: onBudget, isOpen: isOpen }));
|
||||
async EditAccount(
|
||||
accountid: string,
|
||||
name: string,
|
||||
onBudget: boolean,
|
||||
isOpen: boolean
|
||||
) {
|
||||
const result = await POST(
|
||||
"/account/" + accountid,
|
||||
JSON.stringify({
|
||||
name: name,
|
||||
onBudget: onBudget,
|
||||
isOpen: isOpen,
|
||||
})
|
||||
);
|
||||
const response = await result.json();
|
||||
useBudgetsStore().MergeBudgetingData(response);
|
||||
|
||||
@ -175,10 +207,17 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
this.Accounts.delete(accountid);
|
||||
}
|
||||
},
|
||||
addCategoriesForMonth(year: number, month: number, categories: Category[]): void {
|
||||
addCategoriesForMonth(
|
||||
year: number,
|
||||
month: number,
|
||||
categories: Category[]
|
||||
): void {
|
||||
this.$patch((state) => {
|
||||
const yearMap = state.Months.get(year) || new Map<number, Map<string, Category>>();
|
||||
const monthMap = yearMap.get(month) || new Map<string, Category>();
|
||||
const yearMap =
|
||||
state.Months.get(year) ||
|
||||
new Map<number, Map<string, Category>>();
|
||||
const monthMap =
|
||||
yearMap.get(month) || new Map<string, Category>();
|
||||
for (const category of categories) {
|
||||
monthMap.set(category.ID, category);
|
||||
}
|
||||
@ -188,8 +227,7 @@ export const useAccountStore = defineStore("budget/account", {
|
||||
});
|
||||
},
|
||||
logout() {
|
||||
this.$reset()
|
||||
this.$reset();
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
});
|
||||
|
@ -4,17 +4,16 @@ import { useAccountStore } from "./budget-account";
|
||||
import { Budget, useSessionStore } from "./session";
|
||||
|
||||
interface State {
|
||||
CurrentBudgetID: string | null,
|
||||
CurrentBudgetID: string | null;
|
||||
}
|
||||
|
||||
export const useBudgetsStore = defineStore('budget', {
|
||||
export const useBudgetsStore = defineStore("budget", {
|
||||
state: (): State => ({
|
||||
CurrentBudgetID: null,
|
||||
}),
|
||||
getters: {
|
||||
CurrentBudget(): Budget | undefined {
|
||||
if (this.CurrentBudgetID == null)
|
||||
return undefined;
|
||||
if (this.CurrentBudgetID == null) return undefined;
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
return sessionStore.Budgets.get(this.CurrentBudgetID);
|
||||
@ -27,7 +26,7 @@ export const useBudgetsStore = defineStore('budget', {
|
||||
ImportYNAB(formData: FormData) {
|
||||
return POST(
|
||||
"/budget/" + this.CurrentBudgetID + "/import/ynab",
|
||||
formData,
|
||||
formData
|
||||
);
|
||||
},
|
||||
async NewBudget(budgetName: string): Promise<void> {
|
||||
@ -43,8 +42,7 @@ export const useBudgetsStore = defineStore('budget', {
|
||||
async SetCurrentBudget(budgetid: string): Promise<void> {
|
||||
this.CurrentBudgetID = budgetid;
|
||||
|
||||
if (budgetid == null)
|
||||
return
|
||||
if (budgetid == null) return;
|
||||
|
||||
await this.FetchBudget(budgetid);
|
||||
},
|
||||
@ -58,13 +56,15 @@ export const useBudgetsStore = defineStore('budget', {
|
||||
for (const account of response.Accounts || []) {
|
||||
const existingAccount = accounts.Accounts.get(account.ID);
|
||||
account.Transactions = existingAccount?.Transactions ?? [];
|
||||
if(account.LastReconciled.Valid)
|
||||
account.LastReconciled.Time = new Date(account.LastReconciled.Time);
|
||||
if (account.LastReconciled.Valid)
|
||||
account.LastReconciled.Time = new Date(
|
||||
account.LastReconciled.Time
|
||||
);
|
||||
accounts.Accounts.set(account.ID, account);
|
||||
}
|
||||
for (const category of response.Categories || []) {
|
||||
accounts.Categories.set(category.ID, category);
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
@ -1,43 +1,52 @@
|
||||
import { StorageSerializers, useStorage } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia'
|
||||
import { POST } from '../api';
|
||||
import { StorageSerializers, useStorage } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { POST } from "../api";
|
||||
|
||||
interface State {
|
||||
Session: Session | null
|
||||
Budgets: Map<string, Budget>,
|
||||
Session: Session | null;
|
||||
Budgets: Map<string, Budget>;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
Token: string
|
||||
User: string
|
||||
Token: string;
|
||||
User: string;
|
||||
}
|
||||
|
||||
export interface Budget {
|
||||
ID: string
|
||||
Name: string
|
||||
AvailableBalance: number
|
||||
IncomeCategoryID: string
|
||||
ID: string;
|
||||
Name: string;
|
||||
AvailableBalance: number;
|
||||
IncomeCategoryID: string;
|
||||
}
|
||||
|
||||
export const useSessionStore = defineStore('session', {
|
||||
export const useSessionStore = defineStore("session", {
|
||||
state: () => ({
|
||||
Session: useStorage<Session | null>('session', null, undefined, { serializer: StorageSerializers.object }),
|
||||
Budgets: useStorage<Map<string, Budget>>('budgets', new Map<string, Budget>(), undefined, { serializer: StorageSerializers.map }),
|
||||
Session: useStorage<Session | null>("session", null, undefined, {
|
||||
serializer: StorageSerializers.object,
|
||||
}),
|
||||
Budgets: useStorage<Map<string, Budget>>(
|
||||
"budgets",
|
||||
new Map<string, Budget>(),
|
||||
undefined,
|
||||
{ serializer: StorageSerializers.map }
|
||||
),
|
||||
}),
|
||||
getters: {
|
||||
BudgetsList: (state) => [ ...state.Budgets.values() ],
|
||||
AuthHeaders: (state) => ({'Authorization': 'Bearer ' + state.Session?.Token}),
|
||||
BudgetsList: (state) => [...state.Budgets.values()],
|
||||
AuthHeaders: (state) => ({
|
||||
Authorization: "Bearer " + state.Session?.Token,
|
||||
}),
|
||||
LoggedIn: (state) => state.Session != null,
|
||||
},
|
||||
actions: {
|
||||
setTitle(title : string) {
|
||||
setTitle(title: string) {
|
||||
document.title = "Budgeteer - " + title;
|
||||
},
|
||||
loginSuccess(x : any) {
|
||||
loginSuccess(x: any) {
|
||||
this.Session = {
|
||||
User: x.User,
|
||||
Token: x.Token,
|
||||
}
|
||||
};
|
||||
for (const budget of x.Budgets) {
|
||||
this.Budgets.set(budget.ID, budget);
|
||||
}
|
||||
@ -48,8 +57,11 @@ export const useSessionStore = defineStore('session', {
|
||||
this.loginSuccess(result);
|
||||
return result;
|
||||
},
|
||||
async register(login : any) {
|
||||
const response = await POST("/user/register", JSON.stringify(login));
|
||||
async register(login: any) {
|
||||
const response = await POST(
|
||||
"/user/register",
|
||||
JSON.stringify(login)
|
||||
);
|
||||
const result = await response.json();
|
||||
this.loginSuccess(result);
|
||||
return result;
|
||||
@ -58,5 +70,5 @@ export const useSessionStore = defineStore('session', {
|
||||
this.Session = null;
|
||||
this.Budgets.clear();
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
@ -2,17 +2,17 @@ import { useStorage } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
interface State {
|
||||
Menu: MenuSettings
|
||||
Menu: MenuSettings;
|
||||
}
|
||||
|
||||
interface MenuSettings {
|
||||
Show: boolean | null,
|
||||
Expand: boolean | null,
|
||||
Show: boolean | null;
|
||||
Expand: boolean | null;
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', {
|
||||
export const useSettingsStore = defineStore("settings", {
|
||||
state: () => ({
|
||||
Menu: useStorage<MenuSettings>('settings', {
|
||||
Menu: useStorage<MenuSettings>("settings", {
|
||||
Show: null,
|
||||
Expand: false,
|
||||
}),
|
||||
@ -24,5 +24,5 @@ export const useSettingsStore = defineStore('settings', {
|
||||
toggleMenuSize() {
|
||||
this.Menu.Expand = !this.Menu.Expand;
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
@ -1,26 +1,26 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { defineStore } from "pinia";
|
||||
import { POST } from "../api";
|
||||
import { useAccountStore } from "./budget-account";
|
||||
|
||||
interface State {
|
||||
Transactions: Map<string, Transaction>
|
||||
Reconciling: boolean
|
||||
Transactions: Map<string, Transaction>;
|
||||
Reconciling: boolean;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
ID: string
|
||||
Date: Date
|
||||
TransferAccount: string
|
||||
CategoryGroup: string
|
||||
Category: string
|
||||
CategoryID: string | undefined
|
||||
Memo: string
|
||||
Status: string
|
||||
GroupID: string
|
||||
Payee: string
|
||||
PayeeID: string | undefined
|
||||
Amount: number
|
||||
Reconciled: boolean
|
||||
ID: string;
|
||||
Date: Date;
|
||||
TransferAccount: string;
|
||||
CategoryGroup: string;
|
||||
Category: string;
|
||||
CategoryID: string | undefined;
|
||||
Memo: string;
|
||||
Status: string;
|
||||
GroupID: string;
|
||||
Payee: string;
|
||||
PayeeID: string | undefined;
|
||||
Amount: number;
|
||||
Reconciled: boolean;
|
||||
}
|
||||
|
||||
export const useTransactionsStore = defineStore("budget/transactions", {
|
||||
@ -30,8 +30,9 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
||||
}),
|
||||
getters: {
|
||||
ReconcilingBalance(state): number {
|
||||
const accountsStore = useAccountStore()
|
||||
let reconciledBalance = accountsStore.CurrentAccount!.ReconciledBalance;
|
||||
const accountsStore = useAccountStore();
|
||||
let reconciledBalance =
|
||||
accountsStore.CurrentAccount!.ReconciledBalance;
|
||||
for (const transaction of this.TransactionsList) {
|
||||
if (transaction.Reconciled)
|
||||
reconciledBalance += transaction.Amount;
|
||||
@ -39,9 +40,9 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
||||
return reconciledBalance;
|
||||
},
|
||||
TransactionsList(state): Transaction[] {
|
||||
const accountsStore = useAccountStore()
|
||||
return accountsStore.CurrentAccount!.Transactions.map(x => {
|
||||
return this.Transactions.get(x)!
|
||||
const accountsStore = useAccountStore();
|
||||
return accountsStore.CurrentAccount!.Transactions.map((x) => {
|
||||
return this.Transactions.get(x)!;
|
||||
});
|
||||
},
|
||||
},
|
||||
@ -59,25 +60,30 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
||||
},
|
||||
SetReconciledForAllTransactions(value: boolean) {
|
||||
for (const transaction of this.TransactionsList) {
|
||||
if (transaction.Status == "Reconciled")
|
||||
continue;
|
||||
if (transaction.Status == "Reconciled") continue;
|
||||
|
||||
transaction.Reconciled = value;
|
||||
}
|
||||
},
|
||||
async SubmitReconcilation(reconciliationTransactionAmount: number) {
|
||||
const accountsStore = useAccountStore()
|
||||
const accountsStore = useAccountStore();
|
||||
const account = accountsStore.CurrentAccount!;
|
||||
const reconciledTransactions = this.TransactionsList.filter(x => x.Reconciled);
|
||||
const reconciledTransactions = this.TransactionsList.filter(
|
||||
(x) => x.Reconciled
|
||||
);
|
||||
for (const transaction of reconciledTransactions) {
|
||||
account.ReconciledBalance += transaction.Amount;
|
||||
transaction.Status = "Reconciled";
|
||||
transaction.Reconciled = false;
|
||||
}
|
||||
const result = await POST("/account/" + accountsStore.CurrentAccountID + "/reconcile", JSON.stringify({
|
||||
transactionIDs: reconciledTransactions.map(x => x.ID),
|
||||
reconciliationTransactionAmount: reconciliationTransactionAmount.toString(),
|
||||
}));
|
||||
const result = await POST(
|
||||
"/account/" + accountsStore.CurrentAccountID + "/reconcile",
|
||||
JSON.stringify({
|
||||
transactionIDs: reconciledTransactions.map((x) => x.ID),
|
||||
reconciliationTransactionAmount:
|
||||
reconciliationTransactionAmount.toString(),
|
||||
})
|
||||
);
|
||||
const response = await result.json();
|
||||
const recTrans = response.ReconciliationTransaction;
|
||||
if (recTrans) {
|
||||
@ -86,20 +92,19 @@ export const useTransactionsStore = defineStore("budget/transactions", {
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.$reset()
|
||||
this.$reset();
|
||||
},
|
||||
async saveTransaction(payload: string) {
|
||||
const accountsStore = useAccountStore()
|
||||
const accountsStore = useAccountStore();
|
||||
const result = await POST("/transaction/new", payload);
|
||||
const response = await result.json() as Transaction;
|
||||
const response = (await result.json()) as Transaction;
|
||||
this.AddTransactions([response]);
|
||||
accountsStore.CurrentAccount?.Transactions.unshift(response.ID);
|
||||
},
|
||||
async editTransaction(transactionid: string, payload: string) {
|
||||
const result = await POST("/transaction/" + transactionid, payload);
|
||||
const response = await result.json() as Transaction;
|
||||
const response = (await result.json()) as Transaction;
|
||||
this.AddTransactions([response]);
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,10 +1,7 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
// this enables stricter inference for data properties on `this`
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
// this enables stricter inference for data properties on `this`
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
@ -1,30 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
import path from 'path'
|
||||
import path from "path";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
plugins: [
|
||||
vue(),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
|
||||
],
|
||||
define: { "process.env": {} },
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://10.0.0.162:1323/',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
/* remove the need to specify .vue files https://vitejs.dev/config/#resolve-extensions
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://10.0.0.162:1323/",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
/* remove the need to specify .vue files https://vitejs.dev/config/#resolve-extensions
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.js',
|
||||
@ -37,4 +37,4 @@ export default defineConfig({
|
||||
]
|
||||
},
|
||||
*/
|
||||
})
|
||||
});
|
||||
|
351
web/yarn.lock
351
web/yarn.lock
@ -957,6 +957,21 @@
|
||||
resolved "https://registry.yarnpkg.com/@emmetio/scanner/-/scanner-1.0.0.tgz#065b2af6233fe7474d44823e3deb89724af42b5f"
|
||||
integrity sha512-8HqW8EVqjnCmWXVpqAOZf+EGESdkR27odcMMMGefgKXtar00SoYNSryGv//TELI4T3QFsECo78p+0lmalk/CFA==
|
||||
|
||||
"@eslint/eslintrc@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a"
|
||||
integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
espree "^9.3.1"
|
||||
globals "^13.9.0"
|
||||
ignore "^4.0.6"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.0"
|
||||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
@ -1001,6 +1016,20 @@
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@humanwhocodes/config-array@^0.9.2":
|
||||
version "0.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
|
||||
integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^1.2.1"
|
||||
debug "^4.1.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
"@humanwhocodes/object-schema@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72"
|
||||
@ -1281,6 +1310,50 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/parser@^5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.13.0.tgz#0394ed8f2f849273c0bf4b811994d177112ced5c"
|
||||
integrity sha512-GdrU4GvBE29tm2RqWOM0P5QfCtgCyN4hXICj/X9ibKED16136l9ZpoJvCL5pSKtmJzA+NRDzQ312wWMejCVVfg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.13.0"
|
||||
"@typescript-eslint/types" "5.13.0"
|
||||
"@typescript-eslint/typescript-estree" "5.13.0"
|
||||
debug "^4.3.2"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.13.0.tgz#cf6aff61ca497cb19f0397eea8444a58f46156b6"
|
||||
integrity sha512-T4N8UvKYDSfVYdmJq7g2IPJYCRzwtp74KyDZytkR4OL3NRupvswvmJQJ4CX5tDSurW2cvCc1Ia1qM7d0jpa7IA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.13.0"
|
||||
"@typescript-eslint/visitor-keys" "5.13.0"
|
||||
|
||||
"@typescript-eslint/types@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.13.0.tgz#da1de4ae905b1b9ff682cab0bed6b2e3be9c04e5"
|
||||
integrity sha512-LmE/KO6DUy0nFY/OoQU0XelnmDt+V8lPQhh8MOVa7Y5k2gGRd6U9Kp3wAjhB4OHg57tUO0nOnwYQhRRyEAyOyg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.13.0.tgz#b37c07b748ff030a3e93d87c842714e020b78141"
|
||||
integrity sha512-Q9cQow0DeLjnp5DuEDjLZ6JIkwGx3oYZe+BfcNuw/POhtpcxMTy18Icl6BJqTSd+3ftsrfuVb7mNHRZf7xiaNA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.13.0"
|
||||
"@typescript-eslint/visitor-keys" "5.13.0"
|
||||
debug "^4.3.2"
|
||||
globby "^11.0.4"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.13.0.tgz#f45ff55bcce16403b221ac9240fbeeae4764f0fd"
|
||||
integrity sha512-HLKEAS/qA1V7d9EzcpLFykTePmOQqOFim8oCvhY3pZgQ8Hi38hYpHd9e5GN6nQBFQNecNhws5wkS9Y5XIO0s/g==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.13.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@vitejs/plugin-vue@^2.0.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.1.0.tgz#ddf5e0059f84f2ff649afc25ce5a59211e670542"
|
||||
@ -2089,6 +2162,11 @@ acorn-import-assertions@^1.7.6:
|
||||
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
|
||||
integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
|
||||
|
||||
acorn-jsx@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||
|
||||
acorn-node@^1.6.1:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
|
||||
@ -2118,7 +2196,7 @@ acorn@^7.0.0, acorn@^7.1.1:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.0.4, acorn@^8.0.5, acorn@^8.4.1:
|
||||
acorn@^8.0.4, acorn@^8.0.5, acorn@^8.4.1, acorn@^8.7.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
|
||||
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
|
||||
@ -2160,7 +2238,7 @@ ajv-keywords@^5.0.0:
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
|
||||
ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
|
||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
@ -2277,6 +2355,11 @@ argparse@^1.0.7:
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
arr-diff@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
||||
@ -3351,7 +3434,7 @@ cross-spawn@^6.0.0:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.3:
|
||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
@ -3528,7 +3611,7 @@ debug@^3.1.1:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1:
|
||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
||||
@ -3552,6 +3635,11 @@ deep-equal@^1.0.1:
|
||||
object-keys "^1.1.1"
|
||||
regexp.prototype.flags "^1.2.0"
|
||||
|
||||
deep-is@^0.1.3:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
deepmerge@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
|
||||
@ -3724,6 +3812,13 @@ dns-txt@^2.0.2:
|
||||
dependencies:
|
||||
buffer-indexof "^1.0.0"
|
||||
|
||||
doctrine@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
|
||||
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
doctypes@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
|
||||
@ -4046,6 +4141,21 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
|
||||
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
|
||||
|
||||
eslint-plugin-vue@^8.5.0:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-8.5.0.tgz#65832bba43ca713fa5da16bdfcf55d0095677f6f"
|
||||
integrity sha512-i1uHCTAKOoEj12RDvdtONWrGzjFm/djkzqfhmQ0d6M/W8KM81mhswd/z+iTZ0jCpdUedW3YRgcVfQ37/J4zoYQ==
|
||||
dependencies:
|
||||
eslint-utils "^3.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
semver "^7.3.5"
|
||||
vue-eslint-parser "^8.0.1"
|
||||
|
||||
eslint-scope@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
|
||||
@ -4062,11 +4172,93 @@ eslint-scope@^4.0.3:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^7.0.0, eslint-scope@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
|
||||
integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
|
||||
dependencies:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-utils@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
|
||||
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
eslint-visitor-keys@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
|
||||
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
|
||||
|
||||
eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
eslint@^8.10.0:
|
||||
version "8.10.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d"
|
||||
integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.2.0"
|
||||
"@humanwhocodes/config-array" "^0.9.2"
|
||||
ajv "^6.10.0"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
debug "^4.3.2"
|
||||
doctrine "^3.0.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
eslint-scope "^7.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
espree "^9.3.1"
|
||||
esquery "^1.4.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob-parent "^6.0.1"
|
||||
globals "^13.6.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
js-yaml "^4.1.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
lodash.merge "^4.6.2"
|
||||
minimatch "^3.0.4"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.9.1"
|
||||
regexpp "^3.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
strip-json-comments "^3.1.0"
|
||||
text-table "^0.2.0"
|
||||
v8-compile-cache "^2.0.3"
|
||||
|
||||
espree@^9.0.0, espree@^9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd"
|
||||
integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==
|
||||
dependencies:
|
||||
acorn "^8.7.0"
|
||||
acorn-jsx "^5.3.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
|
||||
integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
|
||||
dependencies:
|
||||
estraverse "^5.1.0"
|
||||
|
||||
esrecurse@^4.1.0, esrecurse@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
|
||||
@ -4079,7 +4271,7 @@ estraverse@^4.1.1:
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
|
||||
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
|
||||
|
||||
estraverse@^5.2.0:
|
||||
estraverse@^5.1.0, estraverse@^5.2.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
|
||||
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
|
||||
@ -4289,6 +4481,11 @@ fast-json-stable-stringify@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
fast-levenshtein@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
|
||||
@ -4315,6 +4512,13 @@ figures@^2.0.0:
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
|
||||
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-saver@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
|
||||
@ -4388,6 +4592,19 @@ find-up@^4.0.0, find-up@^4.1.0:
|
||||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
flat-cache@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
||||
integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
|
||||
dependencies:
|
||||
flatted "^3.1.0"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
flatted@^3.1.0:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
|
||||
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
|
||||
@ -4529,6 +4746,11 @@ function-bind@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
@ -4626,7 +4848,14 @@ globals@^11.1.0:
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
|
||||
globby@^11.0.1, globby@^11.0.2, globby@^11.0.3:
|
||||
globals@^13.6.0, globals@^13.9.0:
|
||||
version "13.12.1"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb"
|
||||
integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
|
||||
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
|
||||
@ -4961,7 +5190,7 @@ iferr@^0.1.5:
|
||||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
|
||||
|
||||
ignore@^4.0.3:
|
||||
ignore@^4.0.3, ignore@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
@ -4976,7 +5205,7 @@ immutable@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
|
||||
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
|
||||
|
||||
import-fresh@^3.1.0, import-fresh@^3.2.1:
|
||||
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||
@ -5373,6 +5602,13 @@ js-yaml@^3.13.1:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
|
||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
@ -5413,6 +5649,11 @@ json-schema@0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
@ -5513,6 +5754,14 @@ launch-editor@^2.2.1, launch-editor@^2.3.0:
|
||||
picocolors "^1.0.0"
|
||||
shell-quote "^1.6.1"
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
lilconfig@^2.0.3, lilconfig@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082"
|
||||
@ -5591,6 +5840,11 @@ lodash.memoize@^4.1.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
@ -5979,6 +6233,11 @@ nanomatch@^1.2.9:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
@ -6235,6 +6494,18 @@ opener@^1.5.2:
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||
|
||||
optionator@^0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
|
||||
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
|
||||
dependencies:
|
||||
deep-is "^0.1.3"
|
||||
fast-levenshtein "^2.0.6"
|
||||
levn "^0.4.1"
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.3"
|
||||
|
||||
ora@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318"
|
||||
@ -6817,7 +7088,12 @@ postcss@^8.1.10, postcss@^8.2.15, postcss@^8.2.6, postcss@^8.3.5, postcss@^8.4.5
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
"prettier@^1.18.2 || ^2.0.0":
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@2.5.1, "prettier@^1.18.2 || ^2.0.0":
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
@ -7196,6 +7472,11 @@ regexp.prototype.flags@^1.2.0:
|
||||
call-bind "^1.0.2"
|
||||
define-properties "^1.1.3"
|
||||
|
||||
regexpp@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
|
||||
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
|
||||
|
||||
regexpu-core@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3"
|
||||
@ -7975,6 +8256,11 @@ strip-indent@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
|
||||
integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
|
||||
|
||||
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
stylehacks@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.2.tgz#fa10e5181c6e8dc0bddb4a3fb372e9ac42bba2ad"
|
||||
@ -8112,6 +8398,11 @@ terser@^5.10.0, terser@^5.7.2:
|
||||
source-map "~0.7.2"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
||||
thenify-all@^1.0.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
|
||||
@ -8288,6 +8579,13 @@ tsutils@^2.29.0:
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
@ -8305,6 +8603,18 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
|
||||
dependencies:
|
||||
prelude-ls "^1.2.1"
|
||||
|
||||
type-fest@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
type-fest@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
|
||||
@ -8472,6 +8782,11 @@ uuid@^8.3.2:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||
@ -8644,6 +8959,19 @@ vue-demi@*:
|
||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.1.tgz#f7e18efbecffd11ab069d1472d7a06e319b4174c"
|
||||
integrity sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==
|
||||
|
||||
vue-eslint-parser@^8.0.1:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz#5d31129a1b3dd89c0069ca0a1c88f970c360bd0d"
|
||||
integrity sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
eslint-scope "^7.0.0"
|
||||
eslint-visitor-keys "^3.1.0"
|
||||
espree "^9.0.0"
|
||||
esquery "^1.4.0"
|
||||
lodash "^4.17.21"
|
||||
semver "^7.3.5"
|
||||
|
||||
vue-hot-reload-api@^2.3.0:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2"
|
||||
@ -8953,6 +9281,11 @@ with@^7.0.0:
|
||||
assert-never "^1.2.1"
|
||||
babel-walk "3.0.0-canary-5"
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||
|
||||
worker-farm@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
|
||||
|
Loading…
x
Reference in New Issue
Block a user