diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js index 56694296a..91207901c 100644 --- a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js @@ -47,7 +47,7 @@ describe('GddSend confirm', () => { }) }) - describe('has total balance equal 0', () => { + describe('has totalBalance under 0', () => { beforeEach(async () => { await wrapper.setProps({ balance: 0, diff --git a/frontend/src/components/Menu/Navbar.spec.js b/frontend/src/components/Menu/Navbar.spec.js index e7a0f4784..1e08ad9dc 100644 --- a/frontend/src/components/Menu/Navbar.spec.js +++ b/frontend/src/components/Menu/Navbar.spec.js @@ -48,53 +48,57 @@ describe('Navbar', () => { expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy() }) - it('has twelve b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(12) + it('has thirteen b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(13) }) - it('has first nav-item "amount GDD" in navbar', () => { + it('has nav-item "amount GDD" in navbar', () => { expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('1234 GDD') }) - it('has first nav-item "navigation.overview" in navbar', () => { + it('has nav-item "navigation.overview" in navbar', () => { expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.overview') }) - it('has first nav-item "navigation.send" in navbar', () => { + it('has nav-item "navigation.send" in navbar', () => { expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.send') }) - it('has first nav-item "navigation.transactions" in navbar', () => { + it('has nav-item "navigation.transactions" in navbar', () => { expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.transactions') }) - it('has first nav-item "gdt.gdt" in navbar', () => { + it('has nav-item "gdt.gdt" in navbar', () => { expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('gdt.gdt') }) - it('has first nav-item "navigation.community" in navbar', () => { + it('has nav-item "navigation.community" in navbar', () => { expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.community') }) - it('has first nav-item "navigation.profile" in navbar', () => { + it('has nav-item "navigation.profile" in navbar', () => { expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.profile') }) + + it('has nav-item "navigation.info" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.info') + }) }) describe('navigation Navbar (user has an elopage account)', () => { it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(9).text()).toContain('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(9).find('a').attributes('href')).toBe( + expect(wrapper.findAll('.nav-item').at(10).text()).toContain('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(10).find('a').attributes('href')).toBe( 'https://elopage.com', ) }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(10).text()).toEqual('navigation.admin_area') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.logout') + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(12).text()).toEqual('navigation.logout') }) }) @@ -104,12 +108,12 @@ describe('Navbar', () => { wrapper = Wrapper() }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.admin_area') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(10).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(10).text()).toEqual('navigation.logout') + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(11).text()).toEqual('navigation.logout') }) }) }) diff --git a/frontend/src/components/Menu/Navbar.vue b/frontend/src/components/Menu/Navbar.vue index a0f6e5a9d..d1fe3af9d 100644 --- a/frontend/src/components/Menu/Navbar.vue +++ b/frontend/src/components/Menu/Navbar.vue @@ -64,6 +64,10 @@ {{ $t('navigation.profile') }} + + + {{ $t('navigation.info') }} +
diff --git a/frontend/src/components/Menu/Sidebar.spec.js b/frontend/src/components/Menu/Sidebar.spec.js index 1ffa8bf15..612649762 100644 --- a/frontend/src/components/Menu/Sidebar.spec.js +++ b/frontend/src/components/Menu/Sidebar.spec.js @@ -32,48 +32,62 @@ describe('Sidebar', () => { expect(wrapper.find('div#component-sidebar').exists()).toBeTruthy() }) - describe('navigation Navbar (general elements)', () => { - it('has first nav-item "navigation.overview" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview') + describe('navigation Navbar', () => { + it('has ten b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(10) }) - it('has first nav-item "navigation.send" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') + describe('navigation Navbar (general elements)', () => { + it('has nav-item "navigation.overview" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(0).text()).toEqual('navigation.overview') + }) + + it('has nav-item "navigation.send" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(1).text()).toEqual('navigation.send') + }) + + it('has nav-item "gdt.gdt" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('gdt.gdt') + }) + + it('has nav-item "navigation.community" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.community') + }) + + it('has nav-item "navigation.profile" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.profile') + }) + + it('has nav-item "navigation.info" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.info') + }) }) - it('has first nav-item "navigation.transactions" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions') + describe('navigation Navbar (user has an elopage account)', () => { + it('has ten b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(10) + }) + + it('has a link to the members area', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.members_area') + expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe('#') + }) + + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') + }) + + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') + }) }) - it('has first nav-item "gdt.gdt" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('gdt.gdt') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.community" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.community') - }) - - it('has first nav-item "navigation.profile" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.profile') - }) - }) - - describe('navigation Navbar (user has an elopage account)', () => { - it('has eight b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(9) - }) - - it('has a link to the members area', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.members_area') - expect(wrapper.findAll('.nav-item').at(6).find('a').attributes('href')).toBe('#') - }) - - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.admin_area') - }) - - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.logout') + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout') }) }) @@ -83,16 +97,16 @@ describe('Sidebar', () => { wrapper = Wrapper() }) - it('has seven b-nav-item in the navbar', () => { - expect(wrapper.findAll('.nav-item')).toHaveLength(8) + it('has nine b-nav-item in the navbar', () => { + expect(wrapper.findAll('.nav-item')).toHaveLength(9) }) - it('has first nav-item "navigation.admin_area" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.admin_area') + it('has nav-item "navigation.admin_area" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.admin_area') }) - it('has first nav-item "navigation.logout" in navbar', () => { - expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.logout') + it('has nav-item "navigation.logout" in navbar', () => { + expect(wrapper.findAll('.nav-item').at(8).text()).toEqual('navigation.logout') }) }) }) diff --git a/frontend/src/components/Menu/Sidebar.vue b/frontend/src/components/Menu/Sidebar.vue index fc3ecc21d..4b38851b2 100644 --- a/frontend/src/components/Menu/Sidebar.vue +++ b/frontend/src/components/Menu/Sidebar.vue @@ -28,6 +28,10 @@ {{ $t('navigation.profile') }} + + + {{ $t('navigation.info') }} +
diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index b74770227..03299dd49 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -163,6 +163,26 @@ export const listTransactionLinks = gql` } ` +export const listContributionLinks = gql` + query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { + listContributionLinks(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + links { + id + amount + name + memo + createdAt + validFrom + validTo + maxAmountPerMonth + cycle + maxPerCycle + } + count + } + } +` + export const listContributions = gql` query( $currentPage: Int = 1 @@ -209,3 +229,29 @@ export const listAllContributions = gql` } } ` + +export const communityStatistics = gql` + query { + communityStatistics { + totalUsers + activeUsers + deletedUsers + totalGradidoCreated + totalGradidoDecayed + totalGradidoAvailable + totalGradidoUnbookedDecayed + } + } +` + +export const searchAdminUsers = gql` + query { + searchAdminUsers { + userCount + userList { + firstName + lastName + } + } + } +` diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index bea2b5c98..74c5fca8e 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -26,8 +26,13 @@ "community": "Gemeinschaft", "continue-to-registration": "Weiter zur Registrierung", "current-community": "Aktuelle Gemeinschaft", + "members": "Mitglieder", + "moderators": "Moderatoren", "myContributions": "Meine Beiträge zum Gemeinwohl", + "openContributionLinks": "öffentliche Beitrags-Linkliste", + "openContributionLinkText": "Folgende {count} automatische Schöpfungen werden zur Zeit durch die Gemeinschaft „{name}“ bereitgestellt.", "other-communities": "Weitere Gemeinschaften", + "statistic": "Statistik", "submitContribution": "Beitrag einreichen", "switch-to-this-community": "zu dieser Gemeinschaft wechseln" }, @@ -59,7 +64,8 @@ "yourActivity": "Bitte trage eine Tätigkeit ein!" }, "contribution-link": { - "thanksYouWith": "dankt dir mit" + "thanksYouWith": "dankt dir mit", + "unique": "(einmalig)" }, "decay": { "before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.", @@ -224,6 +230,7 @@ "navigation": { "admin_area": "Adminbereich", "community": "Gemeinschaft", + "info": "Information", "logout": "Abmelden", "members_area": "Mitgliederbereich", "overview": "Übersicht", @@ -298,6 +305,14 @@ "uppercase": "Großbuchstabe erforderlich." } }, + "statistic": { + "activeUsers": "Aktive Mitglieder", + "deletedUsers": "Gelöschte Mitglieder", + "totalGradidoAvailable": "GDD insgesamt im Umlauf", + "totalGradidoCreated": "GDD insgesamt geschöpft", + "totalGradidoDecayed": "GDD insgesamt verfallen", + "totalGradidoUnbookedDecayed": "Gesamter ungebuchter GDD Verfall" + }, "success": "Erfolg", "time": { "days": "Tage", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index da9ae4abf..d7c745c72 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -26,8 +26,13 @@ "community": "Community", "continue-to-registration": "Continue to registration", "current-community": "Current community", + "members": "Members", + "moderators": "Moderators", "myContributions": "My contributions to the common good", + "openContributionLinks": "open Contribution links list", + "openContributionLinkText": "The following {count} automatic creations are currently provided by the \"{name}\" community.", "other-communities": "Other communities", + "statistic": "Statistics", "submitContribution": "Submit contribution", "switch-to-this-community": "Switch to this community" }, @@ -59,7 +64,8 @@ "yourActivity": "Please enter an activity!" }, "contribution-link": { - "thanksYouWith": "thanks you with" + "thanksYouWith": "thanks you with", + "unique": "(unique)" }, "decay": { "before_startblock_transaction": "This transaction does not include decay.", @@ -224,6 +230,7 @@ "navigation": { "admin_area": "Admin Area", "community": "Community", + "info": "Information", "logout": "Logout", "members_area": "Members area", "overview": "Overview", @@ -298,6 +305,14 @@ "uppercase": "One uppercase letter required." } }, + "statistic": { + "activeUsers": "Active members", + "deletedUsers": "Deleted members", + "totalGradidoAvailable": "Total GDD in circulation", + "totalGradidoCreated": "Total created GDD", + "totalGradidoDecayed": "Total GDD decay", + "totalGradidoUnbookedDecayed": "Total unbooked GDD decay" + }, "success": "Success", "time": { "days": "Days", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index fa2cd31a9..689f0926e 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -26,8 +26,13 @@ "community": "Comunidad", "continue-to-registration": "Continuar con el registro", "current-community": "Comunidad actual", + "members": "Miembros", + "moderators": "Moderadores", "myContributions": "Mis contribuciones al bien común", + "openContributionLinks": "lista de enlaces de contribuciones públicas", + "openContributionLinkText": "La comunidad \"{name}\" proporciona actualmente las siguientes {count} creaciones automáticas.", "other-communities": "Otras comunidades", + "statistic": "Estadísticas", "submitContribution": "Aportar una contribución", "switch-to-this-community": "cambiar a esta comunidad" }, @@ -59,7 +64,8 @@ "yourActivity": "¡Por favor, introduce una actividad!" }, "contribution-link": { - "thanksYouWith": "agradecidos con" + "thanksYouWith": "te agradece con", + "unique": "(único)" }, "decay": { "before_startblock_transaction": "Esta transacción no implica disminución en su valor.", @@ -224,6 +230,7 @@ "navigation": { "admin_area": "Área de administración", "community": "Comunidad", + "info": "Información", "logout": "Salir", "members_area": "Área de afiliados", "overview": "Resumen", @@ -298,6 +305,14 @@ "uppercase": "Letra mayúscula requerida." } }, + "statistic": { + "activeUsers": "miembros activos", + "deletedUsers": "miembros eliminados", + "totalGradidoAvailable": "GDD total en circulación", + "totalGradidoCreated": "GDD total creado", + "totalGradidoDecayed": "GDD total decaído", + "totalGradidoUnbookedDecayed": "GDD no contabilizado decaído" + }, "success": "Lo lograste", "time": { "days": "Días", diff --git a/frontend/src/pages/InfoStatistic.spec.js b/frontend/src/pages/InfoStatistic.spec.js new file mode 100644 index 000000000..6adcf77d4 --- /dev/null +++ b/frontend/src/pages/InfoStatistic.spec.js @@ -0,0 +1,130 @@ +import { mount } from '@vue/test-utils' +import InfoStatistic from './InfoStatistic' +import { toastErrorSpy } from '../../test/testSetup' +import { listContributionLinks, communityStatistics, searchAdminUsers } from '@/graphql/queries' + +const localVue = global.localVue + +const apolloQueryMock = jest + .fn() + .mockResolvedValueOnce({ + data: { + listContributionLinks: { + count: 2, + links: [ + { + id: 1, + amount: 200, + name: 'Dokumenta 2017', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2017', + cycle: 'ONCE', + }, + { + id: 2, + amount: 200, + name: 'Dokumenta 2022', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', + cycle: 'ONCE', + }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + data: { + searchAdminUsers: { + userCount: 2, + userList: [ + { firstName: 'Peter', lastName: 'Lustig' }, + { firstName: 'Super', lastName: 'Admin' }, + ], + }, + }, + }) + .mockResolvedValueOnce({ + data: { + communityStatistics: { + totalUsers: 3113, + activeUsers: 1057, + deletedUsers: 35, + totalGradidoCreated: '4083774.05000000000000000000', + totalGradidoDecayed: '-1062639.13634129622923372197', + totalGradidoAvailable: '2513565.869444365732411569', + totalGradidoUnbookedDecayed: '-500474.6738366222166261272', + }, + }, + }) + .mockResolvedValue('default') + +describe('InfoStatistic', () => { + let wrapper + + const mocks = { + $i18n: { + locale: 'en', + }, + $t: jest.fn((t) => t), + $apollo: { + query: apolloQueryMock, + }, + } + + const Wrapper = () => { + return mount(InfoStatistic, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders the info page', () => { + expect(wrapper.find('div.info-statistic').exists()).toBe(true) + }) + + it('calls listContributionLinks', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: listContributionLinks, + fetchPolicy: 'network-only', + }), + ) + }) + + it('calls searchAdminUsers', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: searchAdminUsers, + fetchPolicy: 'network-only', + }), + ) + }) + + it('calls getCommunityStatistics', () => { + expect(apolloQueryMock).toBeCalledWith( + expect.objectContaining({ + query: communityStatistics, + fetchPolicy: 'network-only', + }), + ) + }) + + describe('error apolloQueryMock', () => { + beforeEach(async () => { + jest.clearAllMocks() + apolloQueryMock.mockRejectedValue({ + message: 'uups', + }) + wrapper = Wrapper() + }) + + it('toasts three error messages', () => { + expect(toastErrorSpy).toBeCalledWith( + 'listContributionLinks has no result, use default data', + ) + expect(toastErrorSpy).toBeCalledWith('searchAdminUsers has no result, use default data') + expect(toastErrorSpy).toBeCalledWith('communityStatistics has no result, use default data') + }) + }) + }) +}) diff --git a/frontend/src/pages/InfoStatistic.vue b/frontend/src/pages/InfoStatistic.vue new file mode 100644 index 000000000..3d38e730a --- /dev/null +++ b/frontend/src/pages/InfoStatistic.vue @@ -0,0 +1,166 @@ + + diff --git a/frontend/src/routes/router.test.js b/frontend/src/routes/router.test.js index d3f9cf992..dbc6dfaa9 100644 --- a/frontend/src/routes/router.test.js +++ b/frontend/src/routes/router.test.js @@ -50,7 +50,7 @@ describe('router', () => { }) it('has sixteen routes defined', () => { - expect(routes).toHaveLength(18) + expect(routes).toHaveLength(19) }) describe('overview', () => { @@ -108,6 +108,28 @@ describe('router', () => { }) }) + describe('community', () => { + it('requires authorization', () => { + expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy() + }) + + it('loads the "Community" page', async () => { + const component = await routes.find((r) => r.path === '/community').component() + expect(component.default.name).toBe('Community') + }) + }) + + describe('info', () => { + it('requires authorization', () => { + expect(routes.find((r) => r.path === '/information').meta.requiresAuth).toBeTruthy() + }) + + it('loads the "InfoStatistic" page', async () => { + const component = await routes.find((r) => r.path === '/information').component() + expect(component.default.name).toBe('InfoStatistic') + }) + }) + describe('gdt', () => { it('requires authorization', () => { expect(routes.find((r) => r.path === '/gdt').meta.requiresAuth).toBeTruthy() diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index 2c727304b..5d82be5fb 100755 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -54,6 +54,13 @@ const routes = [ requiresAuth: true, }, }, + { + path: '/information', + component: () => import('@/pages/InfoStatistic.vue'), + meta: { + requiresAuth: true, + }, + }, { path: '/login/:code?', component: () => import('@/pages/Login.vue'),