Move source from flaschengeist main source tree
This commit is contained in:
parent
924728f60d
commit
9bf9c67b5a
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"name": "@flaschengeist/balance",
|
||||
"author": "Ferdinand <rpm@fthiessen.de>",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<q-card>
|
||||
<BalanceHeader v-model="user" :show-selector="showSelector" @open-history="openHistory" />
|
||||
<q-separator />
|
||||
|
||||
<q-card-section v-if="shortCuts" class="row q-col-gutter-md">
|
||||
<div v-for="(shortcut, index) in shortCuts" :key="index" class="col-4">
|
||||
<q-btn
|
||||
v-if="shortcut"
|
||||
push
|
||||
color="primary"
|
||||
style="width: 100%"
|
||||
:label="shortcut.toFixed(2).toString() + ' €'"
|
||||
@click="changeBalance(shortcut)"
|
||||
>
|
||||
<q-popup-proxy context-menu>
|
||||
<q-btn label="Entfernen" @click="removeShortcut(shortcut)" />
|
||||
</q-popup-proxy>
|
||||
<q-tooltip>Rechtsklick um Verknüpfung zu entfernen</q-tooltip>
|
||||
</q-btn>
|
||||
</div></q-card-section
|
||||
>
|
||||
<q-card-section class="row q-col-gutter-md items-center">
|
||||
<div class="col-sm-4 col-xs-12">
|
||||
<q-input
|
||||
v-model.number="amount"
|
||||
type="number"
|
||||
filled
|
||||
label="Eigener Betrag"
|
||||
step="0.1"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-6">
|
||||
<q-btn
|
||||
style="width: 100%"
|
||||
color="primary"
|
||||
label="Anschreiben"
|
||||
@click="changeBalance(amount * -1)"
|
||||
><q-tooltip>Rechtsklick um Betrag als Verknüpfung hinzuzufügen</q-tooltip>
|
||||
<q-popup-proxy v-model="showAddShortcut" context-menu>
|
||||
<q-btn label="neue Verknüpfung" @click="addShortcut"></q-btn>
|
||||
</q-popup-proxy>
|
||||
</q-btn>
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-6">
|
||||
<q-btn
|
||||
v-if="canAddCredit"
|
||||
style="width: 100%"
|
||||
color="secondary"
|
||||
label="Gutschreiben"
|
||||
@click="changeBalance(amount)"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref, defineComponent, onBeforeMount } from 'vue';
|
||||
import { hasPermission, useMainStore } from '@flaschengeist/api';
|
||||
import BalanceHeader from '../components/BalanceHeader.vue';
|
||||
import PERMISSIONS from '../permissions';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BalanceAdd',
|
||||
components: { BalanceHeader },
|
||||
emits: { 'open-history': () => true },
|
||||
setup(_, { emit }) {
|
||||
const store = useBalanceStore();
|
||||
const mainStore = useMainStore();
|
||||
|
||||
onBeforeMount(() => {
|
||||
void store.getShortcuts();
|
||||
});
|
||||
|
||||
const amount = ref<number>(0);
|
||||
const showAddShortcut = ref(false);
|
||||
const user = ref(mainStore.currentUser);
|
||||
const shortCuts = computed(() => store.shortcuts);
|
||||
|
||||
const canAddCredit = hasPermission(PERMISSIONS.CREDIT);
|
||||
const showSelector = hasPermission(PERMISSIONS.DEBIT) || hasPermission(PERMISSIONS.CREDIT);
|
||||
|
||||
function addShortcut() {
|
||||
if (amount.value != 0) void store.createShortcut(amount.value * -1);
|
||||
}
|
||||
function removeShortcut(shortcut: number) {
|
||||
void store.removeShortcut(shortcut);
|
||||
}
|
||||
async function changeBalance(amount: number) {
|
||||
await store.changeBalance(amount, user.value);
|
||||
}
|
||||
|
||||
function openHistory() {
|
||||
emit('open-history');
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
addShortcut,
|
||||
canAddCredit,
|
||||
removeShortcut,
|
||||
showAddShortcut,
|
||||
changeBalance,
|
||||
amount,
|
||||
showSelector,
|
||||
shortCuts,
|
||||
openHistory,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<q-card-section class="fit row justify-left content-center items-center q-col-gutter-sm">
|
||||
<div class="col-5">
|
||||
<div v-if="balance" class="text-h6">
|
||||
Aktueller Stand: {{ balance.balance.toFixed(2) }} €
|
||||
<q-badge v-if="isLocked" color="negative" align="top"> gesperrt </q-badge>
|
||||
</div>
|
||||
<q-spinner v-else color="primary" size="3em" />
|
||||
</div>
|
||||
<div v-if="showSelector" class="col-6">
|
||||
<UserSelector v-model="user" />
|
||||
</div>
|
||||
<div class="col-1 justify-end">
|
||||
<q-btn round flat icon="mdi-format-list-checks" @click="openHistory" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeMount, PropType } from 'vue';
|
||||
import UserSelector from '@flaschengeist/users/src/components/UserSelector.vue';
|
||||
import { useMainStore } from '@flaschengeist/api';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BalanceHeader',
|
||||
components: { UserSelector },
|
||||
props: {
|
||||
showSelector: Boolean,
|
||||
modelValue: {
|
||||
required: true,
|
||||
type: Object as PropType<FG.User>,
|
||||
},
|
||||
},
|
||||
emits: { 'update:modelValue': (u: FG.User) => !!u, 'open-history': () => true },
|
||||
setup(props, { emit }) {
|
||||
const store = useBalanceStore();
|
||||
const mainStore = useMainStore();
|
||||
|
||||
onBeforeMount(() => void store.getBalance(mainStore.currentUser));
|
||||
|
||||
const balance = computed(() =>
|
||||
store.balances.find((x) => x.userid === props.modelValue.userid)
|
||||
);
|
||||
|
||||
const isLocked = computed(
|
||||
() =>
|
||||
balance.value === undefined ||
|
||||
(balance.value.limit !== undefined && balance.value.balance <= balance.value.limit)
|
||||
);
|
||||
const user = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (x: FG.User) => {
|
||||
void store.getBalance(x);
|
||||
emit('update:modelValue', x);
|
||||
},
|
||||
});
|
||||
|
||||
function openHistory() {
|
||||
emit('open-history');
|
||||
}
|
||||
|
||||
return { user, balance, isLocked, openHistory };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<q-card>
|
||||
<BalanceHeader v-model="sender" :show-selector="showSelector" @open-history="openHistory" />
|
||||
<q-separator />
|
||||
<q-card-section class="row q-col-gutter-md items-center">
|
||||
<div class="col-sm-4 col-xs-12">
|
||||
<q-input v-model.number="amount" type="number" filled label="Betrag" step="0.1" min="0" />
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-6">
|
||||
<UserSelector v-model="receiver" label="Empfänger" />
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-6">
|
||||
<q-btn
|
||||
style="width: 100%"
|
||||
color="primary"
|
||||
:disable="sendDisabled"
|
||||
label="Senden"
|
||||
@click="sendAmount"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref, defineComponent } from 'vue';
|
||||
import { hasPermission, useMainStore } from '@flaschengeist/api';
|
||||
import UserSelector from '@flaschengeist/users/src/components/UserSelector.vue';
|
||||
import BalanceHeader from '../components/BalanceHeader.vue';
|
||||
import PERMISSIONS from '../permissions';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BalanceTransfer',
|
||||
components: { BalanceHeader, UserSelector },
|
||||
emits: { 'open-history': () => true },
|
||||
setup(_, { emit }) {
|
||||
const store = useBalanceStore();
|
||||
const mainStore = useMainStore();
|
||||
|
||||
const showSelector = computed(() => hasPermission(PERMISSIONS.SEND_OTHER));
|
||||
const sender = ref<FG.User | undefined>(mainStore.currentUser);
|
||||
const receiver = ref<FG.User | undefined>(undefined);
|
||||
const amount = ref<number>(0);
|
||||
|
||||
const sendDisabled = computed(() => {
|
||||
return !(
|
||||
receiver.value &&
|
||||
sender.value &&
|
||||
sender.value.userid != receiver.value.userid &&
|
||||
amount.value > 0
|
||||
);
|
||||
});
|
||||
|
||||
async function sendAmount() {
|
||||
if (receiver.value) await store.changeBalance(amount.value, receiver.value, sender.value);
|
||||
}
|
||||
|
||||
function openHistory() {
|
||||
emit('open-history');
|
||||
}
|
||||
|
||||
return {
|
||||
sender,
|
||||
receiver,
|
||||
amount,
|
||||
sendAmount,
|
||||
showSelector,
|
||||
sendDisabled,
|
||||
openHistory,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<div>
|
||||
<q-card flat square>
|
||||
<q-card-section class="row items-center justify-between" horizontal>
|
||||
<div class="col-5 text-left q-px-sm">
|
||||
<div :class="{ 'text-negative': isNegative() }" class="text-weight-bold text-h6">
|
||||
<span v-if="isNegative()">-</span>{{ transaction.amount.toFixed(2) }} €
|
||||
</div>
|
||||
<div class="text-caption">{{ text }}</div>
|
||||
<div class="text-caption">{{ timeStr }}</div>
|
||||
</div>
|
||||
<div class="col-5 q-px-sm text-center">
|
||||
<div v-if="isReversed" class="text-subtitle1">Storniert</div>
|
||||
</div>
|
||||
<div class="col-2 q-pr-sm" style="text-align: right">
|
||||
<q-btn
|
||||
:color="isReversed ? 'positive' : 'negative'"
|
||||
aria-label="Reverse transaction"
|
||||
:icon="!isReversed ? 'mdi-trash-can' : 'mdi-check-bold'"
|
||||
size="sm"
|
||||
round
|
||||
:disable="!canReverse"
|
||||
@click="reverse"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-separator />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatDateTime, hasPermission, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||
import { ref, computed, defineComponent, onUnmounted, onMounted, PropType } from 'vue';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Transaction',
|
||||
props: {
|
||||
transaction: {
|
||||
required: true,
|
||||
type: Object as PropType<FG.Transaction>,
|
||||
},
|
||||
},
|
||||
emits: { 'update:transaction': (t: FG.Transaction) => !!t },
|
||||
setup(props, { emit }) {
|
||||
const mainStore = useMainStore();
|
||||
const userStore = useUserStore();
|
||||
const balanceStore = useBalanceStore();
|
||||
const now = ref(Date.now());
|
||||
const ival = setInterval(() => (now.value = Date.now()), 1000);
|
||||
const text = ref('');
|
||||
|
||||
onUnmounted(() => clearInterval(ival));
|
||||
onMounted(() => refreshText());
|
||||
|
||||
const isNegative = () => props.transaction.sender_id === mainStore.currentUser.userid;
|
||||
|
||||
const refreshText = async () => {
|
||||
if (isNegative()) {
|
||||
text.value = 'Anschreiben';
|
||||
if (props.transaction.receiver_id) {
|
||||
const user = <FG.User>await userStore.getUser(props.transaction.receiver_id);
|
||||
text.value = `Gesendet an ${user.display_name}`;
|
||||
}
|
||||
} else {
|
||||
text.value = 'Gutschrift';
|
||||
if (props.transaction.sender_id) {
|
||||
const user = <FG.User>await userStore.getUser(props.transaction.sender_id);
|
||||
text.value = `Bekommen von ${user.display_name}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isReversed = computed(
|
||||
() => props.transaction.reversal_id != undefined || props.transaction.original_id != undefined
|
||||
);
|
||||
|
||||
const canReverse = computed(
|
||||
() =>
|
||||
!isReversed.value &&
|
||||
(hasPermission('balance_reversal') ||
|
||||
(props.transaction.sender_id === mainStore.currentUser.userid &&
|
||||
now.value - props.transaction.time.getTime() < 10000))
|
||||
);
|
||||
|
||||
function reverse() {
|
||||
if (canReverse.value)
|
||||
void balanceStore.revert(props.transaction).then(() => {
|
||||
emit('update:transaction', props.transaction);
|
||||
});
|
||||
}
|
||||
|
||||
const timeStr = computed(() => {
|
||||
const elapsed = (now.value - props.transaction.time.getTime()) / 1000;
|
||||
if (elapsed < 60) return `Vor ${elapsed.toFixed()} s`;
|
||||
return formatDateTime(props.transaction.time, elapsed > 12 * 60 * 60, true, true) + ' Uhr';
|
||||
});
|
||||
|
||||
return { timeStr, reverse, isNegative, text, refreshText, canReverse, isReversed };
|
||||
},
|
||||
watch: { transaction: 'refreshText' },
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<q-card style="text-align: center">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Gerücht: {{ balance.toFixed(2) }} €</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeMount } from 'vue';
|
||||
import { useMainStore } from '@flaschengeist/api';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BalanceWidget',
|
||||
setup() {
|
||||
const store = useBalanceStore();
|
||||
|
||||
onBeforeMount(() => {
|
||||
const mainStore = useMainStore();
|
||||
void store.getBalance(mainStore.currentUser);
|
||||
});
|
||||
|
||||
const balance = computed(() => store.balance?.balance || NaN);
|
||||
|
||||
return { balance };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -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;
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div>
|
||||
<q-page padding>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table :rows="rows" row-key="userid" :columns="columns" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// TODO: Fill usefull data
|
||||
|
||||
import { ref, defineComponent, onMounted } from 'vue';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
// name: 'PageName'
|
||||
setup() {
|
||||
const store = useBalanceStore();
|
||||
|
||||
onMounted(() => void store.getBalances().then((balances) => rows.value.push(...balances)));
|
||||
|
||||
const rows = ref(store.balances);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'userid',
|
||||
label: 'Benutzer ID',
|
||||
field: 'userid',
|
||||
required: true,
|
||||
align: 'left',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'credit',
|
||||
label: 'Haben',
|
||||
field: 'credit',
|
||||
format: (val: number) => val.toFixed(2)
|
||||
},
|
||||
{
|
||||
name: 'debit',
|
||||
label: 'Soll',
|
||||
field: 'debit',
|
||||
format: (val: number) => val.toFixed(2)
|
||||
},
|
||||
{
|
||||
name: 'balance',
|
||||
label: 'Kontostand',
|
||||
format: (_: undefined, row: { debit: number; credit: number }) =>
|
||||
(row.credit - row.debit).toFixed(2)
|
||||
}
|
||||
];
|
||||
return { rows, columns };
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
||||
<q-tab
|
||||
v-for="(tabindex, index) in tabs"
|
||||
:key="'tab' + index"
|
||||
:name="tabindex.name"
|
||||
:label="tabindex.label"
|
||||
/>
|
||||
</q-tabs>
|
||||
<div v-else class="fit row justify-end">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="mdi-menu"
|
||||
@click="
|
||||
showDrawer = !showDrawer;
|
||||
show = false;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
||||
<q-list v-if="!$q.screen.gt.sm && !show" v-model="tab">
|
||||
<q-item
|
||||
v-for="(tabindex, index) in tabs"
|
||||
:key="'tab' + index"
|
||||
:active="tab == tabindex.name"
|
||||
clickable
|
||||
@click="tab = tabindex.name"
|
||||
>
|
||||
<q-item-label>{{ tabindex.label }}</q-item-label>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-list v-if="show">
|
||||
<div v-for="(transaction, index) in transactions" :key="index" class="col-sm-12">
|
||||
<Transaction v-model:transaction="transactions[index]" />
|
||||
</div>
|
||||
</q-list>
|
||||
</q-drawer>
|
||||
<q-tab-panels
|
||||
v-model="tab"
|
||||
style="background-color: transparent"
|
||||
class="q-pa-none col-12"
|
||||
animated
|
||||
>
|
||||
<q-tab-panel name="add" class="q-px-xs">
|
||||
<BalanceAdd
|
||||
@open-history="
|
||||
showDrawer = !showDrawer;
|
||||
show = true;
|
||||
"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="transfer" class="q-px-xs">
|
||||
<BalanceTransfer
|
||||
@open-history="
|
||||
showDrawer = !showDrawer;
|
||||
show = true;
|
||||
"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, onMounted } from 'vue';
|
||||
import { hasSomePermissions, useMainStore } from '@flaschengeist/api';
|
||||
import BalanceTransfer from '../components/BalanceTransfer.vue';
|
||||
import Transaction from '../components/Transaction.vue';
|
||||
import BalanceAdd from '../components/BalanceAdd.vue';
|
||||
import { useBalanceStore } from '../store';
|
||||
import PERMISSIONS from '../permissions';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BalanceManage',
|
||||
components: { BalanceAdd, BalanceTransfer, Transaction },
|
||||
setup() {
|
||||
const balanceStore = useBalanceStore();
|
||||
const mainStore = useMainStore();
|
||||
|
||||
const now = new Date();
|
||||
onMounted(() => {
|
||||
void balanceStore.getTransactions(mainStore.currentUser, {
|
||||
from: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
|
||||
});
|
||||
});
|
||||
|
||||
const transactions = computed(() => {
|
||||
return balanceStore.transactions
|
||||
.filter((t) => t.original_id == undefined)
|
||||
.filter((t) => t.time > new Date(now.getFullYear(), now.getMonth(), now.getDate()))
|
||||
.sort((a, b) => (a.time >= b.time ? -1 : 1));
|
||||
});
|
||||
|
||||
const canAdd = () =>
|
||||
hasSomePermissions([PERMISSIONS.DEBIT, PERMISSIONS.CREDIT, PERMISSIONS.DEBIT_OWN]);
|
||||
|
||||
interface Tab {
|
||||
name: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const tabs: Tab[] = [
|
||||
...(canAdd() ? [{ name: 'add', label: 'Anschreiben' }] : []),
|
||||
...(hasSomePermissions([PERMISSIONS.SEND, PERMISSIONS.SEND_OTHER])
|
||||
? [{ name: 'transfer', label: 'Übertragen' }]
|
||||
: []),
|
||||
];
|
||||
|
||||
//const drawer = ref<boolean>(false);
|
||||
|
||||
/* const showDrawer = computed({
|
||||
get: () => {
|
||||
return !Screen.gt.sm && drawer.value;
|
||||
},
|
||||
set: (val: boolean) => {
|
||||
drawer.value = val;
|
||||
}
|
||||
});
|
||||
*/
|
||||
const showDrawer = ref<boolean>(false);
|
||||
const tab = ref<string>(canAdd() ? 'add' : 'transfer');
|
||||
const show = ref<boolean>(false);
|
||||
return {
|
||||
showDrawer,
|
||||
tab,
|
||||
tabs,
|
||||
transactions,
|
||||
show,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<q-card>
|
||||
<q-card-section class="text-center">
|
||||
<div class="text-h4">Aktueller Stand</div>
|
||||
<div style="font-size: 2em">{{ balance.toFixed(2) }} €</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-section>
|
||||
<q-table
|
||||
v-model:pagination="pagination"
|
||||
title="Buchungen"
|
||||
:rows="data"
|
||||
:columns="columns"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
binary-state-sort
|
||||
@request="onRequest"
|
||||
>
|
||||
<template #top>
|
||||
<q-toggle v-model="showCancelled" label="Stornierte einblenden" />
|
||||
</template>
|
||||
<template #body-cell="props">
|
||||
<q-td :props="props" :class="{ 'bg-grey': props.row.reversal_id != null }">
|
||||
{{ props.value }}
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatDateTime, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||
import { computed, defineComponent, onMounted, ref } from 'vue';
|
||||
import { useBalanceStore } from '../store';
|
||||
|
||||
export default defineComponent({
|
||||
// name: 'PageName'
|
||||
setup() {
|
||||
const store = useBalanceStore();
|
||||
const mainStore = useMainStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
onMounted(() => {
|
||||
void store.getBalance(mainStore.currentUser);
|
||||
void userStore.getUsers().then(() =>
|
||||
onRequest({
|
||||
pagination: pagination.value,
|
||||
filter: undefined
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const showCancelled = ref(false);
|
||||
const data = ref<FG.Transaction[]>([]);
|
||||
const loading = ref(false);
|
||||
const pagination = ref({
|
||||
sortBy: 'time',
|
||||
descending: false,
|
||||
page: 1,
|
||||
rowsPerPage: 3,
|
||||
rowsNumber: 10
|
||||
});
|
||||
|
||||
interface PaginationInterface {
|
||||
sortBy: string;
|
||||
descending: boolean;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
rowsNumber: number;
|
||||
}
|
||||
|
||||
async function onRequest(props: { pagination: PaginationInterface; filter?: string }) {
|
||||
const { page, rowsPerPage, sortBy, descending } = props.pagination;
|
||||
|
||||
loading.value = true;
|
||||
// get all rows if "All" (0) is selected
|
||||
const fetchCount = rowsPerPage === 0 ? pagination.value.rowsNumber : rowsPerPage;
|
||||
// calculate starting row of data
|
||||
const startRow = (page - 1) * rowsPerPage;
|
||||
try {
|
||||
const result = await store.getTransactions(mainStore.currentUser, {
|
||||
offset: startRow,
|
||||
limit: fetchCount,
|
||||
showCancelled: showCancelled.value,
|
||||
showReversals: false
|
||||
});
|
||||
// clear out existing data and add new
|
||||
data.value.splice(0, data.value.length, ...result.transactions);
|
||||
// don't forget to update local pagination object
|
||||
pagination.value.page = page;
|
||||
pagination.value.rowsPerPage = rowsPerPage;
|
||||
pagination.value.sortBy = sortBy;
|
||||
pagination.value.descending = descending;
|
||||
if (result.count) pagination.value.rowsNumber = result.count;
|
||||
} catch (error) {
|
||||
// ...
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const balance = computed(() => store.balance?.balance || NaN);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'time',
|
||||
label: 'Datum',
|
||||
field: 'time',
|
||||
required: true,
|
||||
sortable: true,
|
||||
format: (val: Date) => formatDateTime(new Date(val), true, true, true)
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
format: (_: undefined, row: FG.Transaction) => {
|
||||
if (row.sender_id == null) return 'Gutschrift';
|
||||
else {
|
||||
if (row.receiver_id == null) return 'Angeschrieben';
|
||||
else {
|
||||
if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
|
||||
else return 'Gesendet an X';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
label: 'Text',
|
||||
format: (_: undefined, row: FG.Transaction) => {
|
||||
if (row.sender_id == null) return 'Gutschrift';
|
||||
else {
|
||||
if (row.receiver_id == null) return 'Angeschrieben';
|
||||
else {
|
||||
if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
|
||||
else return 'Gesendet an X';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'amount',
|
||||
label: 'Betrag',
|
||||
field: 'amount',
|
||||
format: (val: number) => `${val.toFixed(2)}€`
|
||||
},
|
||||
{
|
||||
name: 'author_id',
|
||||
label: 'Benutzer',
|
||||
field: 'author_id',
|
||||
format: (val: string) => {
|
||||
const user = userStore.users.filter((x) => x.userid == val);
|
||||
if (user.length > 0) return user[0].display_name;
|
||||
else return val;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return { data, pagination, onRequest, loading, balance, columns, showCancelled };
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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<FG.Transaction>;
|
||||
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<number[]>(
|
||||
`/users/${mainStore.currentUser.userid}/balance/shortcuts`
|
||||
);
|
||||
this.shortcuts = data;
|
||||
}
|
||||
},
|
||||
|
||||
async getBalance(user: FG.User) {
|
||||
const { data } = await api.get<BalanceResponse>(`/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<BalancesResponse[]>('/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<FG.Transaction>(`/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 && (<AxiosResponse>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<TransactionsResponse>(
|
||||
`/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<FG.Transaction>(`/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;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "@quasar/app/tsconfig-preset",
|
||||
"target": "esnext",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src/",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom"
|
||||
],
|
||||
"types": [
|
||||
"@flaschengeist/types",
|
||||
"@quasar/app",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue