Balance: Added Transfer and Admin view + more
* some work on reverting transactions. * Added TODO comments on incomplete features
This commit is contained in:
		
							parent
							
								
									7748d2d8a3
								
							
						
					
					
						commit
						01143e08e8
					
				| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-card-section class="fit row justify-left content-center items-center q-col-gutter-sm">
 | 
				
			||||||
 | 
					    <div class="text-h6 col-6">
 | 
				
			||||||
 | 
					      Aktueller Stand: {{ balance.balance.toFixed(2) }} €
 | 
				
			||||||
 | 
					      <q-badge color="negative" align="top" v-if="isLocked"> gesperrt </q-badge>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div v-if="showSelector" class="col-6">
 | 
				
			||||||
 | 
					      <UserSelector :user="user" @update:user="userUpdated" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </q-card-section>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import { ref, computed, defineComponent, onBeforeMount } from '@vue/composition-api';
 | 
				
			||||||
 | 
					import UserSelector from 'src/plugins/user/components/UserSelector.vue';
 | 
				
			||||||
 | 
					import { StateInterfaceBalance, UserBalance } from '../store/balance';
 | 
				
			||||||
 | 
					import { Store } from 'vuex';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					    showSelector: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
					  name: 'BalanceHeader',
 | 
				
			||||||
 | 
					  components: { UserSelector },
 | 
				
			||||||
 | 
					  props: ['showSelector'],
 | 
				
			||||||
 | 
					  setup(props: Props, { root, emit }) {
 | 
				
			||||||
 | 
					    onBeforeMount(() => void store.dispatch('balance/getBalance'));
 | 
				
			||||||
 | 
					    const store = <Store<StateInterfaceBalance>>root.$store;
 | 
				
			||||||
 | 
					    const user = ref(<FG.User>store.state.user.currentUser);
 | 
				
			||||||
 | 
					    const balance = computed(() => {const balances = store.state.balance.balances; return balances.get(user.value.userid) || {balance: 0, limit: null} ;});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isLocked = computed(() => balance.value.limit !== null && balance.value.balance >= balance.value.limit);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function userUpdated(selectedUser: FG.User) {
 | 
				
			||||||
 | 
					        void store.dispatch('balance/getBalance', selectedUser);
 | 
				
			||||||
 | 
					        user.value = selectedUser;
 | 
				
			||||||
 | 
					        emit('update:user', selectedUser);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { user, balance, isLocked, userUpdated };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,76 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-card>
 | 
				
			||||||
 | 
					    <q-card-actions align="right">
 | 
				
			||||||
 | 
					      <q-btn
 | 
				
			||||||
 | 
					        :color="color()"
 | 
				
			||||||
 | 
					        icon="mdi-trash-can"
 | 
				
			||||||
 | 
					        aria-label="Löschen"
 | 
				
			||||||
 | 
					        :disable="disabled()"
 | 
				
			||||||
 | 
					        @click="reverse(transaction)"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </q-card-actions>
 | 
				
			||||||
 | 
					    <q-card-section>
 | 
				
			||||||
 | 
					      <span>{{ timeStr }}: {{ transaction.amount }}</span>
 | 
				
			||||||
 | 
					    </q-card-section>
 | 
				
			||||||
 | 
					  </q-card>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					// TODO: Better styling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ref, computed, defineComponent, onUnmounted } from '@vue/composition-api';
 | 
				
			||||||
 | 
					import { hasPermission } from 'src/utils/permission';
 | 
				
			||||||
 | 
					import { formatDateTime } from 'src/utils/datetime';
 | 
				
			||||||
 | 
					import { StateInterfaceBalance } from 'src/plugins/balance/store/balance';
 | 
				
			||||||
 | 
					import { Store } from 'vuex';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  transaction: FG.Transaction;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
					  name: 'Transaction',
 | 
				
			||||||
 | 
					  props: ['transaction'],
 | 
				
			||||||
 | 
					  setup(props: Props, { root, emit }) {
 | 
				
			||||||
 | 
					    const now = ref(Date.now());
 | 
				
			||||||
 | 
					    const ival = setInterval(() => (now.value = Date.now()), 1000);
 | 
				
			||||||
 | 
					    const store: Store<StateInterfaceBalance> = <Store<StateInterfaceBalance>>root.$store;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onUnmounted(() => clearInterval(ival));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function canReverse(transaction: FG.Transaction) {
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        hasPermission('balance_reversal', store) ||
 | 
				
			||||||
 | 
					        (transaction.sender_id === store.state.user.currentUser?.userid &&
 | 
				
			||||||
 | 
					          Date.now() - transaction.time.getTime() < 10000)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function color() {
 | 
				
			||||||
 | 
					      return canReverse(props.transaction) ? 'negative' : 'grey';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function disabled() {
 | 
				
			||||||
 | 
					      return !canReverse(props.transaction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function reverse(transaction: FG.Transaction) {
 | 
				
			||||||
 | 
					      if (canReverse(transaction))
 | 
				
			||||||
 | 
					        store
 | 
				
			||||||
 | 
					          .dispatch('balance/revert', transaction)
 | 
				
			||||||
 | 
					          .then(() => {
 | 
				
			||||||
 | 
					            emit('reversed', transaction.id);
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch(error => console.log(error));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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, disabled, color, reverse };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -1,95 +1,123 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <q-page padding>
 | 
					  <q-page padding class="fit row justify-left q-col-gutter-sm">
 | 
				
			||||||
    <q-card>
 | 
					    <div class="col-12">
 | 
				
			||||||
      <q-card-section class="row">
 | 
					      <q-card>
 | 
				
			||||||
        <div class="col-4 row q-pa-sm">
 | 
					        <BalanceHeader @update:user="userUpdated" :showSelector="showSelector" />
 | 
				
			||||||
          <q-btn
 | 
					        <q-separator />
 | 
				
			||||||
            class="col"
 | 
					
 | 
				
			||||||
            color="green"
 | 
					        <q-card-section class="row q-col-gutter-md" v-if="shortCuts">
 | 
				
			||||||
            label="2€"
 | 
					          <div v-for="(amount, index) in shortCuts" v-bind:key="index" class="col-4">
 | 
				
			||||||
            @click="changeBalance(-2)"
 | 
					            <q-btn
 | 
				
			||||||
          />
 | 
					              color="primary"
 | 
				
			||||||
        </div>
 | 
					              style="width: 100%"
 | 
				
			||||||
        <div class="col-4 row q-pa-sm">
 | 
					              :label="amount.toFixed(2).toString() + ' €'"
 | 
				
			||||||
          <q-btn
 | 
					              @click="changeBalance(amount)"
 | 
				
			||||||
            class="col"
 | 
					            />
 | 
				
			||||||
            color="green"
 | 
					          </div>
 | 
				
			||||||
            label="1€"
 | 
					        </q-card-section>
 | 
				
			||||||
            @click="changeBalance(-1)"
 | 
					        <q-card-section class="row q-col-gutter-md items-center">
 | 
				
			||||||
          />
 | 
					          <div class="col-sm-4 col-xs-12">
 | 
				
			||||||
        </div>
 | 
					            <q-input
 | 
				
			||||||
        <div class="col-4 row q-pa-sm">
 | 
					              v-model.number="amount"
 | 
				
			||||||
          <q-btn
 | 
					              type="number"
 | 
				
			||||||
            class="col"
 | 
					              filled
 | 
				
			||||||
            color="green"
 | 
					              label="Eigener Betrag"
 | 
				
			||||||
            label="0,50€"
 | 
					              step="0.1"
 | 
				
			||||||
            @click="changeBalance(-0.5)"
 | 
					              min="0"
 | 
				
			||||||
          />
 | 
					            />
 | 
				
			||||||
        </div>
 | 
					          </div>
 | 
				
			||||||
        <div class="col-4 row q-pa-sm">
 | 
					          <div class="col-sm-4 col-xs-6">
 | 
				
			||||||
          <q-btn
 | 
					            <q-btn
 | 
				
			||||||
            class="col"
 | 
					              style="width: 100%"
 | 
				
			||||||
            color="green"
 | 
					              color="primary"
 | 
				
			||||||
            label="0,40€"
 | 
					              label="Anschreiben"
 | 
				
			||||||
            @click="changeBalance(-0.4)"
 | 
					              @click="changeBalance(amount * -1)"
 | 
				
			||||||
          />
 | 
					              ><q-tooltip>Rechtsklick um Betrag als Verknüpfung hinzuzufügen</q-tooltip>
 | 
				
			||||||
        </div>
 | 
					              <q-popup-proxy context-menu v-model="showAddShortcut">
 | 
				
			||||||
        <div class="col-4 row q-pa-sm">
 | 
					                <q-btn label="neue Verknüpfung" @click="addShortcut" /></q-popup-proxy
 | 
				
			||||||
          <q-btn
 | 
					            ></q-btn>
 | 
				
			||||||
            class="col"
 | 
					          </div>
 | 
				
			||||||
            color="green"
 | 
					          <div class="col-sm-4 col-xs-6">
 | 
				
			||||||
            label="0,20€"
 | 
					            <q-btn
 | 
				
			||||||
            @click="changeBalance(-0.2)"
 | 
					              style="width: 100%"
 | 
				
			||||||
          />
 | 
					              color="secondary"
 | 
				
			||||||
        </div>
 | 
					              label="Gutschreiben"
 | 
				
			||||||
        <div class="col-4 row q-pa-sm">
 | 
					              @click="changeBalance(amount)"
 | 
				
			||||||
          <q-btn
 | 
					            />
 | 
				
			||||||
            class="col"
 | 
					          </div>
 | 
				
			||||||
            color="green"
 | 
					        </q-card-section>
 | 
				
			||||||
            label="0,10€"
 | 
					      </q-card>
 | 
				
			||||||
            @click="changeBalance(-0.1)"
 | 
					    </div>
 | 
				
			||||||
          />
 | 
					    <div v-for="(transaction, index) in transactions" v-bind:key="index" class="col-sm-4 col-xs-6">
 | 
				
			||||||
        </div>
 | 
					      <Transaction :transaction="transaction" @reversed="reversed" />
 | 
				
			||||||
      </q-card-section>
 | 
					    </div>
 | 
				
			||||||
      <q-card-section>
 | 
					 | 
				
			||||||
        <div class="text-h6">{{ balance.toFixed(2) }} €</div>
 | 
					 | 
				
			||||||
      </q-card-section>
 | 
					 | 
				
			||||||
      <q-card-actions>
 | 
					 | 
				
			||||||
        <q-btn label="test" @click="$store.dispatch('balance/getBalance')" />
 | 
					 | 
				
			||||||
      </q-card-actions>
 | 
					 | 
				
			||||||
    </q-card>
 | 
					 | 
				
			||||||
  </q-page>
 | 
					  </q-page>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { computed, defineComponent, onBeforeMount } from '@vue/composition-api';
 | 
					// TODO: Shortcuts are not displayed when first loaded
 | 
				
			||||||
import { BalanceInterface } from 'src/plugins/balance/store/balance';
 | 
					
 | 
				
			||||||
 | 
					import { computed, ref, defineComponent, onBeforeMount } from '@vue/composition-api';
 | 
				
			||||||
 | 
					import { hasPermission } from 'src/utils/permission';
 | 
				
			||||||
 | 
					import { StateInterfaceBalance } from '../store/balance';
 | 
				
			||||||
import { Store } from 'vuex';
 | 
					import { Store } from 'vuex';
 | 
				
			||||||
 | 
					import Transaction from '../components/Transaction.vue';
 | 
				
			||||||
 | 
					import BalanceHeader from '../components/BalanceHeader.vue';
 | 
				
			||||||
 | 
					import PERMISSIONS from '../permissions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					export default defineComponent({
 | 
				
			||||||
  // name: 'PageName'
 | 
					  name: 'BalanceAdd',
 | 
				
			||||||
 | 
					  components: { Transaction, BalanceHeader },
 | 
				
			||||||
  setup(_, { root }) {
 | 
					  setup(_, { root }) {
 | 
				
			||||||
    onBeforeMount(() => {
 | 
					    onBeforeMount(() => void store.dispatch('balance/getShortcuts'));
 | 
				
			||||||
      store.dispatch('balance/getBalance').catch(err => {
 | 
					    const store = <Store<StateInterfaceBalance>>root.$store;
 | 
				
			||||||
        console.warn(err);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const store: Store<{ balance: BalanceInterface }> = <
 | 
					    const amount = ref<number>(0);
 | 
				
			||||||
      Store<{ balance: BalanceInterface }>
 | 
					    const showAddShortcut = ref(false);
 | 
				
			||||||
    >root.$store;
 | 
					    const transactions = ref<FG.Transaction[]>([]);
 | 
				
			||||||
 | 
					    const user = ref(store.state.user.currentUser);
 | 
				
			||||||
 | 
					    const shortCuts = ref(store.state.balance.shortcuts);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const balance = computed<number>(() => {
 | 
					    const showSelector = computed(
 | 
				
			||||||
      return store.state.balance.balance;
 | 
					      () => hasPermission(PERMISSIONS.DEBIT, store) || hasPermission(PERMISSIONS.CREDIT, store)
 | 
				
			||||||
    });
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function addShortcut() {
 | 
				
			||||||
 | 
					      // TODO: Save shortcuts on backend
 | 
				
			||||||
 | 
					      showAddShortcut.value = false;
 | 
				
			||||||
 | 
					      console.log(amount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    function userUpdated(selectedUser: FG.User) {
 | 
				
			||||||
 | 
					      user.value = selectedUser;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    function changeBalance(amount: number) {
 | 
					    function changeBalance(amount: number) {
 | 
				
			||||||
      store
 | 
					      store
 | 
				
			||||||
        .dispatch('balance/changeBalance', amount)
 | 
					        .dispatch('balance/changeBalance', { amount: amount, user: user.value?.userid })
 | 
				
			||||||
 | 
					        .then((transaction: FG.Transaction) => {
 | 
				
			||||||
 | 
					          if (transactions.value.length > 5) transactions.value.pop();
 | 
				
			||||||
 | 
					          transaction.time = new Date(transaction.time);
 | 
				
			||||||
 | 
					          transactions.value.unshift(transaction);
 | 
				
			||||||
 | 
					          console.log(transactions.value);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .catch(err => console.log(err));
 | 
					        .catch(err => console.log(err));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return { balance, changeBalance };
 | 
					    function reversed(id: number) {
 | 
				
			||||||
 | 
					      transactions.value = transactions.value.filter(t => t.id != id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      user,
 | 
				
			||||||
 | 
					      addShortcut,
 | 
				
			||||||
 | 
					      showAddShortcut,
 | 
				
			||||||
 | 
					      changeBalance,
 | 
				
			||||||
 | 
					      transactions,
 | 
				
			||||||
 | 
					      amount,
 | 
				
			||||||
 | 
					      showSelector,
 | 
				
			||||||
 | 
					      shortCuts,
 | 
				
			||||||
 | 
					      reversed,
 | 
				
			||||||
 | 
					      userUpdated
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    <q-page padding>
 | 
				
			||||||
 | 
					      <q-card>
 | 
				
			||||||
 | 
					        <q-card-section>
 | 
				
			||||||
 | 
					          <q-table :data="rows" row-key="userid" :columns="columns" />
 | 
				
			||||||
 | 
					        </q-card-section>
 | 
				
			||||||
 | 
					      </q-card>
 | 
				
			||||||
 | 
					    </q-page>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					// TODO: Load all users / balances
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: Fill usefull data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { computed, defineComponent } from '@vue/composition-api';
 | 
				
			||||||
 | 
					import { StateInterfaceBalance } from '../store/balance';
 | 
				
			||||||
 | 
					import { Store } from 'vuex';
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
					  // name: 'PageName'
 | 
				
			||||||
 | 
					  setup(_, { root }) {
 | 
				
			||||||
 | 
					    const store = <Store<StateInterfaceBalance>>root.$store;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rows = computed(() => {
 | 
				
			||||||
 | 
					      const fo: Array<{ userid: string; balance: number }> = [];
 | 
				
			||||||
 | 
					      store.state.balance.balances.forEach((value, key) =>
 | 
				
			||||||
 | 
					        fo.push(Object.assign(value, { userid: key }))
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      return fo;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const columns = [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name: 'userid',
 | 
				
			||||||
 | 
					        label: 'Benutzer ID',
 | 
				
			||||||
 | 
					        field: 'userid',
 | 
				
			||||||
 | 
					        required: true,
 | 
				
			||||||
 | 
					        align: 'left',
 | 
				
			||||||
 | 
					        sortable: true
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      { name: 'balance', label: 'Kontostand', field: 'balance' },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name: 'limit',
 | 
				
			||||||
 | 
					        label: 'Limit',
 | 
				
			||||||
 | 
					        field: 'limit',
 | 
				
			||||||
 | 
					        format: (val: number) => (val === null ? 'keins' : val)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    return { rows, columns };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,111 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <q-page padding class="fit row justify-left q-col-gutter-sm">
 | 
				
			||||||
 | 
					    <div class="col-12">
 | 
				
			||||||
 | 
					      <q-card>
 | 
				
			||||||
 | 
					        <BalanceHeader @update:user="senderUpdated" :showSelector="showSelector" />
 | 
				
			||||||
 | 
					        <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 :user="receiver" @update:user="receiverUpdated" 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>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div v-for="(transaction, index) in transactions" v-bind:key="index" class="col-sm-4 col-xs-6">
 | 
				
			||||||
 | 
					      <Transaction :transaction="transaction" @reversed="reversed" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </q-page>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import { computed, ref, defineComponent } from '@vue/composition-api';
 | 
				
			||||||
 | 
					import { hasPermission } from 'src/utils/permission';
 | 
				
			||||||
 | 
					import { StateInterfaceBalance } from '../store/balance';
 | 
				
			||||||
 | 
					import { Store } from 'vuex';
 | 
				
			||||||
 | 
					import UserSelector from 'src/plugins/user/components/UserSelector.vue';
 | 
				
			||||||
 | 
					import Transaction from '../components/Transaction.vue';
 | 
				
			||||||
 | 
					import BalanceHeader from '../components/BalanceHeader.vue';
 | 
				
			||||||
 | 
					import PERMISSIONS from '../permissions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineComponent({
 | 
				
			||||||
 | 
					  name: 'BalanceTransfer',
 | 
				
			||||||
 | 
					  components: { Transaction, BalanceHeader, UserSelector },
 | 
				
			||||||
 | 
					  setup(_, { root }) {
 | 
				
			||||||
 | 
					    const store: Store<StateInterfaceBalance> = <Store<StateInterfaceBalance>>root.$store;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const showSelector = computed(() => hasPermission(PERMISSIONS.SEND_OTHER, store));
 | 
				
			||||||
 | 
					    const sender = ref(store.state.user.currentUser);
 | 
				
			||||||
 | 
					    const receiver = ref<FG.User | undefined>(undefined);
 | 
				
			||||||
 | 
					    const amount = ref<number>(0);
 | 
				
			||||||
 | 
					    const transactions = ref<FG.Transaction[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sendDisabled = computed(() => {
 | 
				
			||||||
 | 
					      return !(
 | 
				
			||||||
 | 
					        receiver.value &&
 | 
				
			||||||
 | 
					        sender.value &&
 | 
				
			||||||
 | 
					        sender.value.userid != receiver.value.userid &&
 | 
				
			||||||
 | 
					        amount.value > 0
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function senderUpdated(selectedUser: FG.User) {
 | 
				
			||||||
 | 
					      console.log(selectedUser);
 | 
				
			||||||
 | 
					      sender.value = selectedUser;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    function receiverUpdated(selectedUser: FG.User) {
 | 
				
			||||||
 | 
					      receiver.value = selectedUser;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function reversed(id: number) {
 | 
				
			||||||
 | 
					      transactions.value = transactions.value.filter(value => value.id != id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function sendAmount() {
 | 
				
			||||||
 | 
					      store
 | 
				
			||||||
 | 
					        .dispatch('balance/changeBalance', {
 | 
				
			||||||
 | 
					          amount: amount.value,
 | 
				
			||||||
 | 
					          sender: sender.value?.userid,
 | 
				
			||||||
 | 
					          user: receiver.value?.userid
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then((transaction: FG.Transaction) => {
 | 
				
			||||||
 | 
					          if (transactions.value.length > 5) transactions.value.pop();
 | 
				
			||||||
 | 
					          transaction.time = new Date(transaction.time);
 | 
				
			||||||
 | 
					          transactions.value.unshift(transaction);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(err => console.log(err));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      sender,
 | 
				
			||||||
 | 
					      receiver,
 | 
				
			||||||
 | 
					      amount,
 | 
				
			||||||
 | 
					      sendAmount,
 | 
				
			||||||
 | 
					      transactions,
 | 
				
			||||||
 | 
					      showSelector,
 | 
				
			||||||
 | 
					      senderUpdated,
 | 
				
			||||||
 | 
					      receiverUpdated,
 | 
				
			||||||
 | 
					      sendDisabled,
 | 
				
			||||||
 | 
					      reversed
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</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;
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,12 @@
 | 
				
			||||||
import { Module } from 'vuex';
 | 
					import { Module } from 'vuex';
 | 
				
			||||||
import { StateInterface } from 'src/store';
 | 
					import { StateInterface } from 'src/store';
 | 
				
			||||||
import mainRoutes from './routes';
 | 
					import routes from './routes';
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from 'src/plugins';
 | 
				
			||||||
import balance, { BalanceInterface } from './store/balance';
 | 
					import balance, { BalanceInterface } from './store/balance';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const plugin: FG_Plugin.Plugin = {
 | 
					const plugin: FG_Plugin.Plugin = {
 | 
				
			||||||
  name: 'Balance',
 | 
					  name: 'Balance',
 | 
				
			||||||
  mainRoutes,
 | 
					  mainRoutes: routes,
 | 
				
			||||||
  requiredModules: ['User'],
 | 
					  requiredModules: ['User'],
 | 
				
			||||||
  requiredBackendModules: ['balance'],
 | 
					  requiredBackendModules: ['balance'],
 | 
				
			||||||
  version: '0.0.1',
 | 
					  version: '0.0.1',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,24 +1,5 @@
 | 
				
			||||||
import { FG_Plugin } from 'src/plugins';
 | 
					import { FG_Plugin } from 'src/plugins';
 | 
				
			||||||
 | 
					import permissions from '../permissions';
 | 
				
			||||||
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'
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
 | 
					const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
| 
						 | 
					@ -26,17 +7,33 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
 | 
				
			||||||
    icon: 'mdi-cash-100',
 | 
					    icon: 'mdi-cash-100',
 | 
				
			||||||
    path: 'balance',
 | 
					    path: 'balance',
 | 
				
			||||||
    name: 'balance',
 | 
					    name: 'balance',
 | 
				
			||||||
    component: () => import('../pages/MainPage.vue'),
 | 
					    redirect: { name: 'balance-add' },
 | 
				
			||||||
    meta: { permissions: ['user'] },
 | 
					    meta: { permissions: ['user'] },
 | 
				
			||||||
    children: [
 | 
					    children: [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        title: 'Anschreiben',
 | 
					        title: 'Anschreiben',
 | 
				
			||||||
        icon: 'mdi-cash-100',
 | 
					        icon: 'mdi-cash-plus',
 | 
				
			||||||
        path: 'balance-add',
 | 
					        path: 'add',
 | 
				
			||||||
        name: 'balance-add',
 | 
					        name: 'balance-add',
 | 
				
			||||||
        shortcut: true,
 | 
					        shortcut: true,
 | 
				
			||||||
        meta: { permissions: [permissions.DEBIT_OWN, permissions.SHOW] },
 | 
					        meta: { permissions: [permissions.DEBIT_OWN, permissions.SHOW] },
 | 
				
			||||||
        component: () => import('../pages/Add.vue')
 | 
					        component: () => import('../pages/Add.vue')
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        title: 'Übertragen',
 | 
				
			||||||
 | 
					        icon: 'mdi-cash-refund',
 | 
				
			||||||
 | 
					        path: 'transfer',
 | 
				
			||||||
 | 
					        name: 'balance-transfer',
 | 
				
			||||||
 | 
					        meta: { permissions: [permissions.SEND] },
 | 
				
			||||||
 | 
					        component: () => import('../pages/Transfer.vue')
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        title: 'Verwaltung',
 | 
				
			||||||
 | 
					        icon: 'mdi-account-cash',
 | 
				
			||||||
 | 
					        path: 'admin',
 | 
				
			||||||
 | 
					        name: 'balance-admin',
 | 
				
			||||||
 | 
					        meta: { permissions: [permissions.DEBIT_OWN, permissions.SHOW] },
 | 
				
			||||||
 | 
					        component: () => import('../pages/Admin.vue')
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,51 +9,75 @@ interface BalanceResponse {
 | 
				
			||||||
  debit: number;
 | 
					  debit: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface BalanceInterface extends BalanceResponse {
 | 
					export interface UserBalance extends BalanceResponse {
 | 
				
			||||||
  limit: number;
 | 
					  limit: number | null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface BalanceInterface {
 | 
				
			||||||
 | 
					  balances: Map<string, UserBalance>;
 | 
				
			||||||
 | 
					  shortcuts: Array<number>;
 | 
				
			||||||
  loading: number;
 | 
					  loading: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface StateInterfaceBalance extends StateInterface {
 | 
				
			||||||
 | 
					  balance: BalanceInterface;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const state: BalanceInterface = {
 | 
					const state: BalanceInterface = {
 | 
				
			||||||
  balance: 0,
 | 
					  balances: new Map<string, UserBalance>(),
 | 
				
			||||||
  credit: 0,
 | 
					  shortcuts: [],
 | 
				
			||||||
  debit: 0,
 | 
					 | 
				
			||||||
  limit: 0,
 | 
					 | 
				
			||||||
  loading: 0
 | 
					  loading: 0
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mutations: MutationTree<BalanceInterface> = {
 | 
					const mutations: MutationTree<BalanceInterface> = {
 | 
				
			||||||
  setBalance(state, data: number) {
 | 
					  setBalance(state, data: { userid: string; balance: BalanceResponse }) {
 | 
				
			||||||
    state.balance = data;
 | 
					    state.balances.set(
 | 
				
			||||||
 | 
					      data.userid,
 | 
				
			||||||
 | 
					      Object.assign({ limit: state.balances.get(data.userid)?.limit || null }, data.balance)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setCredit(state, data: number) {
 | 
					  changeBalance(state, data: { userid: string; amount: number }) {
 | 
				
			||||||
    state.credit = data;
 | 
					    const user = <UserBalance>state.balances.get(data.userid);
 | 
				
			||||||
 | 
					    if (data.amount < 0) user.debit += data.amount;
 | 
				
			||||||
 | 
					    else user.credit += data.amount;
 | 
				
			||||||
 | 
					    user.balance += data.amount;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setDebit(state, data: number) {
 | 
					  setLimit(state, data: { userid: string; limit: number | null }) {
 | 
				
			||||||
    state.debit = data;
 | 
					    if (state.balances.has(data.userid))
 | 
				
			||||||
  },
 | 
					      (<UserBalance>state.balances.get(data.userid)).limit = data.limit;
 | 
				
			||||||
  setLimit(state, data: number) {
 | 
					    else state.balances.set(data.userid, { balance: 0, debit: 0, credit: 0, limit: data.limit });
 | 
				
			||||||
    state.limit = data;
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setLoading(state, data = true) {
 | 
					  setLoading(state, data = true) {
 | 
				
			||||||
    if (data) state.loading += 1;
 | 
					    if (data) state.loading += 1;
 | 
				
			||||||
    else state.loading -= 1;
 | 
					    else state.loading -= 1;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  setShortcuts(state, data: Array<number>) {
 | 
				
			||||||
 | 
					    state.shortcuts = data.sort().reverse();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const actions: ActionTree<BalanceInterface, StateInterface> = {
 | 
					const actions: ActionTree<BalanceInterface, StateInterface> = {
 | 
				
			||||||
  getBalance({ commit, rootState }) {
 | 
					  getShortcuts({ commit, state, rootState }, force = false) {
 | 
				
			||||||
 | 
					    if (force || state.shortcuts.length == 0) {
 | 
				
			||||||
 | 
					      commit('setLoading');
 | 
				
			||||||
 | 
					      const user = <FG.User>rootState.user.currentUser;
 | 
				
			||||||
 | 
					      return axios
 | 
				
			||||||
 | 
					        .get(`/users/${user.userid}/balance/shortcuts`)
 | 
				
			||||||
 | 
					        .then(({ data }: AxiosResponse<BalanceResponse>) => {
 | 
				
			||||||
 | 
					          commit('setShortcuts', data);
 | 
				
			||||||
 | 
					          return data;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .finally(() => commit('setLoading', false));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  getBalance({ commit, rootState }, user: FG.User | undefined = undefined) {
 | 
				
			||||||
    commit('setLoading');
 | 
					    commit('setLoading');
 | 
				
			||||||
    axios
 | 
					    if (!user) user = <FG.User>rootState.user.currentUser;
 | 
				
			||||||
      /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
 | 
					    return axios
 | 
				
			||||||
      .get(`/users/${rootState.user.currentUser?.userid}/balance`)
 | 
					      .get(`/users/${user.userid}/balance`)
 | 
				
			||||||
      .then(({ data }: AxiosResponse<BalanceResponse>) => {
 | 
					      .then(({ data }: AxiosResponse<BalanceResponse>) => {
 | 
				
			||||||
        commit('setBalance', data.balance);
 | 
					        commit('setBalance', { userid: user?.userid, balance: data });
 | 
				
			||||||
        commit('setCredit', data.credit);
 | 
					        return data;
 | 
				
			||||||
        commit('setDebit', data.debit);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      .catch(err => {
 | 
					 | 
				
			||||||
        console.warn(err);
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .finally(() => commit('setLoading', false));
 | 
					      .finally(() => commit('setLoading', false));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -70,19 +94,32 @@ const actions: ActionTree<BalanceInterface, StateInterface> = {
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .finally(() => commit('setLoading', false));
 | 
					      .finally(() => commit('setLoading', false));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  changeBalance({ rootState, dispatch, commit }, amount: number) {
 | 
					  revert({ dispatch }, transaction: FG.Transaction) {
 | 
				
			||||||
 | 
					    return axios.delete(`/balance/${transaction.id}`).then(() => {
 | 
				
			||||||
 | 
					      dispatch('getBalance').catch(err => console.warn(err));
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  changeBalance({ dispatch, commit }, data: { amount: number; user: string; sender?: string }) {
 | 
				
			||||||
    commit('setLoading');
 | 
					    commit('setLoading');
 | 
				
			||||||
    axios
 | 
					    return axios
 | 
				
			||||||
      /* eslint-disable-next-line @typescript-eslint/restrict-template-expressions */
 | 
					      .put(`/users/${data.user}/balance`, data)
 | 
				
			||||||
      .put(`/users/${rootState.user.currentUser?.userid}/balance`, <
 | 
					      .then((response: AxiosResponse<FG.Transaction>) => {
 | 
				
			||||||
        { amount: number }
 | 
					        commit(state.balances.has(data.user) ? 'changeBalance' : 'setBalance', {
 | 
				
			||||||
      >{
 | 
					          userid: data.user,
 | 
				
			||||||
        amount: amount
 | 
					          amount: data.amount
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (data.sender)
 | 
				
			||||||
 | 
					          commit(state.balances.has(data.sender) ? 'changeBalance' : 'setBalance', {
 | 
				
			||||||
 | 
					            userid: data.sender,
 | 
				
			||||||
 | 
					            amount: -1 * data.amount
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        return response.data;
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .then(() => {
 | 
					      .catch(err => {
 | 
				
			||||||
 | 
					        // Maybe Balance changed
 | 
				
			||||||
        dispatch('getBalance').catch(err => console.warn(err));
 | 
					        dispatch('getBalance').catch(err => console.warn(err));
 | 
				
			||||||
 | 
					        console.warn(err);
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .catch(err => console.warn(err))
 | 
					 | 
				
			||||||
      .finally(() => commit('setLoading', false));
 | 
					      .finally(() => commit('setLoading', false));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue