diff --git a/admin/src/graphql/communityStatistics.js b/admin/src/graphql/communityStatistics.js index 868bfd02a..3159ee258 100644 --- a/admin/src/graphql/communityStatistics.js +++ b/admin/src/graphql/communityStatistics.js @@ -4,12 +4,14 @@ export const communityStatistics = gql` query { communityStatistics { totalUsers - activeUsers deletedUsers totalGradidoCreated totalGradidoDecayed - totalGradidoAvailable - totalGradidoUnbookedDecayed + dynamicStatisticsFields { + activeUsers + totalGradidoAvailable + totalGradidoUnbookedDecayed + } } } ` diff --git a/admin/src/pages/CommunityStatistic.spec.js b/admin/src/pages/CommunityStatistic.spec.js index 50e04d11f..528548f02 100644 --- a/admin/src/pages/CommunityStatistic.spec.js +++ b/admin/src/pages/CommunityStatistic.spec.js @@ -17,12 +17,14 @@ const defaultData = () => { return { communityStatistics: { totalUsers: 3113, - activeUsers: 1057, deletedUsers: 35, totalGradidoCreated: '4083774.05000000000000000000', totalGradidoDecayed: '-1062639.13634129622923372197', - totalGradidoAvailable: '2513565.869444365732411569', - totalGradidoUnbookedDecayed: '-500474.6738366222166261272', + dynamicStatisticsFields: { + activeUsers: 1057, + totalGradidoAvailable: '2513565.869444365732411569', + totalGradidoUnbookedDecayed: '-500474.6738366222166261272', + }, }, } } diff --git a/admin/src/pages/CommunityStatistic.vue b/admin/src/pages/CommunityStatistic.vue index 3b4865ee3..e656fd2c8 100644 --- a/admin/src/pages/CommunityStatistic.vue +++ b/admin/src/pages/CommunityStatistic.vue @@ -31,7 +31,9 @@ export default { return communityStatistics }, update({ communityStatistics }) { - this.statistics = communityStatistics + const totals = { ...communityStatistics.dynamicStatisticsFields } + this.statistics = { ...communityStatistics, ...totals } + delete this.statistics.dynamicStatisticsFields }, error({ message }) { this.toastError(message) diff --git a/backend/src/graphql/model/CommunityStatistics.ts b/backend/src/graphql/model/CommunityStatistics.ts index 61354115c..87ef595af 100644 --- a/backend/src/graphql/model/CommunityStatistics.ts +++ b/backend/src/graphql/model/CommunityStatistics.ts @@ -2,13 +2,25 @@ import { ObjectType, Field } from 'type-graphql' import Decimal from 'decimal.js-light' @ObjectType() -export class CommunityStatistics { - @Field(() => Number) - totalUsers: number - +export class DynamicStatisticsFields { @Field(() => Number) activeUsers: number + @Field(() => Decimal) + totalGradidoAvailable: Decimal + + @Field(() => Decimal) + totalGradidoUnbookedDecayed: Decimal +} + +@ObjectType() +export class CommunityStatistics { + @Field(() => Number) + allUsers: number + + @Field(() => Number) + totalUsers: number + @Field(() => Number) deletedUsers: number @@ -18,9 +30,7 @@ export class CommunityStatistics { @Field(() => Decimal) totalGradidoDecayed: Decimal - @Field(() => Decimal) - totalGradidoAvailable: Decimal - - @Field(() => Decimal) - totalGradidoUnbookedDecayed: Decimal + // be carefull querying this, takes longer than 2 secs. + @Field(() => DynamicStatisticsFields) + dynamicStatisticsFields: DynamicStatisticsFields } diff --git a/backend/src/graphql/resolver/StatisticsResolver.ts b/backend/src/graphql/resolver/StatisticsResolver.ts index f6c2b9e22..e91840f10 100644 --- a/backend/src/graphql/resolver/StatisticsResolver.ts +++ b/backend/src/graphql/resolver/StatisticsResolver.ts @@ -1,81 +1,113 @@ import Decimal from 'decimal.js-light' -import { Resolver, Query, Authorized } from 'type-graphql' +import { Resolver, Query, Authorized, FieldResolver } from 'type-graphql' import { getConnection } from '@dbTools/typeorm' import { Transaction as DbTransaction } from '@entity/Transaction' import { User as DbUser } from '@entity/User' -import { CommunityStatistics } from '@model/CommunityStatistics' +import { CommunityStatistics, DynamicStatisticsFields } from '@model/CommunityStatistics' import { RIGHTS } from '@/auth/RIGHTS' import { calculateDecay } from '@/util/decay' -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - -@Resolver() +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +@Resolver((of) => CommunityStatistics) export class StatisticsResolver { @Authorized([RIGHTS.COMMUNITY_STATISTICS]) @Query(() => CommunityStatistics) async communityStatistics(): Promise { - const allUsers = await DbUser.count({ withDeleted: true }) - const totalUsers = await DbUser.count() - const deletedUsers = allUsers - totalUsers + return new CommunityStatistics() + } + @FieldResolver(() => Decimal) + async allUsers(): Promise { + return await DbUser.count({ withDeleted: true }) + } + + @FieldResolver() + async totalUsers(): Promise { + return await DbUser.count() + } + + @FieldResolver() + async deletedUsers(): Promise { + return (await this.allUsers()) - (await this.totalUsers()) + } + + @FieldResolver() + async totalGradidoCreated(): Promise { + const queryRunner = getConnection().createQueryRunner() + try { + await queryRunner.connect() + const { totalGradidoCreated } = await queryRunner.manager + .createQueryBuilder() + .select('SUM(transaction.amount) AS totalGradidoCreated') + .from(DbTransaction, 'transaction') + .where('transaction.typeId = 1') + .getRawOne() + return totalGradidoCreated + } finally { + await queryRunner.release() + } + } + + @FieldResolver() + async totalGradidoDecayed(): Promise { + const queryRunner = getConnection().createQueryRunner() + try { + await queryRunner.connect() + const { totalGradidoDecayed } = await queryRunner.manager + .createQueryBuilder() + .select('SUM(transaction.decay) AS totalGradidoDecayed') + .from(DbTransaction, 'transaction') + .where('transaction.decay IS NOT NULL') + .getRawOne() + return totalGradidoDecayed + } finally { + await queryRunner.release() + } + } + + @FieldResolver() + async dynamicStatisticsFields(): Promise { let totalGradidoAvailable: Decimal = new Decimal(0) let totalGradidoUnbookedDecayed: Decimal = new Decimal(0) const receivedCallDate = new Date() const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() + try { + await queryRunner.connect() - const lastUserTransactions = await queryRunner.manager - .createQueryBuilder(DbUser, 'user') - .select('transaction.balance', 'balance') - .addSelect('transaction.balance_date', 'balanceDate') - .innerJoin(DbTransaction, 'transaction', 'user.id = transaction.user_id') - .where( - `transaction.balance_date = (SELECT MAX(t.balance_date) FROM transactions AS t WHERE t.user_id = user.id)`, - ) - .orderBy('transaction.balance_date', 'DESC') - .addOrderBy('transaction.id', 'DESC') - .getRawMany() + const lastUserTransactions = await queryRunner.manager + .createQueryBuilder(DbUser, 'user') + .select('transaction.balance', 'balance') + .addSelect('transaction.balance_date', 'balanceDate') + .innerJoin(DbTransaction, 'transaction', 'user.id = transaction.user_id') + .where( + `transaction.balance_date = (SELECT MAX(t.balance_date) FROM transactions AS t WHERE t.user_id = user.id)`, + ) + .orderBy('transaction.balance_date', 'DESC') + .addOrderBy('transaction.id', 'DESC') + .getRawMany() - const activeUsers = lastUserTransactions.length + const activeUsers = lastUserTransactions.length - lastUserTransactions.forEach(({ balance, balanceDate }) => { - const decay = calculateDecay(new Decimal(balance), new Date(balanceDate), receivedCallDate) - if (decay) { - totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString()) - totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString()) + lastUserTransactions.forEach(({ balance, balanceDate }) => { + const decay = calculateDecay(new Decimal(balance), new Date(balanceDate), receivedCallDate) + if (decay) { + totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString()) + totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString()) + } + }) + + return { + activeUsers, + totalGradidoAvailable, + totalGradidoUnbookedDecayed, } - }) - - const { totalGradidoCreated } = await queryRunner.manager - .createQueryBuilder() - .select('SUM(transaction.amount) AS totalGradidoCreated') - .from(DbTransaction, 'transaction') - .where('transaction.typeId = 1') - .getRawOne() - - const { totalGradidoDecayed } = await queryRunner.manager - .createQueryBuilder() - .select('SUM(transaction.decay) AS totalGradidoDecayed') - .from(DbTransaction, 'transaction') - .where('transaction.decay IS NOT NULL') - .getRawOne() - - await queryRunner.release() - - return { - totalUsers, - activeUsers, - deletedUsers, - totalGradidoCreated, - totalGradidoDecayed, - totalGradidoAvailable, - totalGradidoUnbookedDecayed, + } finally { + await queryRunner.release() } } } diff --git a/frontend/src/components/Contributions/ContributionForm.spec.js b/frontend/src/components/Contributions/ContributionForm.spec.js index bc7ed9785..020e3f552 100644 --- a/frontend/src/components/Contributions/ContributionForm.spec.js +++ b/frontend/src/components/Contributions/ContributionForm.spec.js @@ -23,6 +23,7 @@ describe('ContributionForm', () => { const mocks = { $t: jest.fn((t) => t), $d: jest.fn((d) => d), + $n: jest.fn((n) => n), $store: { state: { creation: ['1000', '1000', '1000'], diff --git a/frontend/src/components/Inputs/InputAmount.spec.js b/frontend/src/components/Inputs/InputAmount.spec.js index b2d524e08..0b03ce769 100644 --- a/frontend/src/components/Inputs/InputAmount.spec.js +++ b/frontend/src/components/Inputs/InputAmount.spec.js @@ -9,10 +9,10 @@ describe('InputAmount', () => { const mocks = { $t: jest.fn((t) => t), + $n: jest.fn((n) => n), $i18n: { locale: jest.fn(() => 'en'), }, - $n: jest.fn((n) => String(n)), $route: { params: {}, }, @@ -46,13 +46,14 @@ describe('InputAmount', () => { describe('amount normalization', () => { describe('if invalid', () => { - beforeEach(() => { + beforeEach(async () => { + await wrapper.setProps({ value: '12m34' }) valid = false }) it('is not normalized', () => { - wrapper.vm.normalizeAmount(valid) - expect(wrapper.vm.amountValue).toBe(0.0) + wrapper.vm.normalizeAmount(false) + expect(wrapper.vm.currentValue).toBe('12m34') }) }) @@ -97,13 +98,14 @@ describe('InputAmount', () => { describe('amount normalization', () => { describe('if invalid', () => { - beforeEach(() => { + beforeEach(async () => { + await wrapper.setProps({ value: '12m34' }) valid = false }) it('is not normalized', () => { wrapper.vm.normalizeAmount(valid) - expect(wrapper.vm.amountValue).toBe(0.0) + expect(wrapper.vm.currentValue).toBe('12m34') }) }) diff --git a/frontend/src/components/Inputs/InputAmount.vue b/frontend/src/components/Inputs/InputAmount.vue index 7a72a3a77..3268a0ec1 100644 --- a/frontend/src/components/Inputs/InputAmount.vue +++ b/frontend/src/components/Inputs/InputAmount.vue @@ -20,7 +20,7 @@ trim v-focus="amountFocused" @focus="amountFocused = true" - @blur="normalizeAmount(true)" + @blur="normalizeAmount(valid)" :disabled="disabled" autocomplete="off" > @@ -90,5 +90,8 @@ export default { this.currentValue = this.$n(this.amountValue, 'ungroupedDecimal') }, }, + mounted() { + if (this.value !== '') this.normalizeAmount(true) + }, } diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 65fde8d1d..3b1186a0c 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -13,7 +13,6 @@ export const verifyLogin = gql` hasElopage publisherId isAdmin - creation hideAmountGDD hideAmountGDT }