diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 3dc2f88..09e832e 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -1,17 +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" - } -} + 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", + }, +}; diff --git a/web/index.html b/web/index.html index b2535d4..506228f 100644 --- a/web/index.html +++ b/web/index.html @@ -1,13 +1,14 @@ - - - - - Vite App - - -
- - + + + + + Vite App + + +
+ + diff --git a/web/postcss.config.js b/web/postcss.config.js index 33ad091..e873f1a 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/web/src/@types/shims-vue.d.ts b/web/src/@types/shims-vue.d.ts index 7dcfc86..811114b 100644 --- a/web/src/@types/shims-vue.d.ts +++ b/web/src/@types/shims-vue.d.ts @@ -1,5 +1,5 @@ declare module "*.vue" { - import { defineComponent } from "vue"; - const component: ReturnType; - export default component; -} \ No newline at end of file + import { defineComponent } from "vue"; + const component: ReturnType; + export default component; +} diff --git a/web/src/App.vue b/web/src/App.vue index 057c1f2..9b55f2f 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -26,29 +26,39 @@ export default defineComponent({ \ No newline at end of file +
+ +
+ + + diff --git a/web/src/api.ts b/web/src/api.ts index c245db5..e5292c9 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1,26 +1,26 @@ 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, - }) -}; + const sessionStore = useSessionStore(); + return fetch(BASE_URL + path, { + headers: sessionStore.AuthHeaders, + }); +} export function POST(path: string, body: FormData | string | null) { - const sessionStore = useSessionStore(); - return fetch(BASE_URL + path, { - method: "POST", - headers: sessionStore.AuthHeaders, - body: body, - }) + const sessionStore = useSessionStore(); + return fetch(BASE_URL + path, { + 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, - }) -} \ No newline at end of file + const sessionStore = useSessionStore(); + return fetch(BASE_URL + path, { + method: "DELETE", + headers: sessionStore.AuthHeaders, + }); +} diff --git a/web/src/components/AccountWithReconciled.vue b/web/src/components/AccountWithReconciled.vue index 044c754..21036d7 100644 --- a/web/src/components/AccountWithReconciled.vue +++ b/web/src/components/AccountWithReconciled.vue @@ -19,13 +19,15 @@ function daysSinceLastReconciled() { \ No newline at end of file + + + {{account.Name}} + + + {{daysSinceLastReconciled()}} + + + diff --git a/web/src/components/Autocomplete.vue b/web/src/components/Autocomplete.vue index 470c4be..b3d275c 100644 --- a/web/src/components/Autocomplete.vue +++ b/web/src/components/Autocomplete.vue @@ -81,22 +81,29 @@ function clear() { \ No newline at end of file +
+ + {{ text }} +
+ {{ suggestion.Name }} +
+
+ diff --git a/web/src/components/Button.vue b/web/src/components/Button.vue index 9491068..e3ef6a8 100644 --- a/web/src/components/Button.vue +++ b/web/src/components/Button.vue @@ -1,10 +1,7 @@ - + \ No newline at end of file + + diff --git a/web/src/components/Card.vue b/web/src/components/Card.vue index 6464848..bf9866a 100644 --- a/web/src/components/Card.vue +++ b/web/src/components/Card.vue @@ -1,8 +1,8 @@ - + \ No newline at end of file +
+ +
+ diff --git a/web/src/components/Checkbox.vue b/web/src/components/Checkbox.vue index 544ec52..b2aab40 100644 --- a/web/src/components/Checkbox.vue +++ b/web/src/components/Checkbox.vue @@ -3,9 +3,9 @@ const props = defineProps(["modelValue"]); \ No newline at end of file + + diff --git a/web/src/components/Currency.vue b/web/src/components/Currency.vue index 809df4f..31c13cc 100644 --- a/web/src/components/Currency.vue +++ b/web/src/components/Currency.vue @@ -1,7 +1,7 @@ \ No newline at end of file + {{ formattedValue }} € + diff --git a/web/src/components/DateInput.vue b/web/src/components/DateInput.vue index 5177b1e..473ad41 100644 --- a/web/src/components/DateInput.vue +++ b/web/src/components/DateInput.vue @@ -26,11 +26,10 @@ function selectAll(event: FocusEvent) { \ No newline at end of file + + diff --git a/web/src/components/Input.vue b/web/src/components/Input.vue index 831cbe1..9b0e32d 100644 --- a/web/src/components/Input.vue +++ b/web/src/components/Input.vue @@ -3,8 +3,8 @@ const props = defineProps(["modelValue"]); \ No newline at end of file + + diff --git a/web/src/components/Modal.vue b/web/src/components/Modal.vue index d628210..721098d 100644 --- a/web/src/components/Modal.vue +++ b/web/src/components/Modal.vue @@ -30,33 +30,38 @@ function submitDialog() { \ No newline at end of file + +
+
+
+

+ {{ buttonText }} +

+ +
+ + +
+
+
+
+ diff --git a/web/src/components/TransactionEditRow.vue b/web/src/components/TransactionEditRow.vue index c7e3f4e..0e0364d 100644 --- a/web/src/components/TransactionEditRow.vue +++ b/web/src/components/TransactionEditRow.vue @@ -38,29 +38,38 @@ function saveTransaction(e: MouseEvent) { \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/TransactionInputRow.vue b/web/src/components/TransactionInputRow.vue index d892eda..5c79180 100644 --- a/web/src/components/TransactionInputRow.vue +++ b/web/src/components/TransactionInputRow.vue @@ -52,32 +52,41 @@ function saveTransaction(e: MouseEvent) { \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/TransactionRow.vue b/web/src/components/TransactionRow.vue index 0d87765..73cd718 100644 --- a/web/src/components/TransactionRow.vue +++ b/web/src/components/TransactionRow.vue @@ -47,34 +47,45 @@ function getStatusSymbol() { \ No newline at end of file + diff --git a/web/src/date.ts b/web/src/date.ts index de7fccf..64a8217 100644 --- a/web/src/date.ts +++ b/web/src/date.ts @@ -1,7 +1,8 @@ export function formatDate(date: Date): string { - return date.toLocaleDateString(undefined, { // you can use undefined as first argument - year: "numeric", - month: "2-digit", - day: "2-digit", - }); -} \ No newline at end of file + return date.toLocaleDateString(undefined, { + // you can use undefined as first argument + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} diff --git a/web/src/dialogs/EditAccount.vue b/web/src/dialogs/EditAccount.vue index d8e3515..707392f 100644 --- a/web/src/dialogs/EditAccount.vue +++ b/web/src/dialogs/EditAccount.vue @@ -20,7 +20,7 @@ function editAccount(e : {cancel:boolean}) : boolean { if(CurrentAccount.value?.ClearedBalance != 0 && !accountOpen.value){ e.cancel = true; error.value = "Cannot close account with balance"; - return false; + return false; } error.value = ""; @@ -42,35 +42,29 @@ function openEditAccount(e : any) { \ No newline at end of file + + +
+ +
+
+ + +
+
+ + +
+
+ {{ error }} +
+
+ diff --git a/web/src/dialogs/NewBudget.vue b/web/src/dialogs/NewBudget.vue index 86d72d7..d1b8de2 100644 --- a/web/src/dialogs/NewBudget.vue +++ b/web/src/dialogs/NewBudget.vue @@ -11,9 +11,14 @@ function saveBudget() { \ No newline at end of file + +
+ +
+
+ diff --git a/web/src/index.css b/web/src/index.css index 345b41c..8fad555 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,15 +3,15 @@ @tailwind utilities; h1 { - font-size: 200%; + font-size: 200%; } a { - text-decoration: underline; + text-decoration: underline; } #app { - font-family: Avenir, Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} \ No newline at end of file + font-family: Avenir, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/web/src/main.ts b/web/src/main.ts index 7fdd123..d0c64c2 100644 --- a/web/src/main.ts +++ b/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((to.params.budgetid)); + const budgetStore = useBudgetsStore(); + await budgetStore.SetCurrentBudget(to.params.budgetid); - const accountStore = useAccountStore(); - await accountStore.SetCurrentAccount((to.params.budgetid), (to.params.accountid)); - next(); -}) + const accountStore = useAccountStore(); + await accountStore.SetCurrentAccount( + to.params.budgetid, + 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 (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(); - } - - } else if (to.matched.some(record => record.meta.hideForAuth)) { - if (loggedIn) { - next({ path: '/dashboard' }); - } else { - next(); - } - } else { - next(); - } + 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(); + } }); 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 \ No newline at end of file +1646426130; +1646512855755; diff --git a/web/src/pages/Account.vue b/web/src/pages/Account.vue index 37949bb..5cfdd70 100644 --- a/web/src/pages/Account.vue +++ b/web/src/pages/Account.vue @@ -42,94 +42,101 @@ function createReconcilationTransaction() { \ No newline at end of file + diff --git a/web/src/pages/Admin.vue b/web/src/pages/Admin.vue index 183df55..da0687d 100644 --- a/web/src/pages/Admin.vue +++ b/web/src/pages/Admin.vue @@ -8,9 +8,9 @@ onMounted(() => { \ No newline at end of file +

Danger Zone

+
+ +

This removes all data and starts from scratch. Not undoable!

+
+ diff --git a/web/src/pages/BudgetSidebar.vue b/web/src/pages/BudgetSidebar.vue index 0407e76..8cb72c5 100644 --- a/web/src/pages/BudgetSidebar.vue +++ b/web/src/pages/BudgetSidebar.vue @@ -22,45 +22,59 @@ const OffBudgetAccountsBalance = computed(() => accountStore.OffBudgetAccountsBa diff --git a/web/src/pages/Budgeting.vue b/web/src/pages/Budgeting.vue index 13a0c7b..86af6cd 100644 --- a/web/src/pages/Budgeting.vue +++ b/web/src/pages/Budgeting.vue @@ -69,47 +69,85 @@ function getGroupState(group: { Name: string, Expand: boolean }): boolean { function assignedChanged(e : Event, category : Category){ const target = e.target as HTMLInputElement; const value = target.valueAsNumber; - POST("/budget/"+CurrentBudgetID.value+"/category/" + category.ID + "/" + selected.value.Year + "/" + (selected.value.Month+1), + POST("/budget/"+CurrentBudgetID.value+"/category/" + category.ID + "/" + selected.value.Year + "/" + (selected.value.Month+1), JSON.stringify({Assigned: category.Assigned})); } diff --git a/web/src/pages/Dashboard.vue b/web/src/pages/Dashboard.vue index 1325af1..ec4aac2 100644 --- a/web/src/pages/Dashboard.vue +++ b/web/src/pages/Dashboard.vue @@ -11,15 +11,19 @@ const BudgetsList = useSessionStore().BudgetsList; diff --git a/web/src/pages/Index.vue b/web/src/pages/Index.vue index d16481a..916684d 100644 --- a/web/src/pages/Index.vue +++ b/web/src/pages/Index.vue @@ -1,13 +1,13 @@ - - - \ No newline at end of file + + + diff --git a/web/src/pages/Login.vue b/web/src/pages/Login.vue index 0914df1..964865d 100644 --- a/web/src/pages/Login.vue +++ b/web/src/pages/Login.vue @@ -28,18 +28,27 @@ function formSubmit(e: MouseEvent) { \ No newline at end of file +
+ + +
+
{{ error }}
+ +

+ New user? + Register instead! +

+ diff --git a/web/src/pages/Register.vue b/web/src/pages/Register.vue index cc4a1ec..a2f13a8 100644 --- a/web/src/pages/Register.vue +++ b/web/src/pages/Register.vue @@ -1,48 +1,59 @@ - - - \ No newline at end of file + + + diff --git a/web/src/pages/Settings.vue b/web/src/pages/Settings.vue index 21dab9a..d605b47 100644 --- a/web/src/pages/Settings.vue +++ b/web/src/pages/Settings.vue @@ -72,53 +72,82 @@ function ynabExport() { saveAs(blob, timeStamp + " " + CurrentBudgetName.value + " - Transactions.tsv"); }) } - \ No newline at end of file + + + +

Export as YNAB TSV

+ +
+ +
+
+ + + diff --git a/web/src/pinia-logger.ts b/web/src/pinia-logger.ts index ff30b98..f6c419d 100644 --- a/web/src/pinia-logger.ts +++ b/web/src/pinia-logger.ts @@ -1,81 +1,107 @@ -import { PiniaPluginContext, StoreGeneric, _ActionsTree, _StoreOnActionListenerContext } from 'pinia'; +import { + PiniaPluginContext, + StoreGeneric, + _ActionsTree, + _StoreOnActionListenerContext, +} from "pinia"; const cloneDeep = (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; +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(); + }); - action.after(() => { - log(); - }); + if (options.logErrors) { + action.onError((error) => { + log(true, error); + }); + } + }); + }; - if (options.logErrors) { - action.onError((error) => { - log(true, error); - }); - } - }); -}; - -export default PiniaLogger; \ No newline at end of file +export default PiniaLogger; diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 03b9870..91217e6 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -1,30 +1,75 @@ -import { createRouter, createWebHistory, RouteLocationNormalized } from 'vue-router' -import Dashboard from '../pages/Dashboard.vue'; -import Login from '../pages/Login.vue'; -import Index from '../pages/Index.vue'; -import Register from '../pages/Register.vue'; -import Account from '@/pages/Account.vue'; -import Settings from '../pages/Settings.vue'; -import Budgeting from '../pages/Budgeting.vue'; -import BudgetSidebar from '../pages/BudgetSidebar.vue'; +import { + createRouter, + createWebHistory, + RouteLocationNormalized, +} from "vue-router"; +import Dashboard from "../pages/Dashboard.vue"; +import Login from "../pages/Login.vue"; +import Index from "../pages/Index.vue"; +import Register from "../pages/Register.vue"; +import Account from "@/pages/Account.vue"; +import Settings from "../pages/Settings.vue"; +import Budgeting from "../pages/Budgeting.vue"; +import BudgetSidebar from "../pages/BudgetSidebar.vue"; const routes = [ - { path: "/", name: "Index", component: Index }, - { path: "/dashboard", name: "Dashboard", component: Dashboard, meta: { requiresAuth: true } }, - { path: "/login", name: "Login", component: Login, meta: { hideForAuth: true } }, - { path: "/register", name: "Register", component: Register, meta: { hideForAuth: true } }, - { path: "/budget/:budgetid/budgeting", name: "Budget", redirect: (to : RouteLocationNormalized) => - '/budget/' + to.params.budgetid + '/budgeting/' + new Date().getFullYear() + '/' + new Date().getMonth(), - meta: { requiresAuth: true } - }, - { path: "/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: 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 }, + }, +]; const router = createRouter({ - history: createWebHistory(), - routes, -}) + history: createWebHistory(), + routes, +}); -export default router \ No newline at end of file +export default router; diff --git a/web/src/stores/budget-account.ts b/web/src/stores/budget-account.ts index e582248..9e2a4e1 100644 --- a/web/src/stores/budget-account.ts +++ b/web/src/stores/budget-account.ts @@ -1,195 +1,233 @@ -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 - CurrentAccountID: string | null - Categories: Map - Months: Map>> - Assignments: [] + Accounts: Map; + CurrentAccountID: string | null; + Categories: Map; + Months: Map>>; + 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", { - state: (): State => ({ - Accounts: new Map(), - CurrentAccountID: null, - Months: new Map>>(), - Categories: new Map(), - Assignments: [], - }), - getters: { - AccountsList(state) { - return [...state.Accounts.values()]; - }, - AllCategoriesForMonth: (state) => (year: number, month: number) => { - const yearMap = state.Months.get(year); - const monthMap = yearMap?.get(month); - return [...monthMap?.values() || []]; - }, - GetCategoryAvailable(state) { - return (category: Category): number => { - return category.AvailableLastMonth + Number(category.Assigned) + category.Activity; - } - }, - GetIncomeCategoryID(state) { - const budget = useBudgetsStore(); - return budget.CurrentBudget?.IncomeCategoryID; - }, - GetIncomeAvailable(state) { - return (year: number, month: number) => { - const IncomeCategoryID = this.GetIncomeCategoryID; - if (IncomeCategoryID == null) - return 0; + state: (): State => ({ + Accounts: new Map(), + CurrentAccountID: null, + Months: new Map>>(), + Categories: new Map(), + Assignments: [], + }), + getters: { + AccountsList(state) { + return [...state.Accounts.values()]; + }, + AllCategoriesForMonth: (state) => (year: number, month: number) => { + const yearMap = state.Months.get(year); + const monthMap = yearMap?.get(month); + return [...(monthMap?.values() || [])]; + }, + GetCategoryAvailable(state) { + return (category: Category): number => { + return ( + category.AvailableLastMonth + + Number(category.Assigned) + + category.Activity + ); + }; + }, + GetIncomeCategoryID(state) { + const budget = useBudgetsStore(); + return budget.CurrentBudget?.IncomeCategoryID; + }, + GetIncomeAvailable(state) { + return (year: number, month: number) => { + const IncomeCategoryID = this.GetIncomeCategoryID; + 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; - return category.AvailableLastMonth; - } - }, - CategoryGroupsForMonth(state) { - return (year: number, month: number) => { - const categories = this.AllCategoriesForMonth(year, month); - const categoryGroups = []; - let prev = undefined; - for (const category of categories) { - if (category.ID == this.GetIncomeCategoryID) - continue; + const categories = this.AllCategoriesForMonth(year, month); + const category = categories.filter( + (x) => x.ID == IncomeCategoryID + )[0]; + if (category == null) return 0; + return category.AvailableLastMonth; + }; + }, + CategoryGroupsForMonth(state) { + return (year: number, month: number) => { + const categories = this.AllCategoriesForMonth(year, month); + const categoryGroups = []; + let prev = undefined; + for (const category of categories) { + if (category.ID == this.GetIncomeCategoryID) continue; - if (prev == undefined || category.Group != prev.Name) { - prev = { - Name: category.Group, - Available: this.GetCategoryAvailable(category), - 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; - 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); - } - }, - GetAccount(state) { - return (accountid: string) => { - return this.Accounts.get(accountid); - } - }, - CurrentAccount(state): Account | undefined { - if (state.CurrentAccountID == null) - return undefined; + if (prev == undefined || category.Group != prev.Name) { + prev = { + Name: category.Group, + Available: this.GetCategoryAvailable(category), + 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; + 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); + }; + }, + GetAccount(state) { + return (accountid: string) => { + return this.Accounts.get(accountid); + }; + }, + CurrentAccount(state): Account | undefined { + if (state.CurrentAccountID == null) return undefined; - return this.GetAccount(state.CurrentAccountID); - }, - OnBudgetAccounts(state) { - return [...state.Accounts.values()].filter(x => x.OnBudget); - }, - OnBudgetAccountsBalance(state): number { - return this.OnBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0); - }, - OffBudgetAccounts(state) { - return [...state.Accounts.values()].filter(x => !x.OnBudget); - }, - OffBudgetAccountsBalance(state): number { - return this.OffBudgetAccounts.reduce((prev, curr) => prev + Number(curr.ClearedBalance), 0); - }, - }, - actions: { - async SetCurrentAccount(budgetid: string, accountid: string) { - if (budgetid == null) - return; + return this.GetAccount(state.CurrentAccountID); + }, + OnBudgetAccounts(state) { + return [...state.Accounts.values()].filter((x) => x.OnBudget); + }, + OnBudgetAccountsBalance(state): number { + return this.OnBudgetAccounts.reduce( + (prev, curr) => prev + Number(curr.ClearedBalance), + 0 + ); + }, + OffBudgetAccounts(state) { + return [...state.Accounts.values()].filter((x) => !x.OnBudget); + }, + OffBudgetAccountsBalance(state): number { + return this.OffBudgetAccounts.reduce( + (prev, curr) => prev + Number(curr.ClearedBalance), + 0 + ); + }, + }, + actions: { + async SetCurrentAccount(budgetid: string, accountid: string) { + if (budgetid == null) return; - this.CurrentAccountID = accountid; + this.CurrentAccountID = accountid; - 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 response = await result.json(); - 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 response = await result.json(); - 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 })); - const response = await result.json(); - useBudgetsStore().MergeBudgetingData(response); + 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 response = await result.json(); + 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 response = await result.json(); + 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, + }) + ); + const response = await result.json(); + useBudgetsStore().MergeBudgetingData(response); - if (!isOpen) { - this.Accounts.delete(accountid); - } - }, - addCategoriesForMonth(year: number, month: number, categories: Category[]): void { - this.$patch((state) => { - const yearMap = state.Months.get(year) || new Map>(); - const monthMap = yearMap.get(month) || new Map(); - for (const category of categories) { - monthMap.set(category.ID, category); - } + if (!isOpen) { + this.Accounts.delete(accountid); + } + }, + addCategoriesForMonth( + year: number, + month: number, + categories: Category[] + ): void { + this.$patch((state) => { + const yearMap = + state.Months.get(year) || + new Map>(); + const monthMap = + yearMap.get(month) || new Map(); + for (const category of categories) { + monthMap.set(category.ID, category); + } - yearMap.set(month, monthMap); - state.Months.set(year, yearMap); - }); - }, - logout() { - this.$reset() - }, - } - -}) + yearMap.set(month, monthMap); + state.Months.set(year, yearMap); + }); + }, + logout() { + this.$reset(); + }, + }, +}); diff --git a/web/src/stores/budget.ts b/web/src/stores/budget.ts index 49b8310..f89af7a 100644 --- a/web/src/stores/budget.ts +++ b/web/src/stores/budget.ts @@ -4,67 +4,67 @@ import { useAccountStore } from "./budget-account"; import { Budget, useSessionStore } from "./session"; interface State { - CurrentBudgetID: string | null, + CurrentBudgetID: string | null; } -export const useBudgetsStore = defineStore('budget', { - state: (): State => ({ - CurrentBudgetID: null, - }), - getters: { - CurrentBudget(): Budget | undefined { - if (this.CurrentBudgetID == null) - return undefined; +export const useBudgetsStore = defineStore("budget", { + state: (): State => ({ + CurrentBudgetID: null, + }), + getters: { + CurrentBudget(): Budget | undefined { + if (this.CurrentBudgetID == null) return undefined; - const sessionStore = useSessionStore(); - return sessionStore.Budgets.get(this.CurrentBudgetID); - }, - CurrentBudgetName(state): string { - return this.CurrentBudget?.Name ?? ""; - }, - }, - actions: { - ImportYNAB(formData: FormData) { - return POST( - "/budget/" + this.CurrentBudgetID + "/import/ynab", - formData, - ); - }, - async NewBudget(budgetName: string): Promise { - const result = await POST( - "/budget/new", - JSON.stringify({ name: budgetName }) - ); - const response = await result.json(); + const sessionStore = useSessionStore(); + return sessionStore.Budgets.get(this.CurrentBudgetID); + }, + CurrentBudgetName(state): string { + return this.CurrentBudget?.Name ?? ""; + }, + }, + actions: { + ImportYNAB(formData: FormData) { + return POST( + "/budget/" + this.CurrentBudgetID + "/import/ynab", + formData + ); + }, + async NewBudget(budgetName: string): Promise { + const result = await POST( + "/budget/new", + JSON.stringify({ name: budgetName }) + ); + const response = await result.json(); - const sessionStore = useSessionStore(); - sessionStore.Budgets.set(response.ID, response); - }, - async SetCurrentBudget(budgetid: string): Promise { - this.CurrentBudgetID = budgetid; + const sessionStore = useSessionStore(); + sessionStore.Budgets.set(response.ID, response); + }, + async SetCurrentBudget(budgetid: string): Promise { + this.CurrentBudgetID = budgetid; - if (budgetid == null) - return + if (budgetid == null) return; - await this.FetchBudget(budgetid); - }, - async FetchBudget(budgetid: string) { - const result = await GET("/budget/" + budgetid); - const response = await result.json(); - this.MergeBudgetingData(response); - }, - MergeBudgetingData(response: any) { - const accounts = useAccountStore(); - 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); - accounts.Accounts.set(account.ID, account); - } - for (const category of response.Categories || []) { - accounts.Categories.set(category.ID, category); - } - }, - } -}) \ No newline at end of file + await this.FetchBudget(budgetid); + }, + async FetchBudget(budgetid: string) { + const result = await GET("/budget/" + budgetid); + const response = await result.json(); + this.MergeBudgetingData(response); + }, + MergeBudgetingData(response: any) { + const accounts = useAccountStore(); + 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 + ); + accounts.Accounts.set(account.ID, account); + } + for (const category of response.Categories || []) { + accounts.Categories.set(category.ID, category); + } + }, + }, +}); diff --git a/web/src/stores/session.ts b/web/src/stores/session.ts index 5b8d690..e6cfa1f 100644 --- a/web/src/stores/session.ts +++ b/web/src/stores/session.ts @@ -1,62 +1,74 @@ -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, + Session: Session | null; + Budgets: Map; } 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', { - state: () => ({ - Session: useStorage('session', null, undefined, { serializer: StorageSerializers.object }), - Budgets: useStorage>('budgets', new Map(), undefined, { serializer: StorageSerializers.map }), - }), - getters: { - BudgetsList: (state) => [ ...state.Budgets.values() ], - AuthHeaders: (state) => ({'Authorization': 'Bearer ' + state.Session?.Token}), - LoggedIn: (state) => state.Session != null, - }, - actions: { - setTitle(title : string) { - document.title = "Budgeteer - " + title; - }, - loginSuccess(x : any) { - this.Session = { - User: x.User, - Token: x.Token, - } - for (const budget of x.Budgets) { - this.Budgets.set(budget.ID, budget); - } - }, - async login(login: any) { - const response = await POST("/user/login", JSON.stringify(login)); - const result = await response.json(); - this.loginSuccess(result); - return result; - }, - async register(login : any) { - const response = await POST("/user/register", JSON.stringify(login)); - const result = await response.json(); - this.loginSuccess(result); - return result; - }, - logout() { - this.Session = null; - this.Budgets.clear(); - }, - } -}) \ No newline at end of file +export const useSessionStore = defineStore("session", { + state: () => ({ + Session: useStorage("session", null, undefined, { + serializer: StorageSerializers.object, + }), + Budgets: useStorage>( + "budgets", + new Map(), + undefined, + { serializer: StorageSerializers.map } + ), + }), + getters: { + BudgetsList: (state) => [...state.Budgets.values()], + AuthHeaders: (state) => ({ + Authorization: "Bearer " + state.Session?.Token, + }), + LoggedIn: (state) => state.Session != null, + }, + actions: { + setTitle(title: string) { + document.title = "Budgeteer - " + title; + }, + loginSuccess(x: any) { + this.Session = { + User: x.User, + Token: x.Token, + }; + for (const budget of x.Budgets) { + this.Budgets.set(budget.ID, budget); + } + }, + async login(login: any) { + const response = await POST("/user/login", JSON.stringify(login)); + const result = await response.json(); + this.loginSuccess(result); + return result; + }, + async register(login: any) { + const response = await POST( + "/user/register", + JSON.stringify(login) + ); + const result = await response.json(); + this.loginSuccess(result); + return result; + }, + logout() { + this.Session = null; + this.Budgets.clear(); + }, + }, +}); diff --git a/web/src/stores/settings.ts b/web/src/stores/settings.ts index c63e70a..4d0ea33 100644 --- a/web/src/stores/settings.ts +++ b/web/src/stores/settings.ts @@ -2,27 +2,27 @@ 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', { - state: () => ({ - Menu: useStorage('settings', { - Show: null, - Expand: false, - }), - }), - actions: { - toggleMenu() { - this.Menu.Show = !this.Menu.Show; - }, - toggleMenuSize() { - this.Menu.Expand = !this.Menu.Expand; - }, - } -}); \ No newline at end of file +export const useSettingsStore = defineStore("settings", { + state: () => ({ + Menu: useStorage("settings", { + Show: null, + Expand: false, + }), + }), + actions: { + toggleMenu() { + this.Menu.Show = !this.Menu.Show; + }, + toggleMenuSize() { + this.Menu.Expand = !this.Menu.Expand; + }, + }, +}); diff --git a/web/src/stores/transactions.ts b/web/src/stores/transactions.ts index e8c9825..67d9ace 100644 --- a/web/src/stores/transactions.ts +++ b/web/src/stores/transactions.ts @@ -1,105 +1,110 @@ -import { defineStore } from "pinia" +import { defineStore } from "pinia"; import { POST } from "../api"; import { useAccountStore } from "./budget-account"; interface State { - Transactions: Map - Reconciling: boolean + Transactions: Map; + 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", { - state: (): State => ({ - Transactions: new Map(), - Reconciling: false, - }), - getters: { - ReconcilingBalance(state): number { - const accountsStore = useAccountStore() - let reconciledBalance = accountsStore.CurrentAccount!.ReconciledBalance; - for (const transaction of this.TransactionsList) { - if (transaction.Reconciled) - reconciledBalance += transaction.Amount; - } - return reconciledBalance; - }, - TransactionsList(state): Transaction[] { - const accountsStore = useAccountStore() - return accountsStore.CurrentAccount!.Transactions.map(x => { - return this.Transactions.get(x)! - }); - }, - }, - actions: { - AddTransactions(transactions: Array) { - const transactionIds = [] as Array; - this.$patch(() => { - for (const transaction of transactions) { - transaction.Date = new Date(transaction.Date); - this.Transactions.set(transaction.ID, transaction); - transactionIds.push(transaction.ID); - } - }); - return transactionIds; - }, - SetReconciledForAllTransactions(value: boolean) { - for (const transaction of this.TransactionsList) { - if (transaction.Status == "Reconciled") - continue; + state: (): State => ({ + Transactions: new Map(), + Reconciling: false, + }), + getters: { + ReconcilingBalance(state): number { + const accountsStore = useAccountStore(); + let reconciledBalance = + accountsStore.CurrentAccount!.ReconciledBalance; + for (const transaction of this.TransactionsList) { + if (transaction.Reconciled) + reconciledBalance += transaction.Amount; + } + return reconciledBalance; + }, + TransactionsList(state): Transaction[] { + const accountsStore = useAccountStore(); + return accountsStore.CurrentAccount!.Transactions.map((x) => { + return this.Transactions.get(x)!; + }); + }, + }, + actions: { + AddTransactions(transactions: Array) { + const transactionIds = [] as Array; + this.$patch(() => { + for (const transaction of transactions) { + transaction.Date = new Date(transaction.Date); + this.Transactions.set(transaction.ID, transaction); + transactionIds.push(transaction.ID); + } + }); + return transactionIds; + }, + SetReconciledForAllTransactions(value: boolean) { + for (const transaction of this.TransactionsList) { + if (transaction.Status == "Reconciled") continue; - transaction.Reconciled = value; - } - }, - async SubmitReconcilation(reconciliationTransactionAmount: number) { - const accountsStore = useAccountStore() - const account = accountsStore.CurrentAccount!; - 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 response = await result.json(); - const recTrans = response.ReconciliationTransaction; - if (recTrans) { - this.AddTransactions([recTrans]); - account.Transactions.unshift(recTrans.ID); - } - }, - logout() { - this.$reset() - }, - async saveTransaction(payload: string) { - const accountsStore = useAccountStore() - const result = await POST("/transaction/new", payload); - 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; - this.AddTransactions([response]); - } - } - -}) + transaction.Reconciled = value; + } + }, + async SubmitReconcilation(reconciliationTransactionAmount: number) { + const accountsStore = useAccountStore(); + const account = accountsStore.CurrentAccount!; + 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 response = await result.json(); + const recTrans = response.ReconciliationTransaction; + if (recTrans) { + this.AddTransactions([recTrans]); + account.Transactions.unshift(recTrans.ID); + } + }, + logout() { + this.$reset(); + }, + async saveTransaction(payload: string) { + const accountsStore = useAccountStore(); + const result = await POST("/transaction/new", payload); + 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; + this.AddTransactions([response]); + }, + }, +}); diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 8b26c47..4aff73d 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -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: [], +}; diff --git a/web/tsconfig.json b/web/tsconfig.json index b4309f4..9f45142 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "target": "esnext", - "module": "esnext", - // this enables stricter inference for data properties on `this` - "strict": true, - "jsx": "preserve", - "moduleResolution": "node" - } -} \ No newline at end of file + "compilerOptions": { + "target": "esnext", + "module": "esnext", + // this enables stricter inference for data properties on `this` + "strict": true, + "jsx": "preserve", + "moduleResolution": "node" + } +} diff --git a/web/vite.config.js b/web/vite.config.js index 8e1b264..3cea5ef 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -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'), - }, - }, - 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 + 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 resolve: { extensions: [ '.js', @@ -37,4 +37,4 @@ export default defineConfig({ ] }, */ -}) +});