[pinia] Implemented and migrated balance

* Fixed revert of transaction if Conflic occures
This commit is contained in:
Ferdinand Thiessen 2021-02-04 02:44:20 +01:00
parent 4a7ed50281
commit fd45a46c01
11 changed files with 294 additions and 254 deletions

View File

@ -1,10 +1,6 @@
<template>
<q-card>
<BalanceHeader
:show-selector="showSelector"
@update:user="userUpdated"
@open-history="openHistory"
/>
<BalanceHeader v-model="user" :show-selector="showSelector" @open-history="openHistory" />
<q-separator />
<q-card-section v-if="shortCuts" class="row q-col-gutter-md">
@ -63,53 +59,39 @@
<script lang="ts">
import { computed, ref, defineComponent, onBeforeMount } from 'vue';
import { hasPermission } from 'src/utils/permission';
import { Store, useStore, mapGetters } from 'vuex';
import BalanceHeader from '../components/BalanceHeader.vue';
import PERMISSIONS from '../permissions';
import { StateInterface } from 'src/store';
import { useBalanceStore } from '../store';
import { useMainStore } from 'src/store';
export default defineComponent({
name: 'BalanceAdd',
components: { BalanceHeader },
emits: { 'open-history': () => true },
setup(_, { emit }) {
const store = useBalanceStore();
const mainStore = useMainStore();
onBeforeMount(() => {
void store.dispatch('balance/getShortcuts');
if ((<FG.Transaction[]>balanceGatters.transactions())?.length == 0)
// No transaction, load at most six since yesterday
void store.dispatch('balance/getTransactions', {
filter: { limit: 6, from: new Date(new Date().setDate(new Date().getDate() - 1)) },
void store.getShortcuts();
});
});
//const store = <Store<StateInterfaceBalance>>root.$store;
const store = useStore<Store<StateInterface>>();
const userGetters = mapGetters('users', ['currentUser', 'roles', 'users']);
const balanceGatters = mapGetters('balance', ['balances', 'transactions', 'shortcuts']);
const amount = ref<number>(0);
const showAddShortcut = ref(false);
const user = ref(<FG.User>userGetters.currentUser());
//const shortCuts = ref(balanceState.shortcuts);
const shortCuts = computed(() => <number[]>balanceGatters.shortcuts());
const user = ref(mainStore.currentUser);
const shortCuts = computed(() => store.shortcuts);
const canAddCredit = computed(() => hasPermission(PERMISSIONS.CREDIT));
const showSelector = computed(
() => hasPermission(PERMISSIONS.DEBIT) || hasPermission(PERMISSIONS.CREDIT)
);
const canAddCredit = hasPermission(PERMISSIONS.CREDIT);
const showSelector = hasPermission(PERMISSIONS.DEBIT) || hasPermission(PERMISSIONS.CREDIT);
function addShortcut() {
if (amount.value != 0) void store.dispatch('balance/addShortcut', amount.value * -1);
if (amount.value != 0) void store.createShortcut(amount.value * -1);
}
function removeShortcut(shortcut: number) {
void store.dispatch('balance/removeShortcut', shortcut);
void store.removeShortcut(shortcut);
}
function userUpdated(selectedUser: FG.User) {
user.value = selectedUser;
}
function changeBalance(amount: number) {
store
.dispatch('balance/changeBalance', { amount: amount, user: user.value?.userid })
.catch((err) => console.log(err));
async function changeBalance(amount: number) {
await store.changeBalance(amount, user.value);
}
function openHistory() {
@ -126,7 +108,6 @@ export default defineComponent({
amount,
showSelector,
shortCuts,
userUpdated,
openHistory,
};
},

View File

@ -1,11 +1,14 @@
<template>
<q-card-section class="fit row justify-left content-center items-center q-col-gutter-sm">
<div class="text-h6 col-5">
<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 :user="user" @update:user="userUpdated" />
<UserSelector v-model="user" />
</div>
<div class="col-1 justify-end">
<q-btn round flat icon="mdi-format-list-checks" @click="openHistory" />
@ -14,46 +17,50 @@
</template>
<script lang="ts">
import { ref, computed, defineComponent, onBeforeMount } from 'vue';
import { computed, defineComponent, onBeforeMount, PropType } from 'vue';
import UserSelector from 'src/plugins/user/components/UserSelector.vue';
import { StateInterfaceBalance, UserBalance } from '../store/balance';
import { useStore, mapGetters } from 'vuex';
import { useBalanceStore } from '../store';
import { useMainStore } from 'src/store';
export default defineComponent({
name: 'BalanceHeader',
components: { UserSelector },
props: { showSelector: Boolean },
emits: { 'update:user': (u: FG.User) => !!u, 'open-history': () => true },
setup(_, { emit }) {
onBeforeMount(() => void store.dispatch('balance/getBalance'));
const store = useStore<StateInterfaceBalance>();
const userGetters = mapGetters('users', ['currentUser']);
const balanceGetters = mapGetters('balance', ['balances']);
const user = ref(<FG.User>userGetters.currentUser());
const balance = computed(() => {
return (
(<Map<string, UserBalance>>balanceGetters.balances()).get(user.value.userid) || {
balance: 0,
limit: null,
}
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.limit !== null && balance.value.balance >= balance.value.limit
() =>
balance.value === undefined ||
(balance.value.limit !== undefined && balance.value.balance <= balance.value.limit)
);
function userUpdated(selectedUser: FG.User) {
void store.dispatch('balance/getBalance', selectedUser);
user.value = selectedUser;
emit('update:user', selectedUser);
}
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, userUpdated, openHistory };
return { user, balance, isLocked, openHistory };
},
});
</script>

View File

@ -1,17 +1,13 @@
<template>
<q-card>
<BalanceHeader
:show-selector="showSelector"
@update:user="senderUpdated"
@open-history="openHistory"
/>
<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 :user="receiver" label="Empfänger" @update:user="receiverUpdated" />
<UserSelector v-model="receiver" label="Empfänger" />
</div>
<div class="col-sm-4 col-xs-6">
<q-btn
@ -29,22 +25,22 @@
<script lang="ts">
import { computed, ref, defineComponent } from 'vue';
import { hasPermission } from 'src/utils/permission';
import { mapGetters, Store, useStore } from 'vuex';
import UserSelector from 'src/plugins/user/components/UserSelector.vue';
import BalanceHeader from '../components/BalanceHeader.vue';
import PERMISSIONS from '../permissions';
import { StateInterface } from 'src/store';
import { useBalanceStore } from '../store';
import { useMainStore } from 'src/store';
export default defineComponent({
name: 'BalanceTransfer',
components: { BalanceHeader, UserSelector },
emits: { 'open-history': () => true },
setup(_, { emit }) {
const store = useStore<Store<StateInterface>>();
const userGetters = mapGetters('users', ['currentUser']);
const store = useBalanceStore();
const mainStore = useMainStore();
const showSelector = computed(() => hasPermission(PERMISSIONS.SEND_OTHER));
const sender = ref(<FG.User>userGetters.currentUser());
const sender = ref<FG.User | undefined>(mainStore.currentUser);
const receiver = ref<FG.User | undefined>(undefined);
const amount = ref<number>(0);
@ -57,22 +53,8 @@ export default defineComponent({
);
});
function senderUpdated(selectedUser: FG.User) {
console.log(selectedUser);
sender.value = selectedUser;
}
function receiverUpdated(selectedUser: FG.User) {
receiver.value = selectedUser;
}
function sendAmount() {
store
.dispatch('balance/changeBalance', {
amount: amount.value,
sender: sender.value?.userid,
user: receiver.value?.userid,
})
.catch((err) => console.log(err));
async function sendAmount() {
if (receiver.value) await store.changeBalance(amount.value, receiver.value, sender.value);
}
function openHistory() {
@ -85,8 +67,6 @@ export default defineComponent({
amount,
sendAmount,
showSelector,
senderUpdated,
receiverUpdated,
sendDisabled,
openHistory,
};

View File

@ -33,8 +33,9 @@
import { ref, computed, defineComponent, onUnmounted, onMounted, PropType } from 'vue';
import { hasPermission } from 'src/utils/permission';
import { formatDateTime } from 'src/utils/datetime';
import { mapGetters, Store, useStore } from 'vuex';
import { StateInterface } from 'src/store';
import { useMainStore } from 'src/store';
import { useUserStore } from 'src/plugins/user/store';
import { useBalanceStore } from '../store';
export default defineComponent({
name: 'Transaction',
@ -46,33 +47,29 @@ export default defineComponent({
},
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 store = useStore<Store<StateInterface>>();
const userGetters = mapGetters('users', ['currentUser']);
const text = ref('');
onUnmounted(() => clearInterval(ival));
onMounted(() => refreshText());
const isNegative = () =>
props.transaction.sender_id === (<FG.User>userGetters.currentUser())?.userid;
const isNegative = () => props.transaction.sender_id === mainStore.currentUser.userid;
const refreshText = async () => {
if (isNegative()) {
text.value = 'Anschreiben';
if (props.transaction.receiver_id !== null) {
const user = <FG.User>await store.dispatch('user/getUser', {
userid: props.transaction.receiver_id,
});
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 !== null) {
const user = <FG.User>await store.dispatch('user/getUser', {
userid: props.transaction.sender_id,
});
if (props.transaction.sender_id) {
const user = <FG.User>await userStore.getUser(props.transaction.sender_id);
text.value = `Bekommen von ${user.display_name}`;
}
}
@ -86,18 +83,15 @@ export default defineComponent({
() =>
!isReversed.value &&
(hasPermission('balance_reversal') ||
(props.transaction.sender_id === (<FG.User>userGetters.currentUser())?.userid &&
(props.transaction.sender_id === mainStore.currentUser.userid &&
now.value - props.transaction.time.getTime() < 10000))
);
function reverse() {
if (canReverse.value)
store
.dispatch('balance/revert', props.transaction)
.then(() => {
void balanceStore.revert(props.transaction).then(() => {
emit('update:transaction', props.transaction);
})
.catch((error) => console.log(error));
});
}
const timeStr = computed(() => {

View File

@ -7,30 +7,21 @@
</template>
<script lang="ts">
import { useMainStore } from 'src/store';
import { useBalanceStore } from '../store';
import { computed, defineComponent, onBeforeMount } from 'vue';
import { UserBalance } from 'src/plugins/balance/store/balance';
import { mapGetters, useStore } from 'vuex';
import { StateInterface } from 'src/store';
export default defineComponent({
name: 'BalanceWidget',
setup() {
const store = useStore<StateInterface>();
const balanceGetters = mapGetters('balance', ['balances']);
const userGetters = mapGetters('users', ['currentUser']);
const store = useBalanceStore();
onBeforeMount(() => {
store.dispatch('balance/getBalance').catch((err) => {
console.warn(err);
});
const mainStore = useMainStore();
void store.getBalance(mainStore.currentUser);
});
const balance = computed(
() =>
(<Map<string, UserBalance>>balanceGetters.balances()).get(
(<FG.User>userGetters.currentUser()).userid
)?.balance || NaN
);
const balance = computed(() => store.balance?.balance || NaN);
return { balance };
},

View File

@ -14,21 +14,16 @@
// TODO: Fill usefull data
import { ref, defineComponent, onMounted } from 'vue';
import { BalancesResponse } from '../store/balance';
import { useStore } from 'vuex';
import { useBalanceStore } from '../store';
export default defineComponent({
// name: 'PageName'
setup() {
const store = useStore();
const store = useBalanceStore();
onMounted(
() =>
void store
.dispatch('balance/getBalances')
.then((balances: Array<BalancesResponse>) => rows.value.push(...balances))
);
onMounted(() => void store.getBalances().then((balances) => rows.value.push(...balances)));
const rows = ref(new Array<BalancesResponse>());
const rows = ref(store.balances);
const columns = [
{

View File

@ -67,29 +67,30 @@
<script lang="ts">
import { computed, defineComponent, ref, onMounted } from 'vue';
import { mapGetters, Store, useStore } from 'vuex';
import { hasSomePermissions } from 'src/utils/permission';
import PERMISSIONS from '../permissions';
import BalanceAdd from '../components/BalanceAdd.vue';
import BalanceTransfer from '../components/BalanceTransfer.vue';
import Transaction from '../components/Transaction.vue';
import { StateInterface } from 'src/store';
import { useBalanceStore } from '../store';
import { useMainStore } from 'src/store';
export default defineComponent({
name: 'BalanceManage',
components: { BalanceAdd, BalanceTransfer, Transaction },
setup() {
//const store = <Store<StateInterfaceBalance>>root.$store;
const store = useStore<Store<StateInterface>>();
const balanceGetters = mapGetters('balance', ['balances', 'transactions']);
const balanceStore = useBalanceStore();
const mainStore = useMainStore();
const now = new Date();
onMounted(() => {
void store.dispatch('balance/getTransactions', {
filter: { from: new Date(now.getFullYear(), now.getMonth(), now.getDate()) },
void balanceStore.getTransactions(mainStore.currentUser, {
from: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
});
});
const transactions = computed(() => {
return (<FG.Transaction[]>balanceGetters.transactions())
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));

View File

@ -33,22 +33,21 @@
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue';
import { TransactionsResponse, UserBalance } from '../store/balance';
import { mapGetters, Store, useStore } from 'vuex';
import { formatDateTime } from 'src/utils/datetime';
import { StateInterface } from 'src/store';
import { useBalanceStore } from '../store';
import { useMainStore } from 'src/store';
import { useUserStore } from 'src/plugins/user/store';
export default defineComponent({
// name: 'PageName'
setup() {
//const store = <Store<StateInterfaceBalance>>root.$store;
const store = useStore<Store<StateInterface>>();
const userGetters = mapGetters('users', ['currentUser', 'users']);
const balanceGetters = mapGetters('balace', ['balances', 'transactions']);
const store = useBalanceStore();
const mainStore = useMainStore();
const userStore = useUserStore();
onMounted(() => {
void store.dispatch('balance/getBalance');
void store.dispatch('user/getUsers').then(() =>
void store.getBalance(mainStore.currentUser);
void userStore.getUsers().then(() =>
onRequest({
pagination: pagination.value,
filter: undefined,
@ -75,7 +74,7 @@ export default defineComponent({
rowsNumber: number;
}
function onRequest(props: { pagination: PaginationInterface; filter?: string }) {
async function onRequest(props: { pagination: PaginationInterface; filter?: string }) {
const { page, rowsPerPage, sortBy, descending } = props.pagination;
loading.value = true;
@ -83,16 +82,13 @@ export default defineComponent({
const fetchCount = rowsPerPage === 0 ? pagination.value.rowsNumber : rowsPerPage;
// calculate starting row of data
const startRow = (page - 1) * rowsPerPage;
store
.dispatch('balance/getTransactions', {
filter: {
try {
const result = await store.getTransactions(mainStore.currentUser, {
offset: startRow,
limit: fetchCount,
showCancelled: showCancelled.value,
showReversals: false,
},
})
.then((result: TransactionsResponse) => {
});
// clear out existing data and add new
data.value.splice(0, data.value.length, ...result.transactions);
// don't forget to update local pagination object
@ -101,16 +97,13 @@ export default defineComponent({
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
if (result.count) pagination.value.rowsNumber = result.count;
})
.finally(() => (loading.value = false));
} catch (error) {
// ...
}
loading.value = false;
}
const balance = computed(
() =>
(<Map<string, UserBalance>>balanceGetters.balances()).get(
(<FG.User>userGetters.currentUser()).userid
)?.balance || NaN
);
const balance = computed(() => store.balance?.balance || NaN);
const columns = [
{
@ -144,8 +137,7 @@ export default defineComponent({
else {
if (row.receiver_id == null) return 'Angeschrieben';
else {
if (row.receiver_id === (<FG.User>userGetters.currentUser())?.userid)
return 'Bekommen von X';
if (row.receiver_id === mainStore.currentUser.userid) return 'Bekommen von X';
else return 'Gesendet an X';
}
}
@ -162,7 +154,7 @@ export default defineComponent({
label: 'Benutzer',
field: 'author_id',
format: (val: string) => {
const user = (<FG.User[]>userGetters.users()).filter((x) => x.userid == val);
const user = userStore.users.filter((x) => x.userid == val);
if (user.length > 0) return user[0].display_name;
else return val;
},

View File

@ -0,0 +1,166 @@
import { api } from 'src/boot/axios';
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;
}
import { defineStore } from 'pinia';
import { useMainStore } from 'src/store';
import { AxiosResponse } from 'axios';
import { Notify } from 'quasar';
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() {
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;
},
},
});

View File

@ -1,66 +0,0 @@
import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
import { AxiosResponse } from 'axios';
import { useMainStore } from 'src/store';
interface BalanceResponse {
balance: number;
credit: number;
debit: 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 balanceStore = defineStore({
id: 'balance',
state: () => ({
balances: [] as BalancesResponse[],
shortcuts: [] as number[],
}),
getters: {
balance() {
const mainStore = useMainStore();
return this.balances.find((v) => v.userid === mainStore.user?.userid);
},
},
actions: {
async createShortcut(shortcut: number) {
const mainStore = useMainStore();
const sc = [...this.shortcuts, shortcut];
sc.sort();
await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, sc);
this.shortcuts = sc;
},
async removeShortcut(shortcut: number) {
const mainStore = useMainStore();
const sc = this.shortcuts.filter((value: number) => value !== shortcut);
await api.put(`/users/${mainStore.currentUser.userid}/balance/shortcuts`, sc);
this.shortcuts = sc;
},
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;
}
},
},
});

View File

@ -33,7 +33,6 @@ export const useUserStore = defineStore({
} catch (error) {
if (!error || !('response' in error) || (<AxiosError>error).response?.status !== 404)
throw error;
return null;
}
} else {
return this.users[idx];