diff --git a/package.json b/package.json new file mode 100644 index 0000000..a60f10e --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "private": true, + "license": "MIT", + "version": "1.0.0-alpha.1", + "name": "@flaschengeist/balance", + "author": "Ferdinand ", + "homepage": "https://flaschengeist.dev/Flaschengeist", + "description": "Flaschengeist balance plugin", + "bugs": { + "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues" + }, + "repository": { + "type": "git", + "url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-balance" + }, + "main": "src/index.ts", + "scripts": { + "valid": "tsc --noEmit", + "pretty": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'", + "lint": "eslint --ext .js,.ts,.vue ./src" + }, + "devDependencies": { + "@flaschengeist/types": "git+https://flaschengeist.dev/ferfissimo/flaschengeist-types.git#develop", + "@quasar/app": "^3.0.0-beta.25", + "axios": "^0.21.1", + "prettier": "^2.3.0", + "typescript": "^4.2.4", + "pinia": "^2.0.0-alpha.19", + "quasar": "^2.0.0-beta.18", + "@typescript-eslint/eslint-plugin": "^4.24.0", + "@typescript-eslint/parser": "^4.24.0", + "eslint": "^7.26.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-vue": "^7.9.0" + }, + "peerDependencies": { + "@flaschengeist/api": "1.0.0-alpha.1", + "@flaschengeist/users": "1.0.0-alpha.1" + }, + "prettier": { + "singleQuote": true, + "semi": true, + "printWidth": 100, + "arrowParens": "always" + } + } + \ No newline at end of file diff --git a/src/components/BalanceAdd.vue b/src/components/BalanceAdd.vue new file mode 100644 index 0000000..ef17e10 --- /dev/null +++ b/src/components/BalanceAdd.vue @@ -0,0 +1,114 @@ + + + diff --git a/src/components/BalanceHeader.vue b/src/components/BalanceHeader.vue new file mode 100644 index 0000000..7a6a5b5 --- /dev/null +++ b/src/components/BalanceHeader.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/components/BalanceTransfer.vue b/src/components/BalanceTransfer.vue new file mode 100644 index 0000000..d4429b8 --- /dev/null +++ b/src/components/BalanceTransfer.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/components/Transaction.vue b/src/components/Transaction.vue new file mode 100644 index 0000000..4b07771 --- /dev/null +++ b/src/components/Transaction.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/components/Widget.vue b/src/components/Widget.vue new file mode 100644 index 0000000..728c222 --- /dev/null +++ b/src/components/Widget.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..13ad746 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +import { FG_Plugin } from '@flaschengeist/types'; +import { defineAsyncComponent } from 'vue'; +import routes from './routes'; + +const plugin: FG_Plugin.Plugin = { + id: 'balance', + name: 'Balance', + innerRoutes: routes, + requiredModules: [['balance']], + version: '0.0.2', + widgets: [ + { + priority: 0, + name: 'current', + permissions: ['balance_show'], + widget: defineAsyncComponent(() => import('./components/Widget.vue')), + }, + ], +}; + +export default plugin; diff --git a/src/pages/Admin.vue b/src/pages/Admin.vue new file mode 100644 index 0000000..eeab423 --- /dev/null +++ b/src/pages/Admin.vue @@ -0,0 +1,59 @@ + + + diff --git a/src/pages/MainPage.vue b/src/pages/MainPage.vue new file mode 100644 index 0000000..1289bfd --- /dev/null +++ b/src/pages/MainPage.vue @@ -0,0 +1,134 @@ + + + diff --git a/src/pages/Overview.vue b/src/pages/Overview.vue new file mode 100644 index 0000000..fc421dd --- /dev/null +++ b/src/pages/Overview.vue @@ -0,0 +1,164 @@ + + + diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 0000000..536f0f5 --- /dev/null +++ b/src/permissions.ts @@ -0,0 +1,21 @@ +const PERMISSIONS = { + // Show own and others balance + SHOW: 'balance_show', + SHOW_OTHER: 'balance_show_others', + // Credit balance (give) + CREDIT: 'balance_credit', + // Debit balance (take) + DEBIT: 'balance_debit', + // Debit own balance only + DEBIT_OWN: 'balance_debit_own', + // Send from to other + SEND: 'balance_send', + // Send from other to another + SEND_OTHER: 'balance_send_others', + // Can set limit for users + SET_LIMIT: 'balance_set_limit', + //Allow sending / sub while exceeding the set limit + EXCEED_LIMIT: 'balance_exceed_limit', +}; + +export default PERMISSIONS; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..13c5b4e --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,50 @@ +import { FG_Plugin } from '@flaschengeist/types'; +import permissions from '../permissions'; + +const mainRoutes: FG_Plugin.MenuRoute[] = [ + { + title: 'Gerücht', + icon: 'mdi-cash-100', + permissions: ['user'], + route: { + path: 'balance', + name: 'balance', + redirect: { name: 'balance-view' }, + }, + children: [ + { + title: 'Übersicht', + icon: 'mdi-cash-check', + permissions: [permissions.SHOW], + route: { + path: 'overview', + name: 'balance-view', + component: () => import('../pages/Overview.vue'), + }, + }, + { + title: 'Buchen', + icon: 'mdi-cash-plus', + shortcut: true, + permissions: [permissions.DEBIT_OWN, permissions.SHOW], + route: { + path: 'change', + name: 'balance-change', + component: () => import('../pages/MainPage.vue'), + }, + }, + { + title: 'Verwaltung', + icon: 'mdi-account-cash', + permissions: [permissions.SET_LIMIT, permissions.SHOW_OTHER], + route: { + path: 'admin', + name: 'balance-admin', + component: () => import('../pages/Admin.vue'), + }, + }, + ], + }, +]; + +export default mainRoutes; diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts new file mode 100644 index 0000000..eab6529 --- /dev/null +++ b/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +//https://github.com/vuejs/vue-next/issues/3130 +declare module '*.vue' { + import { ComponentOptions } from 'vue'; + const component: ComponentOptions; + export default component; +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..6394acc --- /dev/null +++ b/src/store.ts @@ -0,0 +1,164 @@ +import { api, useMainStore } from '@flaschengeist/api'; +import { defineStore } from 'pinia'; +import { AxiosResponse } from 'axios'; +import { Notify } from 'quasar'; + +interface BalanceResponse { + balance: number; + credit: number; + debit: number; + limit?: number; +} + +export interface BalancesResponse extends BalanceResponse { + userid: string; +} + +export interface TransactionsResponse { + transactions: Array; + count?: number; +} + +function fixTransaction(t: FG.Transaction) { + t.time = new Date(t.time); +} + +export const useBalanceStore = defineStore({ + id: 'balance', + + state: () => ({ + balances: [] as BalancesResponse[], + shortcuts: [] as number[], + transactions: [] as FG.Transaction[], + _balances_dirty: 0, + }), + + getters: { + balance(): BalancesResponse | undefined { + const mainStore = useMainStore(); + return this.balances.find((v) => v.userid === mainStore.user?.userid); + }, + }, + + actions: { + async createShortcut(shortcut: number) { + const mainStore = useMainStore(); + this.shortcuts.push(shortcut); + this.shortcuts.sort((a, b) => a - b); + await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, this.shortcuts); + }, + + async removeShortcut(shortcut: number) { + const mainStore = useMainStore(); + this.shortcuts = this.shortcuts.filter((value: number) => value !== shortcut); + this.shortcuts.sort((a, b) => a - b); + await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, this.shortcuts); + }, + + async getShortcuts(force = false) { + if (force || this.shortcuts.length == 0) { + const mainStore = useMainStore(); + const { data } = await api.get( + `/users/${mainStore.currentUser.userid}/balance/shortcuts` + ); + this.shortcuts = data; + } + }, + + async getBalance(user: FG.User) { + const { data } = await api.get(`/users/${user.userid}/balance`); + const idx = this.balances.findIndex((x) => x.userid === user.userid); + if (idx == -1) this.balances.push(Object.assign(data, { userid: user.userid })); + else this.balances[idx] = Object.assign(data, { userid: user.userid }); + return data; + }, + + async getBalances(force = false) { + if ( + force || + this.balances.length == 0 || + new Date().getTime() - this._balances_dirty > 60000 + ) { + const { data } = await api.get('/balance'); + this.balances = data; + } + return this.balances; + }, + + async changeBalance(amount: number, user: FG.User, sender: FG.User | undefined = undefined) { + const mainStore = useMainStore(); + try { + const { data } = await api.put(`/users/${user.userid}/balance`, { + amount, + user: user.userid, + sender: sender?.userid, + }); + fixTransaction(data); + if ( + user.userid === mainStore.currentUser.userid || + sender?.userid === mainStore.currentUser.userid + ) + this.transactions.push(data); + const f = this.balances.find((x) => x.userid === user.userid); + if (f) f.balance += amount; + if (sender) { + const f = this.balances.find((x) => x.userid === sender.userid); + if (f) f.balance += -1 * amount; + } + this._balances_dirty = 0; + return data; + } catch ({ response }) { + // Maybe Balance changed + if (response && (response).status == 409) { + Notify.create({ + type: 'negative', + group: false, + message: 'Das Limit wurde überschritten!', + timeout: 10000, + progress: true, + actions: [{ icon: 'mdi-close', color: 'white' }], + }); + //void this.getTransactions(true); + void this.getBalance(sender ? sender : user); + } + } + }, + + async getTransactions( + user: FG.User, + filter: + | { + limit?: number; + offset?: number; + from?: Date; + to?: Date; + showReversals?: boolean; + showCancelled?: boolean; + } + | undefined = undefined + ) { + if (!filter) filter = { limit: 10 }; + const { data } = await api.get( + `/users/${user.userid}/balance/transactions`, + { params: filter } + ); + data.transactions.forEach((t) => fixTransaction(t)); + if (data.transactions) this.transactions.push(...data.transactions); + return data; + }, + + async revert(transaction: FG.Transaction) { + try { + const { data } = await api.delete(`/balance/${transaction.id}`); + fixTransaction(data); + const f = this.transactions.find((x) => x.id === transaction.id); + if (f) f.reversal_id = data.id; + this.transactions.push(data); + console.log(data); + } catch (error) { + // ... + } + this._balances_dirty = 0; + }, + }, +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eb38103 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@quasar/app/tsconfig-preset", + "target": "esnext", + "compilerOptions": { + "baseUrl": "src/", + "lib": [ + "es2020", + "dom" + ], + "types": [ + "@flaschengeist/types", + "@quasar/app", + "node" + ] + } +}