Merge branch 'master' into 2010-send-email-to-transaction-link-sender

This commit is contained in:
Hannes Heine 2022-08-11 14:40:42 +02:00 committed by GitHub
commit 37d9ef1c30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 3234 additions and 488 deletions

View File

@ -4,8 +4,55 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0)
- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102)
- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101)
- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099)
- fix: Use Inner Join for Contribution and User [`#2100`](https://github.com/gradido/gradido/pull/2100)
- Enable copying the link, username, amount, and memo text after transaction link creation [`#2098`](https://github.com/gradido/gradido/pull/2098)
- feat: Insert Missing Contributions Migration [`#2053`](https://github.com/gradido/gradido/pull/2053)
- [Bug] Wallet improvments for Contributions [`#2090`](https://github.com/gradido/gradido/pull/2090)
- [Fix] Add createdAt & contributionDate to ContributionListItems [`#2093`](https://github.com/gradido/gradido/pull/2093)
- fix: 🍰 Reset Amount In Contribution Form And Write A Test [`#2086`](https://github.com/gradido/gradido/pull/2086)
- [Feat] Replace logic to validation-provider. [`#2088`](https://github.com/gradido/gradido/pull/2088)
- fix: Add Confirm Dialog on Delete Contribution [`#2087`](https://github.com/gradido/gradido/pull/2087)
- fix: Admin Cannot Edit User Contribution [`#2085`](https://github.com/gradido/gradido/pull/2085)
- fix: Update contribution_date when Moved by Seed [`#2083`](https://github.com/gradido/gradido/pull/2083)
- chore: 🍰 Provide Volume For Backend Log-Files In Docker [`#2067`](https://github.com/gradido/gradido/pull/2067)
- feat: 🍰 Community Contribution Site And Form [`#2042`](https://github.com/gradido/gradido/pull/2042)
- [Refactor] Move MEMO_MIN_CHARS and MEMO_MAX_CHARS to const file [`#2082`](https://github.com/gradido/gradido/pull/2082)
- Fix: Test memo length on createContribution & updateContribution [`#2080`](https://github.com/gradido/gradido/pull/2080)
- chore: 🍰 Change `image` Entries In Docker Compose Files And Get Apple M1 Running [`#2050`](https://github.com/gradido/gradido/pull/2050)
- fix: Windows 0D 0A Linebreaks to Unix 0A [`#2064`](https://github.com/gradido/gradido/pull/2064)
- Add contributionDate to the Contribution object. [`#2066`](https://github.com/gradido/gradido/pull/2066)
- fix: Add Contributions to User [`#2062`](https://github.com/gradido/gradido/pull/2062)
- Feat: ContributionResolver - delete mutation [`#2035`](https://github.com/gradido/gradido/pull/2035)
- Fix: Add count to list contributions [`#2061`](https://github.com/gradido/gradido/pull/2061)
- docu: Explain how `.env` Files are Working [`#2022`](https://github.com/gradido/gradido/pull/2022)
- [WIP] 1794 feature event protocol 1 implement the basics of the business event protocol [`#1997`](https://github.com/gradido/gradido/pull/1997)
- docs: 🍰 Document The Setup Of The GraphQL Playground [`#2060`](https://github.com/gradido/gradido/pull/2060)
- Feat: List all contribution [`#2057`](https://github.com/gradido/gradido/pull/2057)
- feat: Do not log IntrospectionQuery from Query Browser [`#2059`](https://github.com/gradido/gradido/pull/2059)
- Feat: Add confirmedBy and confirmedAt for the contribution query. [`#2052`](https://github.com/gradido/gradido/pull/2052)
- Add open creations to webapp [`#2048`](https://github.com/gradido/gradido/pull/2048)
- Prevent session expiration modal from displaying negative seconds, when session is expired for more than 0 seconds [`#2054`](https://github.com/gradido/gradido/pull/2054)
- feat: mutation contribution update [`#2032`](https://github.com/gradido/gradido/pull/2032)
- feat: Login Returns Open Creations for User [`#2046`](https://github.com/gradido/gradido/pull/2046)
- Migrate transaction to valid dataset for gradido node [`#2029`](https://github.com/gradido/gradido/pull/2029)
- feat: implement contribution list query [`#2031`](https://github.com/gradido/gradido/pull/2031)
- add code for moving user creation date if transaction before exist [`#2034`](https://github.com/gradido/gradido/pull/2034)
- change text from page [`#2037`](https://github.com/gradido/gradido/pull/2037)
- Transaction link: copy link, text and more [`#2030`](https://github.com/gradido/gradido/pull/2030)
- change welcome in community text [`#2025`](https://github.com/gradido/gradido/pull/2025)
- changed link color in navbar and language switch [`#2024`](https://github.com/gradido/gradido/pull/2024)
- feat: ContributionResolver - createContribution [`#2009`](https://github.com/gradido/gradido/pull/2009)
#### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1)
> 30 June 2022
- release: 1.10.1 [`#2021`](https://github.com/gradido/gradido/pull/2021)
- automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001)
- 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998)
- Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996)

44
DOCKER_MORE_CLOSELY.md Normal file
View File

@ -0,0 +1,44 @@
# Docker More Closely
## Apple M1 Platform
***Attention:** For using Docker commands in Apple M1 environments!*
### Enviroment Variable For Apple M1 Platform
To set the Docker platform environment variable in your terminal tab, run:
```bash
# set env variable for your shell
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
```
### Docker Compose Override File For Apple M1 Platform
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
```bash
# in main folder
# for development
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
# for production
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
```
## Analysing Docker Builds
To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it!
The `dive build` command is exactly the right one to fulfill what we are looking for.
We can use it just like the `docker build` command and get an analysis afterwards.
So, in our main folder, we use it in the following way:
```bash
# in main folder
$ dive build --target <layer-name> -t "gradido/<app-name>:local-<layer-name>" <app-folder-name-or-dot>/
```
For the specific applications, see our [publish.yml](.github/workflows/publish.yml).

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.10.1",
"version": "1.11.0",
"license": "Apache-2.0",
"private": false,
"scripts": {

View File

@ -28,7 +28,7 @@ const propsData = {
amount: 210,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: 1,
moderator: null,
creation: [790, 1000, 1000],
__typename: 'PendingCreation',
},
@ -66,7 +66,7 @@ const propsData = {
},
},
{ key: 'moderator', label: 'moderator' },
{ key: 'edit_creation', label: 'edit' },
{ key: 'editCreation', label: 'edit' },
{ key: 'confirm', label: 'save' },
],
toggleDetails: false,
@ -113,6 +113,10 @@ describe('OpenCreationsTable', () => {
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBe(true)
})
it('has no button.bi-pencil-square for user contribution ', () => {
expect(wrapper.findAll('tr').at(2).find('.bi-pencil-square').exists()).toBe(false)
})
describe('show edit details', () => {
beforeEach(async () => {
await wrapper.findAll('tr').at(1).find('.bi-pencil-square').trigger('click')

View File

@ -11,8 +11,14 @@
<b-icon icon="x" variant="light"></b-icon>
</b-button>
</template>
<template #cell(edit_creation)="row">
<b-button variant="info" size="md" @click="rowToggleDetails(row, 0)" class="mr-2">
<template #cell(editCreation)="row">
<b-button
v-if="row.item.moderator"
variant="info"
size="md"
@click="rowToggleDetails(row, 0)"
class="mr-2"
>
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
</b-button>
</template>

View File

@ -11,6 +11,7 @@
:per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
</div>
</template>

View File

@ -35,6 +35,7 @@
"creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für",
"deleteNow": "Möchtest du diesen Beitrag zur Gemeinschaft wirklich löschen?",
"enter_text": "Text eintragen",
"form": "Schöpfungsformular",
"min_characters": "Mindestens 10 Zeichen eingeben",

View File

@ -35,6 +35,7 @@
"creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for",
"deleteNow": "Do you really want to delete this contribution to the community?",
"enter_text": "Enter text",
"form": "Creation form",
"min_characters": "Enter at least 10 characters",

View File

@ -29,6 +29,7 @@
per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
</b-col>
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">

View File

@ -18,7 +18,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 0,
moderator: 2,
},
{
id: 2,
@ -28,7 +28,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 0,
moderator: 2,
},
],
},
@ -80,28 +80,54 @@ describe('CreationConfirm', () => {
})
describe('remove creation with success', () => {
beforeEach(async () => {
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
let spy
it('calls the adminDeleteContribution mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminDeleteContribution,
variables: { id: 1 },
describe('admin confirms deletion', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve('some value'))
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
it('opens a modal', () => {
expect(spy).toBeCalled()
})
it('calls the adminDeleteContribution mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminDeleteContribution,
variables: { id: 1 },
})
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
})
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
describe('admin cancels deletion', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(false))
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
it('does not call the adminDeleteContribution mutation', () => {
expect(apolloMutateMock).not.toBeCalled()
})
})
})
describe('remove creation with error', () => {
let spy
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve('some value'))
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
})

View File

@ -34,20 +34,23 @@ export default {
},
methods: {
removeCreation(item) {
this.$apollo
.mutate({
mutation: adminDeleteContribution,
variables: {
id: item.id,
},
})
.then((result) => {
this.updatePendingCreations(item.id)
this.toastSuccess(this.$t('creation_form.toasted_delete'))
})
.catch((error) => {
this.toastError(error.message)
})
this.$bvModal.msgBoxConfirm(this.$t('creation_form.deleteNow')).then(async (value) => {
if (value)
await this.$apollo
.mutate({
mutation: adminDeleteContribution,
variables: {
id: item.id,
},
})
.then((result) => {
this.updatePendingCreations(item.id)
this.toastSuccess(this.$t('creation_form.toasted_delete'))
})
.catch((error) => {
this.toastError(error.message)
})
})
},
confirmCreation() {
this.$apollo
@ -114,7 +117,7 @@ export default {
},
},
{ key: 'moderator', label: this.$t('moderator') },
{ key: 'edit_creation', label: this.$t('edit') },
{ key: 'editCreation', label: this.$t('edit') },
{ key: 'confirm', label: this.$t('save') },
]
},

View File

@ -52,6 +52,7 @@
per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
<div></div>
</div>

View File

@ -1,60 +1,60 @@
CONFIG_VERSION=v9.2022-07-07
# Server
PORT=4000
JWT_SECRET=secret123
JWT_EXPIRES_IN=10m
GRAPHIQL=false
GDT_API_URL=https://gdt.gradido.net
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
# Klicktipp
KLICKTIPP=false
KLICKTTIPP_API_URL=https://api.klicktipp.com
KLICKTIPP_USER=gradido_test
KLICKTIPP_PASSWORD=secret321
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
# Community
COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
# Login Server
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
# EMail
EMAIL=false
EMAIL_USERNAME=gradido_email
EMAIL_SENDER=info@gradido.net
EMAIL_PASSWORD=xxx
EMAIL_SMTP_URL=gmail.com
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin}
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
EMAIL_LINK_OVERVIEW=http://localhost/overview
EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info
CONFIG_VERSION=v9.2022-07-07
# Server
PORT=4000
JWT_SECRET=secret123
JWT_EXPIRES_IN=10m
GRAPHIQL=false
GDT_API_URL=https://gdt.gradido.net
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
# Klicktipp
KLICKTIPP=false
KLICKTTIPP_API_URL=https://api.klicktipp.com
KLICKTIPP_USER=gradido_test
KLICKTIPP_PASSWORD=secret321
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
# Community
COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
# Login Server
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
# EMail
EMAIL=false
EMAIL_USERNAME=gradido_email
EMAIL_SENDER=info@gradido.net
EMAIL_PASSWORD=xxx
EMAIL_SMTP_URL=gmail.com
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin}
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
EMAIL_LINK_OVERVIEW=http://localhost/overview
EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.10.1",
"version": "1.11.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",

View File

@ -26,9 +26,11 @@ export enum RIGHTS {
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
GDT_BALANCE = 'GDT_BALANCE',
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION',
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',
@ -44,7 +46,6 @@ export enum RIGHTS {
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
}

View File

@ -24,9 +24,11 @@ export const ROLE_USER = new Role('user', [
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTION_LINKS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -1,129 +1,129 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
import dotenv from 'dotenv'
import Decimal from 'decimal.js-light'
dotenv.config()
Decimal.set({
precision: 25,
rounding: Decimal.ROUND_HALF_UP,
})
const constants = {
DB_VERSION: '0043-add_event_protocol_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v9.2022-07-07',
CURRENT: '',
},
}
const server = {
PORT: process.env.PORT || 4000,
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
PRODUCTION: process.env.NODE_ENV === 'production' || false,
}
const database = {
DB_HOST: process.env.DB_HOST || 'localhost',
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
DB_USER: process.env.DB_USER || 'root',
DB_PASSWORD: process.env.DB_PASSWORD || '',
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
}
const klicktipp = {
KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com',
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test',
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321',
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE',
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
}
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
COMMUNITY_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
const loginServer = {
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
EMAIL_LINK_FORGOTPASSWORD:
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
// time in minutes a optin code is valid
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
: 1440,
// time in minutes that must pass to request a new optin code
EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME
? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10
: 10,
}
const webhook = {
// Elopage
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
// Check config version
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
if (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
constants.CONFIG_VERSION.CURRENT,
)
) {
throw new Error(
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
)
}
const CONFIG = {
...constants,
...server,
...database,
...klicktipp,
...community,
...email,
...loginServer,
...webhook,
...eventProtocol,
}
export default CONFIG
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
import dotenv from 'dotenv'
import Decimal from 'decimal.js-light'
dotenv.config()
Decimal.set({
precision: 25,
rounding: Decimal.ROUND_HALF_UP,
})
const constants = {
DB_VERSION: '0044-insert_missing_contributions',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v9.2022-07-07',
CURRENT: '',
},
}
const server = {
PORT: process.env.PORT || 4000,
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
PRODUCTION: process.env.NODE_ENV === 'production' || false,
}
const database = {
DB_HOST: process.env.DB_HOST || 'localhost',
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
DB_USER: process.env.DB_USER || 'root',
DB_PASSWORD: process.env.DB_PASSWORD || '',
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
}
const klicktipp = {
KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com',
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test',
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321',
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE',
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
}
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
COMMUNITY_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
const loginServer = {
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
}
const email = {
EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
EMAIL_LINK_FORGOTPASSWORD:
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
// time in minutes a optin code is valid
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
: 1440,
// time in minutes that must pass to request a new optin code
EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME
? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10
: 10,
}
const webhook = {
// Elopage
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
// Check config version
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
if (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
constants.CONFIG_VERSION.CURRENT,
)
) {
throw new Error(
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
)
}
const CONFIG = {
...constants,
...server,
...database,
...klicktipp,
...community,
...email,
...loginServer,
...webhook,
...eventProtocol,
}
export default CONFIG

View File

@ -15,6 +15,7 @@ export class Contribution {
this.deletedAt = contribution.deletedAt
this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy
this.contributionDate = contribution.contributionDate
}
@Field(() => Number)
@ -43,6 +44,9 @@ export class Contribution {
@Field(() => Number, { nullable: true })
confirmedBy: number | null
@Field(() => Date)
contributionDate: Date
}
@ObjectType()

View File

@ -1857,11 +1857,17 @@ describe('AdminResolver', () => {
})
})
// TODO: Set this test in new location to have datas
describe('listContributionLinks', () => {
it('returns an error', async () => {
it('returns an empty object', async () => {
await expect(query({ query: listContributionLinks })).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
data: {
listContributionLinks: {
count: 0,
links: [],
},
},
}),
)
})

View File

@ -54,11 +54,11 @@ import {
updateCreations,
} from './util/creations'
import {
CONTRIBUTIONLINK_MEMO_MAX_CHARS,
CONTRIBUTIONLINK_MEMO_MIN_CHARS,
CONTRIBUTIONLINK_NAME_MAX_CHARS,
CONTRIBUTIONLINK_NAME_MIN_CHARS,
FULL_CREATION_AVAILABLE,
MEMO_MAX_CHARS,
MEMO_MIN_CHARS,
} from './const/const'
// const EMAIL_OPT_IN_REGISTER = 1
@ -595,11 +595,8 @@ export class AdminResolver {
logger.error(`The memo must be initialized!`)
throw new Error(`The memo must be initialized!`)
}
if (
memo.length < CONTRIBUTIONLINK_MEMO_MIN_CHARS ||
memo.length > CONTRIBUTIONLINK_MEMO_MAX_CHARS
) {
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_MEMO_MIN_CHARS} and max=${CONTRIBUTIONLINK_MEMO_MAX_CHARS}`
if (memo.length < MEMO_MIN_CHARS || memo.length > MEMO_MAX_CHARS) {
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${MEMO_MIN_CHARS} and max=${MEMO_MAX_CHARS}`
logger.error(`${msg}`)
throw new Error(`${msg}`)
}

View File

@ -4,7 +4,9 @@
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import {
adminUpdateContribution,
confirmContribution,
createContribution,
deleteContribution,
updateContribution,
} from '@/seeds/graphql/mutations'
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries'
@ -64,6 +66,42 @@ describe('ContributionResolver', () => {
})
describe('input not valid', () => {
it('throws error when memo length smaller than 5 chars', async () => {
const date = new Date()
await expect(
mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test',
creationDate: date.toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
}),
)
})
it('throws error when memo length greater than 255 chars', async () => {
const date = new Date()
await expect(
mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
creationDate: date.toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
}),
)
})
it('throws error when creationDate not-valid', async () => {
await expect(
mutate({
@ -311,6 +349,48 @@ describe('ContributionResolver', () => {
})
})
describe('Memo length smaller than 5 chars', () => {
it('throws error', async () => {
const date = new Date()
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 100.0,
memo: 'Test',
creationDate: date.toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
}),
)
})
})
describe('Memo length greater than 255 chars', () => {
it('throws error', async () => {
const date = new Date()
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 100.0,
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
creationDate: date.toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
}),
)
})
})
describe('wrong user tries to update the contribution', () => {
beforeAll(async () => {
await query({
@ -487,6 +567,11 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns allCreation', async () => {
await expect(
query({
@ -522,4 +607,129 @@ describe('ContributionResolver', () => {
})
})
})
describe('deleteContribution', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: deleteContribution,
variables: {
id: -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
result = await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('wrong contribution id', () => {
it('returns an error', async () => {
await expect(
mutate({
mutation: deleteContribution,
variables: {
id: -1,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Contribution not found for given id.')],
}),
)
})
})
describe('other user sends a deleteContribtuion', () => {
it('returns an error', async () => {
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await expect(
mutate({
mutation: deleteContribution,
variables: {
id: result.data.createContribution.id,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Can not delete contribution of another user')],
}),
)
})
})
describe('User deletes own contribution', () => {
it('deletes successfully', async () => {
await expect(
mutate({
mutation: deleteContribution,
variables: {
id: result.data.createContribution.id,
},
}),
).resolves.toBeTruthy()
})
})
describe('User deletes already confirmed contribution', () => {
it('throws an error', async () => {
await query({
query: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: confirmContribution,
variables: {
id: result.data.createContribution.id,
},
})
await query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await expect(
mutate({
mutation: deleteContribution,
variables: {
id: result.data.createContribution.id,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('A confirmed contribution can not be deleted')],
}),
)
})
})
})
})
})

View File

@ -3,7 +3,7 @@ import { Context, getUser } from '@/server/context'
import { backendLogger as logger } from '@/server/logger'
import { Contribution as dbContribution } from '@entity/Contribution'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
import { FindOperator, IsNull } from '@dbTools/typeorm'
import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm'
import ContributionArgs from '@arg/ContributionArgs'
import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order'
@ -11,6 +11,7 @@ import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { User } from '@model/User'
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
@Resolver()
export class ContributionResolver {
@ -20,6 +21,16 @@ export class ContributionResolver {
@Args() { amount, memo, creationDate }: ContributionArgs,
@Ctx() context: Context,
): Promise<UnconfirmedContribution> {
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
const user = getUser(context)
const creations = await getUserCreation(user.id)
logger.trace('creations', creations)
@ -38,6 +49,27 @@ export class ContributionResolver {
return new UnconfirmedContribution(contribution, user, creations)
}
@Authorized([RIGHTS.DELETE_CONTRIBUTION])
@Mutation(() => Boolean)
async deleteContribution(
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<boolean> {
const user = getUser(context)
const contribution = await dbContribution.findOne(id)
if (!contribution) {
throw new Error('Contribution not found for given id.')
}
if (contribution.userId !== user.id) {
throw new Error('Can not delete contribution of another user')
}
if (contribution.confirmedAt) {
throw new Error('A confirmed contribution can not be deleted')
}
const res = await contribution.softRemove()
return !!res
}
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async listContributions(
@ -74,14 +106,15 @@ export class ContributionResolver {
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionListResult> {
const [dbContributions, count] = await dbContribution.findAndCount({
relations: ['user'],
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
const [dbContributions, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(dbContribution, 'c')
.innerJoinAndSelect('c.user', 'u')
.orderBy('c.createdAt', order)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
.getManyAndCount()
return new ContributionListResult(
count,
dbContributions.map(
@ -98,6 +131,16 @@ export class ContributionResolver {
@Args() { amount, memo, creationDate }: ContributionArgs,
@Ctx() context: Context,
): Promise<UnconfirmedContribution> {
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
const user = getUser(context)
const contributionToUpdate = await dbContribution.findOne({

View File

@ -37,9 +37,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
import Decimal from 'decimal.js-light'
import { BalanceResolver } from './BalanceResolver'
const MEMO_MAX_CHARS = 255
const MEMO_MIN_CHARS = 5
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
export const executeTransaction = async (
amount: Decimal,

View File

@ -198,7 +198,7 @@ describe('UserResolver', () => {
it('sets "de" as default language', async () => {
await mutate({
mutation: createUser,
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'fr' },
})
await expect(User.find()).resolves.toEqual(
expect.arrayContaining([

View File

@ -43,7 +43,7 @@ const isPassword = (password: string): boolean => {
return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
}
const LANGUAGES = ['de', 'en']
const LANGUAGES = ['de', 'en', 'es']
const DEFAULT_LANGUAGE = 'de'
const isLanguage = (language: string): boolean => {
return LANGUAGES.includes(language)

View File

@ -8,5 +8,5 @@ export const FULL_CREATION_AVAILABLE = [
]
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
export const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
export const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
export const MEMO_MAX_CHARS = 255
export const MEMO_MIN_CHARS = 5

View File

@ -35,12 +35,17 @@ export const creationFactory = async (
if (creation.confirmed) {
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
if (creation.moveCreationDate) {
const transaction = await Transaction.findOneOrFail({
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
order: { balanceDate: 'DESC' },
})
if (transaction.decay.equals(0) && transaction.creationDate) {
confirmedCreation.contributionDate = new Date(
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
)
transaction.creationDate = new Date(
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
)
@ -48,6 +53,7 @@ export const creationFactory = async (
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
)
await transaction.save()
await confirmedCreation.save()
}
}
} else {

View File

@ -255,3 +255,9 @@ export const updateContribution = gql`
}
}
`
export const deleteContribution = gql`
mutation ($id: Int!) {
deleteContribution(id: $id)
}
`

View File

@ -0,0 +1,83 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@DeleteDateColumn()
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
isAdmin: Date | null
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@Column({
type: 'text',
name: 'passphrase',
collation: 'utf8mb4_unicode_ci',
nullable: true,
default: null,
})
passphrase: string
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
}

View File

@ -0,0 +1,34 @@
/* MIGRATION TO INSERT contributions for all transactions with type creation that do not have a contribution yet */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
`INSERT INTO gradido_community.contributions
(user_id, created_at, contribution_date, memo, amount, moderator_id, confirmed_by, confirmed_at, transaction_id)
SELECT
user_id,
balance_date,
creation_date AS contribution_date,
memo,
amount,
20 AS moderator_id,
502 AS confirmed_by,
balance_date AS confirmed_at,
id
FROM
gradido_community.transactions
WHERE
type_id = 1
AND NOT EXISTS(
SELECT * FROM gradido_community.contributions
WHERE gradido_community.contributions.transaction_id = gradido_community.transactions.id);`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'DELETE FROM `contributions` WHERE `contributions`.`moderator_id` = 20 AND `contributions`.`confirmed_by` = 502 AND `contributions`.`created_at` = `contributions`.`confirmed_at`;',
)
}

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"version": "1.10.1",
"version": "1.11.0",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",

View File

@ -1,84 +1,84 @@
GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log
# start script
DEPLOY_SEED_DATA=false
# nginx
NGINX_REWRITE_LEGACY_URLS=true
NGINX_SSL=true
NGINX_SERVER_NAME=stage1.gradido.net
NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem
NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem
NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem
NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf
NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page
# webhook
WEBHOOK_GITHUB_SECRET=secret
WEBHOOK_GITHUB_BRANCH=master
# community
COMMUNITY_NAME="Gradido Development Stage1"
COMMUNITY_URL=https://stage1.gradido.net/
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend
BACKEND_CONFIG_VERSION=v9.2022-07-07
JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
KLICKTIPP=false
KLICKTIPP_USER=
KLICKTIPP_PASSWORD=
KLICKTIPP_APIKEY_DE=
KLICKTIPP_APIKEY_EN=
EMAIL=true
EMAIL_USERNAME=peter@lustig.de
EMAIL_SENDER=peter@lustig.de
EMAIL_PASSWORD=1234
EMAIL_SMTP_URL=smtp.lustig.de
EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code}
EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin}
EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password
EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview
EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# database
DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend
FRONTEND_CONFIG_VERSION=v2.2022-04-07
GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
DEFAULT_PUBLISHER_ID=2896
META_URL=http://localhost
META_TITLE_DE="Gradido Dein Dankbarkeitskonto"
META_TITLE_EN="Gradido - Your gratitude account"
META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle."
META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all."
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# admin
ADMIN_CONFIG_VERSION=v1.2022-03-18
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log
# start script
DEPLOY_SEED_DATA=false
# nginx
NGINX_REWRITE_LEGACY_URLS=true
NGINX_SSL=true
NGINX_SERVER_NAME=stage1.gradido.net
NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem
NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem
NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem
NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf
NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page
# webhook
WEBHOOK_GITHUB_SECRET=secret
WEBHOOK_GITHUB_BRANCH=master
# community
COMMUNITY_NAME="Gradido Development Stage1"
COMMUNITY_URL=https://stage1.gradido.net/
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend
BACKEND_CONFIG_VERSION=v9.2022-07-07
JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
KLICKTIPP=false
KLICKTIPP_USER=
KLICKTIPP_PASSWORD=
KLICKTIPP_APIKEY_DE=
KLICKTIPP_APIKEY_EN=
EMAIL=true
EMAIL_USERNAME=peter@lustig.de
EMAIL_SENDER=peter@lustig.de
EMAIL_PASSWORD=1234
EMAIL_SMTP_URL=smtp.lustig.de
EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code}
EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin}
EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password
EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview
EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# database
DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend
FRONTEND_CONFIG_VERSION=v2.2022-04-07
GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
DEFAULT_PUBLISHER_ID=2896
META_URL=http://localhost
META_TITLE_DE="Gradido Dein Dankbarkeitskonto"
META_TITLE_EN="Gradido - Your gratitude account"
META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle."
META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all."
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# admin
ADMIN_CONFIG_VERSION=v1.2022-03-18
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
WALLET_URL=https://stage1.gradido.net/login

View File

@ -0,0 +1,43 @@
# This file defines the Apple M1 chip settings. It overrides docker-compose.override.yml,
# which defines the development settings.
# To use it it is required to explicitly define if you want to build with it:
# > docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
version: "3.4"
services:
########################################################
# FRONTEND #############################################
########################################################
frontend:
platform: linux/amd64
########################################################
# ADMIN INTERFACE ######################################
########################################################
admin:
platform: linux/amd64
#########################################################
## MARIADB ##############################################
#########################################################
mariadb:
platform: linux/amd64
########################################################
# BACKEND ##############################################
########################################################
backend:
platform: linux/amd64
########################################################
# DATABASE #############################################
########################################################
database:
platform: linux/amd64
#########################################################
## NGINX ################################################
#########################################################
nginx:
platform: linux/amd64

View File

@ -1,11 +1,13 @@
version: "3.4"
services:
########################################################
# FRONTEND #############################################
########################################################
frontend:
image: gradido/frontend:development
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/frontend:local-development
build:
target: development
environment:
@ -22,7 +24,8 @@ services:
# ADMIN INTERFACE ######################################
########################################################
admin:
image: gradido/admin:development
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/admin:local-development
build:
target: development
environment:
@ -39,7 +42,8 @@ services:
# BACKEND ##############################################
########################################################
backend:
image: gradido/backend:development
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/backend:local-development
build:
target: development
networks:
@ -62,10 +66,11 @@ services:
########################################################
database:
# we always run on production here since else the service lingers
# feel free to change this behaviour if it seems useful
# Due to problems with the volume caching the built files
# we changed this to test build. This keeps the service running.
image: gradido/database:test_up
# feel free to change this behaviour if it seems useful
# Due to problems with the volume caching the built files
# we changed this to test build. This keeps the service running.
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/database:local-test_up
build:
target: test_up
environment:
@ -89,9 +94,7 @@ services:
#########################################################
## NGINX ################################################
#########################################################
nginx:
volumes:
- ./logs/nginx:/var/log/nginx
# nginx:
#########################################################
## PHPMYADMIN ###########################################

View File

@ -6,6 +6,7 @@ services:
# BACKEND ##############################################
########################################################
backend:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/backend:test
build:
target: test

View File

@ -6,11 +6,13 @@
version: "3.4"
services:
########################################################
# FRONTEND #############################################
########################################################
frontend:
image: gradido/frontend:latest
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/frontend:local-production
build:
context: ./frontend
target: production
@ -35,7 +37,8 @@ services:
# ADMIN INTERFACE ######################################
########################################################
admin:
image: gradido/admin:latest
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/admin:local-production
build:
context: ./admin
target: production
@ -77,7 +80,8 @@ services:
# BACKEND ##############################################
########################################################
backend:
image: gradido/backend:latest
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/backend:local-production
build:
# since we have to include the entities from ./database we cannot define the context as ./backend
# this might blow build image size to the moon ?!
@ -103,12 +107,16 @@ services:
# Application only envs
#env_file:
# - ./frontend/.env
volumes:
# <host_machine_directy>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/backend:/logs/backend
########################################################
# DATABASE #############################################
########################################################
database:
#image: gradido/database:production_up
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/database:local-production_up
build:
context: ./database
target: production_up
@ -144,6 +152,8 @@ services:
- admin
ports:
- 80:80
volumes:
- ./logs/nginx:/var/log/nginx
networks:
external-net:

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.10.1",
"version": "1.11.0",
"private": true,
"scripts": {
"start": "node run/server.js",

View File

@ -3,8 +3,11 @@
<b-input-group v-if="canCopyLink" size="lg" class="mb-3" prepend="Link">
<b-form-input :value="link" type="text" readonly></b-form-input>
<b-input-group-append>
<b-button size="sm" text="Button" variant="primary" @click="CopyLink">
{{ $t('gdd_per_link.copy') }}
<b-button size="sm" text="Button" variant="primary" @click="copyLinkWithText">
{{ $t('gdd_per_link.copy-link-with-text') }}
</b-button>
<b-button size="sm" text="Button" variant="primary" @click="copyLink">
{{ $t('gdd_per_link.copy-link') }}
</b-button>
<b-button variant="primary" class="text-light" @click="$emit('show-qr-code-button')">
<b-img src="img/svg/qr-code.svg" width="19" class="svg"></b-img>
@ -18,29 +21,10 @@
</div>
</template>
<script>
import { copyLinks } from '../mixins/copyLinks'
export default {
name: 'ClipboardCopy',
props: {
link: { type: String, required: true },
},
data() {
return {
canCopyLink: true,
}
},
methods: {
CopyLink() {
navigator.clipboard
.writeText(this.link)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
})
.catch(() => {
this.canCopyLink = false
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
},
mixins: [copyLinks],
}
</script>
<style>

View File

@ -97,24 +97,14 @@ describe('ContentFooter', () => {
)
})
it('has a link to the members area', () => {
expect(wrapper.findAll('a.nav-link').at(2).text()).toEqual('navigation.members_area')
})
it('links to the elopage', () => {
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
'https://elopage.com/s/gradido/sign_in?locale=en',
)
})
it('links to the whitepaper', () => {
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
'https://docs.google.com/document/d/1kcX1guOi6tDgnFHD9tf7fB_MneKTx-0nHJxzdN8ygNs/edit?usp=sharing',
)
})
it('links to the support', () => {
expect(wrapper.findAll('a.nav-link').at(4).attributes('href')).toEqual(
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
'https://gradido.net/en/contact/',
)
})
@ -142,20 +132,14 @@ describe('ContentFooter', () => {
)
})
it('links to the German elopage when locale is de', () => {
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
'https://elopage.com/s/gradido/sign_in?locale=de',
)
})
it('links to the German whitepaper when locale is de', () => {
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
expect(wrapper.findAll('a.nav-link').at(2).attributes('href')).toEqual(
'https://docs.google.com/document/d/1jZp-DiiMPI9ZPNXmjsvOQ1BtnfDFfx8BX7CDmA8KKjY/edit?usp=sharing',
)
})
it('links to the German support-page when locale is de', () => {
expect(wrapper.findAll('a.nav-link').at(4).attributes('href')).toEqual(
expect(wrapper.findAll('a.nav-link').at(3).attributes('href')).toEqual(
'https://gradido.net/de/contact/',
)
})

View File

@ -34,12 +34,6 @@
<b-nav-item :href="`https://gradido.net/${$i18n.locale}/datenschutz/`" target="_blank">
{{ $t('footer.privacy_policy') }}
</b-nav-item>
<b-nav-item
:href="`https://elopage.com/s/gradido/sign_in?locale=${$i18n.locale}`"
target="_blank"
>
{{ $t('navigation.members_area') }}
</b-nav-item>
<b-nav-item
:href="
$i18n.locale === 'de'

View File

@ -0,0 +1,114 @@
import { mount } from '@vue/test-utils'
import ContributionForm from './ContributionForm.vue'
const localVue = global.localVue
describe('ContributionForm', () => {
let wrapper
const propsData = {
value: {
id: null,
date: '',
memo: '',
amount: '',
},
}
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$store: {
state: {
creation: ['1000', '1000', '1000'],
},
},
$i18n: {
locale: 'en',
},
}
const Wrapper = () => {
return mount(ContributionForm, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-form', () => {
expect(wrapper.find('div.contribution-form').exists()).toBe(true)
})
describe('empty form data', () => {
describe('buttons', () => {
it('has reset enabled', () => {
expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy()
})
it('has submit disabled', () => {
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
})
})
})
describe('set contrubtion', () => {
describe('fill in form data', () => {
const now = new Date().toISOString()
beforeEach(async () => {
await wrapper.setData({
form: {
id: null,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
})
})
describe('buttons', () => {
it('has reset enabled', () => {
expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy()
})
it('has submit enabled', () => {
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeFalsy()
})
})
})
})
describe('update contrubtion', () => {
describe('fill in form data and "id"', () => {
const now = new Date().toISOString()
beforeEach(async () => {
await wrapper.setData({
form: {
id: 2,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
})
})
describe('buttons', () => {
it('has reset enabled', () => {
expect(wrapper.find('button[type="reset"]').attributes('disabled')).toBeFalsy()
})
it('has submit enabled', () => {
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBeFalsy()
})
})
})
})
})
})

View File

@ -0,0 +1,183 @@
<template>
<div class="container contribution-form">
<div class="my-3">
<h3>{{ $t('contribution.formText.yourContribution') }}</h3>
{{ $t('contribution.formText.bringYourTalentsTo') }}
<ul class="my-3">
<li v-html="lastMonthObject"></li>
<li v-html="thisMonthObject"></li>
</ul>
<div class="my-3">
<b>{{ $t('contribution.formText.describeYourCommunity') }}</b>
</div>
</div>
<b-form ref="form" @submit.prevent="submit" class="border p-3">
<label>{{ $t('contribution.selectDate') }} {{ $t('math.asterisk') }}</label>
<b-form-datepicker
id="contribution-date"
v-model="form.date"
size="lg"
:locale="$i18n.locale"
:max="maximalDate"
:min="minimalDate"
class="mb-4"
reset-value=""
:label-no-date-selected="$t('contribution.noDateSelected')"
required
>
<template #nav-prev-year><span></span></template>
<template #nav-next-year><span></span></template>
</b-form-datepicker>
<validation-provider
:rules="{
min: minlength,
max: maxlength,
}"
:name="$t('form.message')"
v-slot="{ errors }"
>
<label class="mt-3">{{ $t('contribution.activity') }} {{ $t('math.asterisk') }}</label>
<b-form-textarea
id="contribution-memo"
v-model="form.memo"
rows="3"
max-rows="6"
:placeholder="$t('contribution.yourActivity')"
required
></b-form-textarea>
<b-col v-if="errors">
<span v-for="error in errors" class="errors" :key="error">{{ error }}</span>
</b-col>
</validation-provider>
<label class="mt-3">{{ $t('form.amount') }} {{ $t('math.asterisk') }}</label>
<b-input-group size="lg" prepend="GDD">
<b-form-input
id="contribution-amount"
v-model="form.amount"
type="text"
:formatter="numberFormat"
></b-form-input>
</b-input-group>
<div
v-if="isThisMonth && parseInt(form.amount) > parseInt(maxGddThisMonth)"
class="text-danger text-right"
>
{{ $t('contribution.formText.maxGDDforMonth', { amount: maxGddThisMonth }) }}
</div>
<div
v-if="!isThisMonth && parseInt(form.amount) > parseInt(maxGddLastMonth)"
class="text-danger text-right"
>
{{ $t('contribution.formText.maxGDDforMonth', { amount: maxGddLastMonth }) }}
</div>
<b-row class="mt-3">
<b-col>
<b-button class="test-cancel" type="reset" variant="secondary" @click="reset">
{{ $t('form.cancel') }}
</b-button>
</b-col>
<b-col class="text-right">
<b-button class="test-submit" type="submit" variant="primary" :disabled="disabled">
{{ form.id ? $t('form.change') : $t('contribution.submit') }}
</b-button>
</b-col>
</b-row>
</b-form>
<p class="p-2">{{ $t('math.asterisk') }} {{ $t('form.mandatoryField') }}</p>
</div>
</template>
<script>
export default {
name: 'ContributionForm',
props: {
value: { type: Object, required: true },
updateAmount: { type: String, required: false },
},
data() {
return {
minlength: 5,
maxlength: 255,
maximalDate: new Date(),
form: this.value, // includes 'id'
}
},
methods: {
numberFormat(value) {
return value.replace(/\D/g, '')
},
submit() {
this.form.amount = this.numberFormat(this.form.amount)
if (this.form.id) {
this.$emit('update-contribution', this.form)
} else {
this.$emit('set-contribution', this.form)
}
this.reset()
},
reset() {
this.$refs.form.reset()
this.form.id = null
this.form.date = ''
this.form.memo = ''
this.form.amount = ''
},
},
computed: {
/*
* minimalDate() = Sets the date to the 1st of the previous month.
*
*/
minimalDate() {
return new Date(this.maximalDate.getFullYear(), this.maximalDate.getMonth() - 1, 1)
},
disabled() {
if (
this.form.date === '' ||
this.form.memo.length < this.minlength ||
parseInt(this.form.amount) <= 0 ||
parseInt(this.form.amount) > 1000 ||
(this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddThisMonth)) ||
(!this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddLastMonth))
)
return true
return false
},
lastMonthObject() {
// new Date().getMonth === 1 If the current month is January, then one year must be gone back in the previous month
const obj = {
monthAndYear: this.$d(new Date(this.minimalDate), 'monthAndYear'),
creation: this.maxGddLastMonth,
}
return this.$t('contribution.formText.openAmountForMonth', obj)
},
thisMonthObject() {
const obj = {
monthAndYear: this.$d(new Date(), 'monthAndYear'),
creation: this.maxGddThisMonth,
}
return this.$t('contribution.formText.openAmountForMonth', obj)
},
isThisMonth() {
return new Date(this.form.date).getMonth() === new Date().getMonth()
},
maxGddLastMonth() {
// When edited, the amount is added back on top of the amount
return this.form.id && !this.isThisMonth
? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount)
: this.$store.state.creation[1]
},
maxGddThisMonth() {
// When edited, the amount is added back on top of the amount
return this.form.id && this.isThisMonth
? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount)
: this.$store.state.creation[2]
},
},
}
</script>
<style>
span.errors {
color: red;
}
</style>

View File

@ -0,0 +1,120 @@
import { mount } from '@vue/test-utils'
import ContributionList from './ContributionList.vue'
const localVue = global.localVue
describe('ContributionList', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
const propsData = {
contributionCount: 3,
showPagination: true,
pageSize: 25,
items: [
{
id: 0,
date: '07/06/2022',
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
amount: '200',
},
{
id: 1,
date: '06/22/2022',
memo: 'Ich habe 30 Stunden Frau Müller beim EInkaufen und im Haushalt geholfen.',
amount: '600',
},
{
id: 2,
date: '05/04/2022',
memo:
'Ich habe 50 Stunden den Nachbarkindern bei ihren Hausaufgaben geholfen und Nachhilfeunterricht gegeben.',
amount: '1000',
},
],
}
const Wrapper = () => {
return mount(ContributionList, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a DIV .contribution-list', () => {
expect(wrapper.find('div.contribution-list').exists()).toBe(true)
})
describe('pagination', () => {
describe('list count smaller than page size', () => {
it('has no pagination buttons', () => {
expect(wrapper.find('ul.pagination').exists()).toBe(false)
})
})
describe('list count greater than page size', () => {
beforeEach(() => {
wrapper.setProps({ contributionCount: 33 })
})
it('has pagination buttons', () => {
expect(wrapper.find('ul.pagination').exists()).toBe(true)
})
})
describe('switch page', () => {
const scrollToMock = jest.fn()
window.scrollTo = scrollToMock
beforeEach(async () => {
await wrapper.setProps({ contributionCount: 33 })
wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
})
it('emits update contribution list', () => {
expect(wrapper.emitted('update-list-contributions')).toEqual([
[{ currentPage: 2, pageSize: 25 }],
])
})
it('scrolls to top', () => {
expect(scrollToMock).toBeCalledWith(0, 0)
})
})
})
describe('update contribution', () => {
beforeEach(() => {
wrapper
.findComponent({ name: 'ContributionListItem' })
.vm.$emit('update-contribution-form', 'item')
})
it('emits update contribution form', () => {
expect(wrapper.emitted('update-contribution-form')).toEqual([['item']])
})
})
describe('delete contribution', () => {
beforeEach(() => {
wrapper
.findComponent({ name: 'ContributionListItem' })
.vm.$emit('delete-contribution', { id: 2 })
})
it('emits delete contribution', () => {
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]])
})
})
})
})

View File

@ -0,0 +1,77 @@
<template>
<div class="contribution-list container">
<div class="list-group" v-for="item in items" :key="item.id">
<contribution-list-item
v-bind="item"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
/>
</div>
<b-pagination
v-if="isPaginationVisible"
class="mt-3"
pills
size="lg"
v-model="currentPage"
:per-page="pageSize"
:total-rows="contributionCount"
align="center"
:hide-ellipsis="true"
></b-pagination>
</div>
</template>
<script>
import ContributionListItem from '@/components/Contributions/ContributionListItem.vue'
export default {
name: 'ContributionList',
components: {
ContributionListItem,
},
props: {
items: {
type: Array,
required: true,
},
contributionCount: {
type: Number,
required: true,
},
showPagination: {
type: Boolean,
required: true,
},
pageSize: { type: Number, default: 25 },
},
data() {
return {
currentPage: 1,
}
},
methods: {
updateListContributions() {
this.$emit('update-list-contributions', {
currentPage: this.currentPage,
pageSize: this.pageSize,
})
window.scrollTo(0, 0)
},
updateContributionForm(item) {
this.$emit('update-contribution-form', item)
},
deleteContribution(item) {
this.$emit('delete-contribution', item)
},
},
computed: {
isPaginationVisible() {
return this.showPagination && this.pageSize < this.contributionCount
},
},
watch: {
currentPage() {
this.updateListContributions()
},
},
}
</script>

View File

@ -0,0 +1,145 @@
import { mount } from '@vue/test-utils'
import ContributionListItem from './ContributionListItem.vue'
const localVue = global.localVue
describe('ContributionListItem', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
}
const propsData = {
id: 1,
createdAt: '26/07/2022',
contributionDate: '07/06/2022',
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
amount: '200',
}
const Wrapper = () => {
return mount(ContributionListItem, {
localVue,
mocks,
propsData,
})
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('has a DIV .contribution-list-item', () => {
expect(wrapper.find('div.contribution-list-item').exists()).toBe(true)
})
describe('contribution type', () => {
it('is pending by default', () => {
expect(wrapper.vm.type).toBe('pending')
})
it('is deleted when deletedAt is present', async () => {
await wrapper.setProps({ deletedAt: new Date().toISOString() })
expect(wrapper.vm.type).toBe('deleted')
})
it('is confirmed when confirmedAt is present', async () => {
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
expect(wrapper.vm.type).toBe('confirmed')
})
})
describe('contribution icon', () => {
it('is bell-fill by default', () => {
expect(wrapper.vm.icon).toBe('bell-fill')
})
it('is x-circle when deletedAt is present', async () => {
await wrapper.setProps({ deletedAt: new Date().toISOString() })
expect(wrapper.vm.icon).toBe('x-circle')
})
it('is check when confirmedAt is present', async () => {
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
expect(wrapper.vm.icon).toBe('check')
})
})
describe('contribution variant', () => {
it('is primary by default', () => {
expect(wrapper.vm.variant).toBe('primary')
})
it('is danger when deletedAt is present', async () => {
await wrapper.setProps({ deletedAt: new Date().toISOString() })
expect(wrapper.vm.variant).toBe('danger')
})
it('is success at when confirmedAt is present', async () => {
await wrapper.setProps({ confirmedAt: new Date().toISOString() })
expect(wrapper.vm.variant).toBe('success')
})
})
describe('date', () => {
it('is equal to createdAt', () => {
expect(wrapper.vm.date).toBe(wrapper.vm.createdAt)
})
})
describe('delete contribution', () => {
let spy
describe('edit contribution', () => {
beforeEach(() => {
wrapper.findAll('div.pointer').at(0).trigger('click')
})
it('emits update contribution form', () => {
expect(wrapper.emitted('update-contribution-form')).toEqual([
[
{
id: 1,
contributionDate: '07/06/2022',
memo: 'Ich habe 10 Stunden die Elbwiesen von Müll befreit.',
amount: '200',
},
],
])
})
})
describe('confirm deletion', () => {
beforeEach(() => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
wrapper.findAll('div.pointer').at(1).trigger('click')
})
it('opens the modal', () => {
expect(spy).toBeCalledWith('contribution.delete')
})
it('emits delete contribution', () => {
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 1 }]])
})
})
describe('cancel deletion', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(false))
await wrapper.findAll('div.pointer').at(1).trigger('click')
})
it('does not emit delete contribution', () => {
expect(wrapper.emitted('delete-contribution')).toBeFalsy()
})
})
})
})
})

View File

@ -0,0 +1,114 @@
<template>
<div class="contribution-list-item">
<slot>
<div class="border p-3 w-100 mb-1" :class="`border-${variant}`">
<div class="d-inline-flex">
<div class="mr-2"><b-icon :icon="icon" :variant="variant" class="h2"></b-icon></div>
<div v-if="firstName" class="mr-3">{{ firstName }} {{ lastName }}</div>
<div class="mr-2" :class="type != 'deleted' ? 'font-weight-bold' : ''">
{{ amount | GDD }}
</div>
{{ $t('math.minus') }}
<div class="mx-2">{{ $d(new Date(date), 'short') }}</div>
</div>
<div class="mr-2">
<span>{{ $t('contribution.date') }}</span>
<span>
{{ $d(new Date(contributionDate), 'monthAndYear') }}
</span>
</div>
<div class="mr-2">{{ memo }}</div>
<div v-if="type === 'pending' && !firstName" class="d-flex flex-row-reverse">
<div
class="pointer ml-5"
@click="
$emit('update-contribution-form', {
id: id,
contributionDate: contributionDate,
memo: memo,
amount: amount,
})
"
>
<b-icon icon="pencil" class="h2"></b-icon>
</div>
<div class="pointer" @click="deleteContribution({ id })">
<b-icon icon="trash" class="h2"></b-icon>
</div>
</div>
</div>
</slot>
</div>
</template>
<script>
export default {
name: 'ContributionListItem',
props: {
id: {
type: Number,
},
amount: {
type: String,
},
memo: {
type: String,
},
firstName: {
type: String,
required: false,
},
lastName: {
type: String,
required: false,
},
createdAt: {
type: String,
},
contributionDate: {
type: String,
},
deletedAt: {
type: String,
required: false,
},
confirmedBy: {
type: Number,
required: false,
},
confirmedAt: {
type: String,
required: false,
},
},
computed: {
type() {
if (this.deletedAt) return 'deleted'
if (this.confirmedAt) return 'confirmed'
return 'pending'
},
icon() {
if (this.deletedAt) return 'x-circle'
if (this.confirmedAt) return 'check'
return 'bell-fill'
},
variant() {
if (this.deletedAt) return 'danger'
if (this.confirmedAt) return 'success'
return 'primary'
},
date() {
// if (this.deletedAt) return this.deletedAt
// if (this.confirmedAt) return this.confirmedAt
// return this.contributionDate
return this.createdAt
},
},
methods: {
deleteContribution(item) {
this.$bvModal.msgBoxConfirm(this.$t('contribution.delete')).then(async (value) => {
if (value) this.$emit('delete-contribution', item)
})
},
},
}
</script>

View File

@ -3,7 +3,13 @@
<b-col>
<b-card class="p-0 gradido-custom-background">
<div class="h3 mb-4">{{ $t('gdd_per_link.created') }}</div>
<clipboard-copy :link="link" @show-qr-code-button="showQrCodeButton" />
<clipboard-copy
:link="link"
:amount="amount"
:memo="memo"
:validUntil="validUntil"
@show-qr-code-button="showQrCodeButton"
></clipboard-copy>
<div class="text-center">
<figure-qr-code v-if="showQrcode" :link="link" />
@ -27,10 +33,10 @@ export default {
FigureQrCode,
},
props: {
link: {
type: String,
required: true,
},
link: { type: String, required: true },
amount: { type: String, required: true },
memo: { type: String, required: true },
validUntil: { type: String, required: true },
},
data() {
return {

View File

@ -69,6 +69,7 @@
:per-page="pageSize"
:total-rows="transactionCount"
align="center"
:hide-ellipsis="true"
></b-pagination>
<div v-if="transactionCount <= 0" class="mt-4 text-center">

View File

@ -36,6 +36,7 @@
:per-page="pageSize"
:total-rows="transactionGdtCount"
align="center"
:hide-ellipsis="true"
></b-pagination>
</div>
</template>

View File

@ -45,7 +45,7 @@ describe('LanguageSwitch', () => {
expect(wrapper.find('div.language-switch').exists()).toBeTruthy()
})
describe('with locales en and de', () => {
describe('with locales en, de and es', () => {
describe('empty store', () => {
describe('navigator language is "en-US"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
@ -69,11 +69,22 @@ describe('LanguageSwitch', () => {
})
})
describe('navigator language is "es-ES" (not supported)', () => {
describe('navigator language is "es-ES"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Español as language ', async () => {
languageGetter.mockReturnValue('es-ES')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es')
})
})
describe('navigator language is "fr-FR" (not supported)', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue('es-ES')
languageGetter.mockReturnValue('fr-FR')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('English - en')
@ -101,9 +112,18 @@ describe('LanguageSwitch', () => {
})
})
describe('language "es" in store', () => {
it('shows Español as language', async () => {
wrapper.vm.$store.state.language = 'es'
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.find('button.dropdown-toggle').text()).toBe('Español - es')
})
})
describe('dropdown menu', () => {
it('has English and German as languages to choose', () => {
expect(wrapper.findAll('li')).toHaveLength(2)
expect(wrapper.findAll('li')).toHaveLength(3)
})
it('has English as first language to choose', () => {
@ -113,6 +133,10 @@ describe('LanguageSwitch', () => {
it('has German as second language to choose', () => {
expect(wrapper.findAll('li').at(1).text()).toBe('Deutsch')
})
it('has Español as second language to choose', () => {
expect(wrapper.findAll('li').at(2).text()).toBe('Español')
})
})
})

View File

@ -66,10 +66,19 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
})
describe('navigator language is "es-ES" (not supported)', () => {
describe('navigator language is "es-ES"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Español as language ', async () => {
languageGetter.mockReturnValue('es-ES')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
})
})
describe('navigator language is "fr-FR" (not supported)', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue('es-ES')
languageGetter.mockReturnValue('fr-FR')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
@ -93,9 +102,17 @@ describe('LanguageSwitch', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
})
describe('language "es" in store', () => {
it('shows Español as language', async () => {
wrapper.vm.$store.state.language = 'es'
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
})
})
describe('language menu', () => {
it('has English and German as languages to choose', () => {
expect(wrapper.findAll('span.locales')).toHaveLength(2)
it('has English, German and Español as languages to choose', () => {
expect(wrapper.findAll('span.locales')).toHaveLength(3)
})
it('has English as first language to choose', () => {
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
@ -103,6 +120,9 @@ describe('LanguageSwitch', () => {
it('has German as second language to choose', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
it('has Español as third language to choose', () => {
expect(wrapper.findAll('span.locales').at(2).text()).toBe('Español')
})
})
})

View File

@ -16,6 +16,7 @@ export default {
options: [
{ value: 'de', text: this.$t('settings.language.de') },
{ value: 'en', text: this.$t('settings.language.en') },
{ value: 'es', text: this.$t('settings.language.es') },
],
}
},

View File

@ -17,7 +17,7 @@ const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
hasElopage: false,
hasElopage: true,
isAdmin: true,
},
},
@ -39,15 +39,17 @@ describe('Navbar', () => {
expect(wrapper.find('div.component-navbar').exists()).toBeTruthy()
})
describe('navigation Navbar', () => {
describe('navigation Navbar (general elements)', () => {
it('has .navbar-brand in the navbar', () => {
expect(wrapper.find('.navbar-brand').exists()).toBeTruthy()
})
it('has b-navbar-toggle in the navbar', () => {
expect(wrapper.find('.navbar-toggler').exists()).toBeTruthy()
})
it('has ten b-nav-item in the navbar', () => {
expect(wrapper.findAll('.nav-item')).toHaveLength(10)
expect(wrapper.findAll('.nav-item')).toHaveLength(11)
})
it('has first nav-item "amount GDD" in navbar', () => {
@ -57,31 +59,57 @@ describe('Navbar', () => {
it('has first 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', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toEqual('navigation.send')
})
it('has first nav-item "navigation.transactions" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.transactions')
})
it('has first nav-item "navigation.profile" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.profile')
it('has first nav-item "navigation.transactions" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.community')
})
it('has first nav-item "navigation.profile" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(7).text()).toEqual('navigation.profile')
})
})
describe('navigation Navbar (user has an elopage account)', () => {
it('has a link to the members area', () => {
expect(wrapper.findAll('.nav-item').at(7).text()).toContain('navigation.members_area')
expect(wrapper.findAll('.nav-item').at(7).find('a').attributes('href')).toBe(
expect(wrapper.findAll('.nav-item').at(8).text()).toContain('navigation.members_area')
expect(wrapper.findAll('.nav-item').at(8).find('a').attributes('href')).toBe(
'https://elopage.com',
)
})
it('has first nav-item "navigation.admin_area" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(9).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')
})
})
describe('navigation Navbar (user has no elopage account)', () => {
beforeAll(() => {
mocks.$store.state.hasElopage = false
wrapper = Wrapper()
})
it('has first 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.logout" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(9).text()).toEqual('navigation.logout')
})
})
})
describe('check watch visible true', () => {
beforeEach(async () => {
await wrapper.setProps({ visible: true })

View File

@ -52,17 +52,18 @@
<b-icon icon="layout-text-sidebar-reverse" aria-hidden="true"></b-icon>
{{ $t('navigation.transactions') }}
</b-nav-item>
<b-nav-item to="/community" class="mb-3">
<b-icon icon="people" aria-hidden="true"></b-icon>
{{ $t('navigation.community') }}
</b-nav-item>
<b-nav-item to="/profile" class="mb-3">
<b-icon icon="gear" aria-hidden="true"></b-icon>
{{ $t('navigation.profile') }}
</b-nav-item>
<br />
<b-nav-item :href="elopageUri" class="mb-3" target="_blank">
<b-nav-item v-if="$store.state.hasElopage" :href="elopageUri" class="mb-3" target="_blank">
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
{{ $t('navigation.members_area') }}
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">
{{ $t('math.exclaim') }}
</b-badge>
</b-nav-item>
<b-nav-item class="mb-3" v-if="$store.state.isAdmin" @click="$emit('admin')">
<b-icon icon="shield-check" aria-hidden="true"></b-icon>

View File

@ -27,15 +27,12 @@ describe('Sidebar', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#component-sidebar').exists()).toBeTruthy()
})
describe('navigation Navbar', () => {
it('has seven b-nav-item in the navbar', () => {
expect(wrapper.findAll('.nav-item')).toHaveLength(7)
})
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')
})
@ -47,16 +44,49 @@ describe('Sidebar', () => {
it('has first nav-item "navigation.transactions" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions')
})
it('has first nav-item "navigation.community" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(3).text()).toContain('navigation.community')
})
it('has first nav-item "navigation.profile" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('navigation.profile')
expect(wrapper.findAll('.nav-item').at(4).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(8)
})
it('has a link to the members area', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('navigation.members_area')
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe('#')
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.members_area')
expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('#')
})
it('has first nav-item "navigation.admin_area" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).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')
})
})
describe('navigation Navbar (user has no elopage account)', () => {
beforeAll(() => {
mocks.$store.state.hasElopage = false
wrapper = Wrapper()
})
it('has seven b-nav-item in the navbar', () => {
expect(wrapper.findAll('.nav-item')).toHaveLength(7)
})
it('has first nav-item "navigation.admin_area" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toEqual('navigation.admin_area')
})
it('has first nav-item "navigation.logout" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(6).text()).toEqual('navigation.logout')
})

View File

@ -16,6 +16,10 @@
<b-icon icon="layout-text-sidebar-reverse" aria-hidden="true"></b-icon>
{{ $t('navigation.transactions') }}
</b-nav-item>
<b-nav-item to="/community" class="mb-3">
<b-icon icon="people" aria-hidden="true"></b-icon>
{{ $t('navigation.community') }}
</b-nav-item>
<b-nav-item to="/profile" class="mb-3">
<b-icon icon="gear" aria-hidden="true"></b-icon>
{{ $t('navigation.profile') }}
@ -23,12 +27,14 @@
</b-nav>
<hr />
<b-nav vertical class="w-100">
<b-nav-item class="mb-3" :href="elopageUri" target="_blank">
<b-nav-item
v-if="$store.state.hasElopage"
class="mb-3"
:href="elopageUri"
target="_blank"
>
<b-icon icon="link45deg" aria-hidden="true"></b-icon>
{{ $t('navigation.members_area') }}
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">
{{ $t('math.exclaim') }}
</b-badge>
</b-nav-item>
<b-nav-item class="mb-3" v-if="$store.state.isAdmin" @click="$emit('admin')">
<b-icon icon="shield-check" aria-hidden="true"></b-icon>

View File

@ -18,17 +18,17 @@
<b-icon icon="three-dots-vertical"></b-icon>
</template>
<b-dropdown-item v-if="validLink" class="test-copy-link" @click="copy">
<b-dropdown-item v-if="validLink" class="test-copy-link" @click="copyLink">
<b-icon icon="clipboard"></b-icon>
{{ $t('gdd_per_link.copy') }}
{{ $t('gdd_per_link.copy-link') }}
</b-dropdown-item>
<b-dropdown-item
v-if="validLink"
class="test-copy-text pt-3"
@click="copyLinkWithText()"
@click="copyLinkWithText"
>
<b-icon icon="clipboard-plus"></b-icon>
{{ $t('gdd_per_link.copy-with-text') }}
{{ $t('gdd_per_link.copy-link-with-text') }}
</b-dropdown-item>
<b-dropdown-item
v-if="validLink"
@ -76,9 +76,11 @@ import MemoRow from '../TransactionRows/MemoRow'
import DateRow from '../TransactionRows/DateRow'
import DecayRow from '../TransactionRows/DecayRow'
import FigureQrCode from '@/components/QrCode/FigureQrCode.vue'
import { copyLinks } from '../../mixins/copyLinks'
export default {
name: 'TransactionLink',
mixins: [copyLinks],
components: {
TypeIcon,
AmountAndNameRow,
@ -88,43 +90,10 @@ export default {
FigureQrCode,
},
props: {
amount: { type: String, required: true },
link: { type: String, required: true },
holdAvailableAmount: { type: String, required: true },
id: { type: Number, required: true },
memo: { type: String, required: true },
validUntil: { type: String, required: true },
},
methods: {
copy() {
navigator.clipboard
.writeText(this.link)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
})
.catch(() => {
this.$bvModal.show('modalPopoverCopyError' + this.id)
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
copyLinkWithText() {
navigator.clipboard
.writeText(
`${this.link}
${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido.
"${this.memo}"
${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', {
date: this.$d(new Date(this.validUntil), 'short'),
})}`,
)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied'))
})
.catch(() => {
this.$bvModal.show('modalPopoverCopyError' + this.id)
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
deleteLink() {
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => {
if (value)

View File

@ -74,6 +74,9 @@ export const createTransactionLink = gql`
mutation($amount: Decimal!, $memo: String!) {
createTransactionLink(amount: $amount, memo: $memo) {
link
amount
memo
validUntil
}
}
`
@ -89,3 +92,33 @@ export const redeemTransactionLink = gql`
redeemTransactionLink(code: $code)
}
`
export const createContribution = gql`
mutation($creationDate: String!, $memo: String!, $amount: Decimal!) {
createContribution(creationDate: $creationDate, memo: $memo, amount: $amount) {
amount
memo
}
}
`
export const updateContribution = gql`
mutation($contributionId: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
updateContribution(
contributionId: $contributionId
amount: $amount
memo: $memo
creationDate: $creationDate
) {
id
amount
memo
}
}
`
export const deleteContribution = gql`
mutation($id: Int!) {
deleteContribution(id: $id)
}
`

View File

@ -162,3 +162,50 @@ export const listTransactionLinks = gql`
}
}
`
export const listContributions = gql`
query(
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$filterConfirmed: Boolean = false
) {
listContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
filterConfirmed: $filterConfirmed
) {
contributionCount
contributionList {
id
amount
memo
createdAt
contributionDate
confirmedAt
confirmedBy
deletedAt
}
}
}
`
export const listAllContributions = gql`
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
contributionCount
contributionList {
id
firstName
lastName
amount
memo
createdAt
contributionDate
confirmedAt
confirmedBy
}
}
}
`

View File

@ -3,6 +3,7 @@ import VueI18n from 'vue-i18n'
import en from 'vee-validate/dist/locale/en'
import de from 'vee-validate/dist/locale/de'
import es from 'vee-validate/dist/locale/es'
Vue.use(VueI18n)
@ -26,6 +27,12 @@ function loadLocaleMessages() {
...messages[locale],
}
}
if (locale === 'es') {
messages[locale] = {
validations: es,
...messages[locale],
}
}
}
})
return messages
@ -58,6 +65,19 @@ const numberFormats = {
useGrouping: false,
},
},
es: {
decimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
ungroupedDecimal: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: false,
},
},
}
const dateTimeFormats = {
@ -75,6 +95,19 @@ const dateTimeFormats = {
hour: 'numeric',
minute: 'numeric',
},
monthShort: {
month: 'short',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
monthAndYear: {
month: 'long',
year: 'numeric',
},
},
de: {
short: {
@ -90,6 +123,47 @@ const dateTimeFormats = {
hour: 'numeric',
minute: 'numeric',
},
monthShort: {
month: 'short',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
monthAndYear: {
month: 'long',
year: 'numeric',
},
},
es: {
short: {
day: 'numeric',
month: 'numeric',
year: 'numeric',
},
long: {
day: 'numeric',
month: 'long',
year: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
},
monthShort: {
month: 'short',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
monthAndYear: {
month: 'long',
year: 'numeric',
},
},
}

View File

@ -26,9 +26,38 @@
"community": "Gemeinschaft",
"continue-to-registration": "Weiter zur Registrierung",
"current-community": "Aktuelle Gemeinschaft",
"myContributions": "Meine Beiträge zum Gemeinwohl",
"other-communities": "Weitere Gemeinschaften",
"submitContribution": "Beitrag einreichen",
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
},
"contribution": {
"activity": "Tätigkeit",
"alert": {
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
"confirm": "bestätigt",
"myContributionNoteList": "Eingereichte Beiträge, die noch nicht bestätigt wurden, kannst du jederzeit bearbeiten oder löschen.",
"myContributionNoteSupport": "Es wird bald an dieser Stelle die Möglichkeit geben das ein Dialog zwischen Moderatoren und dir stattfinden kann. Solltest du jetzt Probleme haben bitte nimm Kontakt mit dem Support auf.",
"pending": "Eingereicht und wartet auf Bestätigung",
"rejected": "abgelehnt"
},
"date": "Beitrag für:",
"delete": "Beitrag löschen! Bist du sicher?",
"deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.",
"formText": {
"bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.",
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit mit Angabe der Stunden und trage einen Betrag von 20 GDD pro Stunde ein! Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
"maxGDDforMonth": "Du kannst für den ausgewählten Monat nur noch maximal {amount} GDD einreichen.",
"openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.",
"yourContribution": "Dein Beitrag zum Gemeinwohl"
},
"noDateSelected": "Wähle irgendein Datum im Monat",
"selectDate": "Wann war dein Beitrag?",
"submit": "Einreichen",
"submitted": "Der Beitrag wurde eingereicht.",
"updated": "Der Beitrag wurde geändert.",
"yourActivity": "Bitte trage eine Tätigkeit ein!"
},
"contribution-link": {
"thanksYouWith": "dankt dir mit"
},
@ -78,17 +107,18 @@
"amount": "Betrag",
"at": "am",
"cancel": "Abbrechen",
"change": "Ändern",
"check_now": "Jetzt prüfen",
"close": "Schließen",
"current_balance": "Aktueller Kontostand",
"date": "Datum",
"description": "Beschreibung",
"edit": "Bearbeiten",
"email": "E-Mail",
"firstname": "Vorname",
"from": "Von",
"generate_now": "Jetzt generieren",
"lastname": "Nachname",
"mandatoryField": "Pflichtfeld",
"memo": "Nachricht",
"message": "Nachricht",
"new_balance": "Neuer Kontostand nach Bestätigung",
@ -123,8 +153,8 @@
"GDD": "GDD",
"gdd_per_link": {
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
"copy": "kopieren",
"copy-with-text": "Link und Text kopieren",
"copy-link": "Link kopieren",
"copy-link-with-text": "Link und Text kopieren",
"created": "Der Link wurde erstellt!",
"credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!",
"decay-14-day": "Vergänglichkeit für 14 Tage",
@ -176,8 +206,8 @@
"login": "Anmeldung",
"math": {
"aprox": "~",
"asterisk": "*",
"equal": "=",
"exclaim": "!",
"minus": "",
"pipe": "|"
},
@ -193,6 +223,7 @@
},
"navigation": {
"admin_area": "Adminbereich",
"community": "Gemeinschaft",
"logout": "Abmelden",
"members_area": "Mitgliederbereich",
"overview": "Übersicht",
@ -215,6 +246,7 @@
"changeLanguage": "Sprache ändern",
"de": "Deutsch",
"en": "English",
"es": "Español",
"success": "Deine Sprache wurde erfolgreich geändert."
},
"name": {
@ -271,6 +303,7 @@
"days": "Tage",
"hours": "Stunden",
"minutes": "Minuten",
"month": "Monat",
"months": "Monate",
"seconds": "Sekunden",
"years": "Jahr"

View File

@ -26,9 +26,38 @@
"community": "Community",
"continue-to-registration": "Continue to registration",
"current-community": "Current community",
"myContributions": "My contributions to the common good",
"other-communities": "Other communities",
"submitContribution": "Submit contribution",
"switch-to-this-community": "Switch to this community"
},
"contribution": {
"activity": "Activity",
"alert": {
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
"confirm": "confirmed",
"myContributionNoteList": "You can edit or delete entries that have not yet been confirmed at any time.",
"myContributionNoteSupport": "Soon there will be the possibility for a dialogue between moderators and you. If you have any problems now, please contact the support.",
"pending": "Submitted and waiting for confirmation",
"rejected": "deleted"
},
"date": "Contribution for:",
"delete": "Delete Contribution! Are you sure?",
"deleted": "The contribution has been deleted! But it will remain visible.",
"formText": {
"bringYourTalentsTo": "Bring your talents to the community! Your voluntary commitment will be rewarded with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
"describeYourCommunity": "Describe your community service activity with hours and enter an amount of 20 GDD per hour! After confirmation by a moderator, the amount will be credited to your account.",
"maxGDDforMonth": "You can only submit a maximum of {amount} GDD for the selected month.",
"openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.",
"yourContribution": "Your contribution to the common good"
},
"noDateSelected": "Choose any date in the month",
"selectDate": "When was your contribution?",
"submit": "Submit",
"submitted": "The contribution was submitted.",
"updated": "The contribution was changed.",
"yourActivity": "Please enter an activity!"
},
"contribution-link": {
"thanksYouWith": "thanks you with"
},
@ -78,17 +107,18 @@
"amount": "Amount",
"at": "at",
"cancel": "Cancel",
"change": "Change",
"check_now": "Check now",
"close": "Close",
"current_balance": "Current Balance",
"date": "Date",
"description": "Description",
"edit": "Edit",
"email": "Email",
"firstname": "Firstname",
"from": "from",
"generate_now": "Generate now",
"lastname": "Lastname",
"mandatoryField": "mandatory field",
"memo": "Message",
"message": "Message",
"new_balance": "Account balance after confirmation",
@ -123,8 +153,8 @@
"GDD": "GDD",
"gdd_per_link": {
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
"copy": "copy",
"copy-with-text": "Copy link and text",
"copy-link": "Copy link",
"copy-link-with-text": "Copy link and text",
"created": "Link was created!",
"credit-your-gradido": "For the Gradido to be credited, click on the link!",
"decay-14-day": "Decay for 14 days",
@ -176,8 +206,8 @@
"login": "Login",
"math": {
"aprox": "~",
"asterisk": "*",
"equal": "=",
"exclaim": "!",
"minus": "",
"pipe": "|"
},
@ -193,6 +223,7 @@
},
"navigation": {
"admin_area": "Admin Area",
"community": "Community",
"logout": "Logout",
"members_area": "Members area",
"overview": "Overview",
@ -215,6 +246,7 @@
"changeLanguage": "Change language",
"de": "Deutsch",
"en": "English",
"es": "Español",
"success": "Your language has been successfully updated."
},
"name": {
@ -271,6 +303,7 @@
"days": "Days",
"hours": "Hours",
"minutes": "Minutes",
"month": "Month",
"months": "Months",
"seconds": "Seconds",
"years": "Year"

View File

@ -0,0 +1,324 @@
{
"100": "100%",
"1000thanks": "1000 Gracias, por estar con nosotros!",
"125": "125%",
"85": "85%",
"advanced-calculation": "Proyección",
"auth": {
"left": {
"dignity": "Dignidad",
"donation": "Donación",
"gratitude": "Gratitud",
"hasAccount": "Ya estas registrado?",
"hereLogin": "Regístrate aquí",
"learnMore": "Infórmate aquí …",
"oneDignity": "Damos los unos a los otros y agradecemos con Gradido.",
"oneDonation": "Eres un regalo para la comunidad. 1000 gracias por estar con nosotros.",
"oneGratitude": "Por los demás, por toda la humanidad, por la naturaleza."
},
"navbar": {
"aboutGradido": "Sobre Gradido"
}
},
"back": "Volver",
"community": {
"choose-another-community": "Escoger otra comunidad",
"community": "Comunidad",
"continue-to-registration": "Continuar con el registro",
"current-community": "Comunidad actual",
"myContributions": "Mis contribuciones al bien común",
"other-communities": "Otras comunidades",
"submitContribution": "Aportar una contribución",
"switch-to-this-community": "cambiar a esta comunidad"
},
"contribution": {
"activity": "Actividad",
"alert": {
"communityNoteList": "Aquí encontrarás todas las contribuciones enviadas y confirmadas de todos los miembros de esta comunidad.",
"confirm": "confirmado",
"myContributionNoteList": "Puedes editar o eliminar las contribuciones enviadas que aún no han sido confirmadas en cualquier momento.",
"myContributionNoteSupport": "Pronto existirá la posibilidad de que puedas dialogar con los moderadores. Si tienes algún problema ahora, ponte en contacto con el equipo de asistencia.",
"pending": "Enviado y a la espera de confirmación",
"rejected": "rechazado"
},
"date": "Contribución para:",
"delete": "Eliminar la contribución. ¿Estás seguro?",
"deleted": "¡La contribución ha sido borrada! Pero seguirá siendo visible.",
"formText": {
"bringYourTalentsTo": "¡Contribuye a la comunidad con tus talentos! Premiamos tu compromiso voluntario con 20 GDD por hora hasta un máximo de 1.000 GDD al mes.",
"describeYourCommunity": "¡Describe tu contribución al bien-común con detalles de las horas e introduce una cantidad de 20 GDD por hora! Tras la confirmación de un moderador, el importe se abonará en tu cuenta.",
"maxGDDforMonth": "Sólo puede presentar un máximo de {amount} GDD para el mes seleccionado.",
"openAmountForMonth": "Para <b>{monthAndYear}</b> aún puedes presentar <b>{creation}</b> GDD.",
"yourContribution": "Tu contribución a la comunidad."
},
"noDateSelected": "Elige cualquier fecha del mes.",
"selectDate": "¿Cuando fue tu contribución?",
"submit": "Enviar",
"submitted": "Tu contribución ha sido enviada.",
"updated": "La contribución se modificó.",
"yourActivity": "¡Por favor, introduce una actividad!"
},
"contribution-link": {
"thanksYouWith": "agradecidos con"
},
"decay": {
"before_startblock_transaction": "Esta transacción no implica disminución en su valor.",
"calculation_decay": "Cálculo de la disminución gradual del valor",
"calculation_total": "Cálculo de la suma total",
"decay": "Disminución gradual del valor",
"decay_introduced": "La disminución gradual empezó el:",
"decay_since_last_transaction": "Disminución gradual",
"last_transaction": "Transacción anterior",
"past_time": "Tiempo transcurrido",
"Starting_block_decay": "Startblock disminución gradual",
"total": "Total",
"types": {
"creation": "Creado",
"noDecay": "sin disminución gradual",
"receive": "Recibido",
"send": "Enviado"
}
},
"delete": "Eliminar",
"em-dash": "—",
"error": {
"email-already-sent": "Ya te hemos enviado un correo electrónico hace menos de 10 minutos.",
"empty-transactionlist": "Ha habido un error en la transmisión del número de sus transacciones.",
"error": "Error!",
"no-account": "Lamentablemente no hemos podido encontrar una cuenta (activada) con estos datos.",
"no-transactionlist": "Lamentablemente, hubo un error. No se ha transmitido ninguna transacción desde el servidor.",
"no-user": "No hay usuario con estas referencias.",
"session-expired": "La sesión se cerró por razones de seguridad.",
"unknown-error": "Error desconocido: "
},
"followUs": "sigue nos:",
"footer": {
"app_version": "App versión {version}",
"copyright": {
"link": "Gradido-Akademie",
"year": "© {year}"
},
"imprint": "Aviso legal",
"privacy_policy": "Protección de Datos",
"short_hash": "({shortHash})",
"whitepaper": "Whitepaper"
},
"form": {
"amount": "Importe",
"at": "am",
"cancel": "Cancelar",
"change": "Cambiar",
"check_now": "Revisar",
"close": "Cerrar",
"current_balance": "Saldo de cuenta actual",
"date": "Fecha",
"description": "Descripción",
"email": "E-Mail",
"firstname": "Nombre",
"from": "De",
"generate_now": "crear ahora",
"lastname": "Apellido",
"mandatoryField": "campo obligatorio",
"memo": "Mensaje",
"message": "Noticia",
"new_balance": "Saldo de cuenta nuevo depués de confirmación",
"no_gdd_available": "No dispones de GDD para enviar.",
"password": "Contraseña",
"passwordRepeat": "Repetir contraseña",
"password_new": "contraseña nueva",
"password_new_repeat": "Repetir contraseña nueva",
"password_old": "contraseña antigua",
"recipient": "Destinatario",
"reset": "Restablecer",
"save": "Guardar",
"scann_code": "<strong>QR Code Scanner</strong> - Escanea el código QR de tu pareja",
"sender": "Remitente",
"send_check": "Confirma tu transacción. Por favor revisa toda la información nuevamente!",
"send_now": "Enviar ahora",
"send_transaction_error": "Desafortunadamente, la transacción no se pudo ejecutar!",
"send_transaction_success": "Su transacción fue ejecutada con éxito",
"sorry": "Disculpa",
"thx": "Gracias",
"time": "Tiempo",
"to": "hasta",
"to1": "para",
"validation": {
"gddSendAmount": "El campo {_field_} debe ser un número entre {min} y {max} con un máximo de dos decimales",
"is-not": "No es posible transferirte Gradidos a ti mismo",
"usernmae-regex": "El nombre de usuario debe comenzar con una letra seguida de al menos dos caracteres alfanuméricos.",
"usernmae-unique": "Este nombre de usuario ya está adjudicado."
},
"your_amount": "Tu importe"
},
"GDD": "GDD",
"gdd_per_link": {
"choose-amount": "Selecciona una cantidad que te gustaría enviar a través de un enlace. También puedes ingresar un mensaje. Cuando haces clic en 'Generar ahora', se crea un enlace que puedes enviar.",
"copy-link": "Copiar enlace",
"copy-link-with-text": "Copiar texto y enlace",
"created": "El enlace ha sido creado",
"credit-your-gradido": "Para que se te acrediten los Gradidos, haz clic en el enlace!",
"decay-14-day": "Disminución gradual por 14 días",
"delete-the-link": "Eliminar el enlace?",
"deleted": "El enlace ha sido eliminado!",
"expiredOn": "Vencido el:",
"has-account": "Ya tienes una cuenta Gradido?",
"header": "Transferir Gradidos por medio de un enlace",
"isFree": "Gradido es gratis en todo el mundo.",
"link-and-text-copied": "El enlace y su mensaje se han copiado en el portapapeles. Ahora puedes ponerlo en un correo electrónico o mensaje.",
"link-copied": "El enlace se ha copiado en el portapapeles. Ahora puedes pegarlo en un correo electrónico o mensaje.",
"link-deleted": "El enlace se eliminó el {date}.",
"link-expired": "El enlace ya no es válido. La validez expiró el {date}.",
"link-overview": "Resumen de enlaces",
"links_count": "Enlaces activos",
"links_sum": "Enlaces abiertos y códigos QR",
"no-account": "Aún no tienes una cuenta de Gradido?",
"no-redeem": "No puedes canjear tu propio enlace!",
"not-copied": "¡Desafortunadamente, su dispositivo no permite copiar! Copie el enlace manualmente!",
"redeem": "Canjear",
"redeem-text": "¿Quieres canjear el importe ahora?",
"redeemed": "¡Canjeado con éxito! Tu cuenta ha sido acreditada con {n} GDD.",
"redeemed-at": "El enlace ya se canjeó el {date}.",
"redeemed-title": "canjeado",
"to-login": "iniciar sesión",
"to-register": "Registre una nueva cuenta.",
"validUntil": "Válido hasta",
"validUntilDate": "El enlace es válido hasta el {date} ."
},
"gdt": {
"calculation": "Cálculo del Gradido Transform",
"contribution": "Importe",
"conversion": "Conversión",
"conversion-gdt-euro": "Conversión Euro / Gradido Transform (GDT)",
"credit": "Abono",
"factor": "Factor",
"formula": "Formula de cálculo",
"funding": "Las donaciones",
"gdt": "Gradido Transform",
"gdt-received": "Gradido Transform (GDT) recibido",
"no-transactions": "Aún no tienes un Gradido Transform (GDT).",
"not-reachable": "No es posible acceder al servidor GDT.",
"publisher": "Tu nuevo miembro referido ha pagado la cuota",
"raise": "Aumento",
"recruited-member": "Miembro invitado"
},
"language": "Idioma",
"link-load": "recargar el último enlace |recargar los últimos {n} enlaces | descargar más {n} enlaces",
"login": "iniciar sesión",
"math": {
"aprox": "~",
"asterisk": "*",
"equal": "=",
"minus": "",
"pipe": "|"
},
"message": {
"activateEmail": "Tu cuenta aún no ha sido activada. Por favor revisa tu correo electrónico y haz clic en el enlace de activación o solicita uno nuevo enlace de activación a través de la página restablecer contraseña.",
"checkEmail": "Tu correo electrónico ha sido verificado con éxito. Puedes registrarte ahora.",
"email": "Te hemos enviado un correo electrónico.",
"errorTitle": "Atención!",
"register": "Ya estás registrado, por favor revisa tu correo electrónico y haz clic en el enlace de activación.",
"reset": "Tu contraseña ha sido cambiada.",
"title": "Gracias!",
"unsetPassword": "Tu contraseña aún no ha sido configurada. Por favor reinícialo."
},
"navigation": {
"admin_area": "Área de administración",
"community": "Comunidad",
"logout": "Salir",
"members_area": "Área de afiliados",
"overview": "Resumen",
"profile": "Mi Perfil",
"send": "Enviar",
"support": "Soporte",
"transactions": "Transacciones"
},
"qrCode": "Código QR",
"send_gdd": "Enviar GDD",
"send_per_link": "Enviar GDD mediante un enlace",
"session": {
"extend": "Permanecer en sesión iniciada",
"lightText": "Si no has realizado ninguna acción durante más de 10 minutos, se cerrará tu sesión por razones de seguridad.",
"logoutIn": "Cerrar sesión en ",
"warningText": "Aún estas?"
},
"settings": {
"language": {
"changeLanguage": "Cambiar idioma",
"de": "Deutsch",
"en": "English",
"es": "Español",
"success": "Tu idioma ha sido cambiado con éxito."
},
"name": {
"change-name": "Cambiar nombre",
"change-success": "Tu nombre ha sido cambiado con éxito."
},
"newsletter": {
"newsletter": "Informaciones por correo electrónico",
"newsletterFalse": "No recibirás informaciones por correo electrónico.",
"newsletterTrue": "Recibirás informaciones por correo electrónico."
},
"password": {
"change-password": "Cambiar contraseña",
"forgot_pwd": "Olvide la contraseña?",
"resend_subtitle": "Su enlace de activación ha caducado. Puedes solicitar uno nuevo aquí.",
"reset": "Restablecer contraseña",
"reset-password": {
"text": "Ahora introduce una nueva contraseña con la que quieras acceder a tu cuenta de Gradido en el futuro.."
},
"send_now": "Enviar",
"set": "Establecer contraseña",
"set-password": {
"text": "Ahora guarda tu nueva contraseña, que podrás utilizar para acceder a tu cuenta de Gradido en el futuro."
},
"subtitle": "Si has olvidado tu contraseña, puedes restablecerla aquí."
}
},
"signin": "Iniciar sesión",
"signup": "Registrarse",
"site": {
"forgotPassword": {
"heading": "Por favor, introduce la dirección de correo electrónico con la que estas registrado en Gradido."
},
"login": {
"heading": "Inicia sesión con tus datos de acceso. Manténlos seguros en todo momento!"
},
"resetPassword": {
"heading": "Por favor, introduce tu contraseña y repítela."
},
"signup": {
"agree": "Acepto la <a href='https://gradido.net/de/datenschutz/' target='_blank' >Política de privacidad</a>.",
"dont_match": "Las contraseñas no coinciden.",
"heading": "Regístrate introduciendo todos los datos completos y en los campos correctos.",
"lowercase": "Se requiere una letra minúscula.",
"minimum": "Al menos 8 caracteres.",
"no-whitespace": "Sin espacios ni tabulaciones.",
"one_number": "Se requiere un número.",
"special-char": "Caracteres especiales requeridos (por ejemplo, _ o &)",
"uppercase": "Letra mayúscula requerida."
}
},
"success": "Lo lograste",
"time": {
"days": "Días",
"hours": "Horas",
"minutes": "Minutos",
"month": "Mes",
"months": "Meses",
"seconds": "Segundos",
"years": "Año"
},
"transaction": {
"gdd-text": "Transacciones Gradido",
"gdt-text": "Transacciones GradidoTransform ",
"nullTransactions": "Todavía no tienes ninguna transacción en tu cuenta.",
"receiverDeleted": "La cuenta del destinatario ha sido eliminada.",
"receiverNotFound": "Destinatario no encontrado",
"show_all": "Ver todas las transacciones de <strong>{count}</strong>"
},
"transaction-link": {
"send_you": "te envía"
},
"via_link": "atraves de un enlace",
"welcome": "Bienvenido a la comunidad."
}

View File

@ -11,6 +11,12 @@ const locales = [
iso: 'de-DE',
enabled: true,
},
{
name: 'Español',
code: 'es',
iso: 'es-ES',
enabled: true,
},
]
export default locales

View File

@ -0,0 +1,44 @@
export const copyLinks = {
props: {
link: { type: String, required: true },
amount: { type: String, required: true },
memo: { type: String, required: true },
validUntil: { type: String, required: true },
},
data() {
return {
canCopyLink: true,
}
},
methods: {
copyLink() {
navigator.clipboard
.writeText(this.link)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
})
.catch(() => {
this.canCopyLink = false
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
copyLinkWithText() {
navigator.clipboard
.writeText(
`${this.link}
${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido.
"${this.memo}"
${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', {
date: this.$d(new Date(this.validUntil), 'short'),
})}`,
)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied'))
})
.catch(() => {
this.canCopyLink = false
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
},
}

View File

@ -0,0 +1,408 @@
import { mount } from '@vue/test-utils'
import Community from './Community'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
const localVue = global.localVue
const mockStoreDispach = jest.fn()
const apolloQueryMock = jest.fn()
const apolloMutationMock = jest.fn()
describe('Community', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
mutate: apolloMutationMock,
},
$store: {
dispatch: mockStoreDispach,
state: {
creation: ['1000', '1000', '1000'],
},
},
$i18n: {
locale: 'en',
},
}
const Wrapper = () => {
return mount(Community, {
localVue,
mocks,
})
}
describe('mount', () => {
beforeEach(() => {
apolloQueryMock.mockResolvedValue({
data: {
listContributions: {
contributionList: [
{
id: 1555,
amount: '200',
memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel',
createdAt: '2022-07-15T08:47:06.000Z',
deletedAt: null,
confirmedBy: null,
confirmedAt: null,
},
],
contributionCount: 1,
},
listAllContributions: {
contributionList: [
{
id: 1555,
amount: '200',
memo: 'Fleisig, fleisig am Arbeiten mein lieber Freund, 50 Zeichen sind viel',
createdAt: '2022-07-15T08:47:06.000Z',
deletedAt: null,
confirmedBy: null,
confirmedAt: null,
},
{
id: 1556,
amount: '400',
memo: 'Ein anderer lieber Freund ist auch sehr felißig am Arbeiten!!!!',
createdAt: '2022-07-16T08:47:06.000Z',
deletedAt: null,
confirmedBy: null,
confirmedAt: null,
},
],
contributionCount: 2,
},
},
})
wrapper = Wrapper()
})
it('has a DIV .community-page', () => {
expect(wrapper.find('div.community-page').exists()).toBe(true)
})
describe('tabs', () => {
it('has three tabs', () => {
expect(wrapper.findAll('div[role="tabpanel"]')).toHaveLength(3)
})
it('has first tab active by default', () => {
expect(wrapper.findAll('div[role="tabpanel"]').at(0).classes('active')).toBe(true)
})
})
describe('API calls after creation', () => {
it('emits update transactions', () => {
expect(wrapper.emitted('update-transactions')).toEqual([[0]])
})
it('queries list of own contributions', () => {
expect(apolloQueryMock).toBeCalledWith({
fetchPolicy: 'no-cache',
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
},
})
})
it('queries list of all contributions', () => {
expect(apolloQueryMock).toBeCalledWith({
fetchPolicy: 'no-cache',
query: listAllContributions,
variables: {
currentPage: 1,
pageSize: 25,
},
})
})
describe('server response is error', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloQueryMock.mockRejectedValue({ message: 'Ups' })
wrapper = Wrapper()
})
it('toasts two errors', () => {
expect(toastErrorSpy).toBeCalledTimes(2)
expect(toastErrorSpy).toBeCalledWith('Ups')
})
})
})
describe('set contrubtion', () => {
describe('with success', () => {
const now = new Date().toISOString()
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockResolvedValue({
data: {
createContribution: true,
},
})
await wrapper.setData({
form: {
id: null,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
})
await wrapper.find('form').trigger('submit')
})
it('calls the create contribution mutation', () => {
expect(apolloMutationMock).toBeCalledWith({
fetchPolicy: 'no-cache',
mutation: createContribution,
variables: {
creationDate: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
})
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('contribution.submitted')
})
it('updates the contribution list', () => {
expect(apolloQueryMock).toBeCalledWith({
fetchPolicy: 'no-cache',
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
},
})
})
it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({
query: verifyLogin,
fetchPolicy: 'network-only',
})
})
it('set all data to the default values)', () => {
expect(wrapper.vm.form.id).toBe(null)
expect(wrapper.vm.form.date).toBe('')
expect(wrapper.vm.form.memo).toBe('')
expect(wrapper.vm.form.amount).toBe('')
})
})
describe('with error', () => {
const now = new Date().toISOString()
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockRejectedValue({
message: 'Ouch!',
})
await wrapper.setData({
form: {
id: null,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '200',
},
})
await wrapper.find('form').trigger('submit')
})
it('toasts the error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
})
describe('update contrubtion', () => {
describe('with success', () => {
const now = new Date().toISOString()
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockResolvedValue({
data: {
updateContribution: true,
},
})
await wrapper
.findComponent({ name: 'ContributionForm' })
.vm.$emit('update-contribution', {
id: 2,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '400',
})
})
it('calls the update contribution mutation', () => {
expect(apolloMutationMock).toBeCalledWith({
fetchPolicy: 'no-cache',
mutation: updateContribution,
variables: {
contributionId: 2,
creationDate: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '400',
},
})
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('contribution.updated')
})
it('updates the contribution list', () => {
expect(apolloQueryMock).toBeCalledWith({
fetchPolicy: 'no-cache',
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
},
})
})
it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({
query: verifyLogin,
fetchPolicy: 'network-only',
})
})
it('set all data to the default values)', () => {
expect(wrapper.vm.form.id).toBe(null)
expect(wrapper.vm.form.date).toBe('')
expect(wrapper.vm.form.memo).toBe('')
expect(wrapper.vm.form.amount).toBe('')
})
})
describe('with error', () => {
const now = new Date().toISOString()
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockRejectedValue({
message: 'Oh No!',
})
await wrapper
.findComponent({ name: 'ContributionForm' })
.vm.$emit('update-contribution', {
id: 2,
date: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '400',
})
})
it('toasts the error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh No!')
})
})
})
describe('delete contribution', () => {
let contributionListComponent
beforeEach(async () => {
await wrapper.setData({ tabIndex: 1 })
contributionListComponent = await wrapper.findComponent({ name: 'ContributionList' })
})
describe('with success', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockResolvedValue({
data: {
deleteContribution: true,
},
})
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
})
it('calls the API', () => {
expect(apolloMutationMock).toBeCalledWith({
fetchPolicy: 'no-cache',
mutation: deleteContribution,
variables: {
id: 2,
},
})
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('contribution.deleted')
})
it('updates the contribution list', () => {
expect(apolloQueryMock).toBeCalledWith({
fetchPolicy: 'no-cache',
query: listContributions,
variables: {
currentPage: 1,
pageSize: 25,
},
})
})
it('verifies the login (to get the new creations available)', () => {
expect(apolloQueryMock).toBeCalledWith({
query: verifyLogin,
fetchPolicy: 'network-only',
})
})
})
describe('with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutationMock.mockRejectedValue({
message: 'Oh my god!',
})
contributionListComponent.vm.$emit('delete-contribution', { id: 2 })
})
it('toasts the error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh my god!')
})
})
})
describe('update contribution form', () => {
const now = new Date().toISOString()
beforeEach(async () => {
await wrapper.setData({ tabIndex: 1 })
await wrapper
.findComponent({ name: 'ContributionList' })
.vm.$emit('update-contribution-form', {
id: 2,
contributionDate: now,
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
amount: '400',
})
})
it('sets the form data to the new values', () => {
expect(wrapper.vm.form.id).toBe(2)
expect(wrapper.vm.form.date).toBe(now)
expect(wrapper.vm.form.memo).toBe('Mein Beitrag zur Gemeinschaft für diesen Monat ...')
expect(wrapper.vm.form.amount).toBe('400')
})
it('sets tab index back to 0', () => {
expect(wrapper.vm.tabIndex).toBe(0)
})
})
})
})

View File

@ -0,0 +1,276 @@
<template>
<div class="community-page">
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" align="center">
<b-tab :title="$t('community.submitContribution')" active>
<contribution-form
@set-contribution="setContribution"
@update-contribution="updateContribution"
v-model="form"
:updateAmount="updateAmount"
/>
</b-tab>
<b-tab :title="$t('community.myContributions')">
<div>
<b-alert show dismissible fade variant="secondary" class="text-dark">
<h4 class="alert-heading">{{ $t('community.myContributions') }}</h4>
<p>
{{ $t('contribution.alert.myContributionNoteList') }}
</p>
<ul class="h2">
<li>
<b-icon icon="bell-fill" variant="primary"></b-icon>
{{ $t('contribution.alert.pending') }}
</li>
<li>
<b-icon icon="check" variant="success"></b-icon>
{{ $t('contribution.alert.confirm') }}
</li>
<li>
<b-icon icon="x-circle" variant="danger"></b-icon>
{{ $t('contribution.alert.rejected') }}
</li>
</ul>
<hr />
<p class="mb-0">
{{ $t('contribution.alert.myContributionNoteSupport') }}
</p>
</b-alert>
</div>
<contribution-list
:items="items"
@update-list-contributions="updateListContributions"
@update-contribution-form="updateContributionForm"
@delete-contribution="deleteContribution"
:contributionCount="contributionCount"
:showPagination="true"
:pageSize="pageSize"
/>
</b-tab>
<b-tab :title="$t('navigation.community')">
<b-alert show dismissible fade variant="secondary" class="text-dark">
<h4 class="alert-heading">{{ $t('navigation.community') }}</h4>
<p>
{{ $t('contribution.alert.communityNoteList') }}
</p>
<ul class="h2">
<li>
<b-icon icon="bell-fill" variant="primary"></b-icon>
{{ $t('contribution.alert.pending') }}
</li>
<li>
<b-icon icon="check" variant="success"></b-icon>
{{ $t('contribution.alert.confirm') }}
</li>
</ul>
</b-alert>
<contribution-list
:items="itemsAll"
@update-list-contributions="updateListAllContributions"
@update-contribution-form="updateContributionForm"
:contributionCount="contributionCountAll"
:showPagination="true"
:pageSize="pageSizeAll"
/>
</b-tab>
</b-tabs>
</div>
</div>
</template>
<script>
import ContributionForm from '@/components/Contributions/ContributionForm.vue'
import ContributionList from '@/components/Contributions/ContributionList.vue'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, verifyLogin } from '@/graphql/queries'
export default {
name: 'Community',
components: {
ContributionForm,
ContributionList,
},
data() {
return {
tabIndex: 0,
items: [],
itemsAll: [],
currentPage: 1,
pageSize: 25,
pageSizeAll: 25,
contributionCount: 0,
contributionCountAll: 0,
form: {
id: null,
date: '',
memo: '',
amount: '',
},
updateAmount: '',
}
},
methods: {
setContribution(data) {
this.$apollo
.mutate({
fetchPolicy: 'no-cache',
mutation: createContribution,
variables: {
creationDate: data.date,
memo: data.memo,
amount: data.amount,
},
})
.then((result) => {
this.toastSuccess(this.$t('contribution.submitted'))
this.updateListContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.updateListAllContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.verifyLogin()
})
.catch((err) => {
this.toastError(err.message)
})
},
updateContribution(data) {
this.$apollo
.mutate({
fetchPolicy: 'no-cache',
mutation: updateContribution,
variables: {
contributionId: data.id,
creationDate: data.date,
memo: data.memo,
amount: data.amount,
},
})
.then((result) => {
this.toastSuccess(this.$t('contribution.updated'))
this.updateListContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.updateListAllContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.verifyLogin()
})
.catch((err) => {
this.toastError(err.message)
})
},
deleteContribution(data) {
this.$apollo
.mutate({
fetchPolicy: 'no-cache',
mutation: deleteContribution,
variables: {
id: data.id,
},
})
.then((result) => {
this.toastSuccess(this.$t('contribution.deleted'))
this.updateListContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.updateListAllContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.verifyLogin()
})
.catch((err) => {
this.toastError(err.message)
})
},
updateListAllContributions(pagination) {
this.$apollo
.query({
fetchPolicy: 'no-cache',
query: listAllContributions,
variables: {
currentPage: pagination.currentPage,
pageSize: pagination.pageSize,
},
})
.then((result) => {
const {
data: { listAllContributions },
} = result
this.contributionCountAll = listAllContributions.contributionCount
this.itemsAll = listAllContributions.contributionList
})
.catch((err) => {
this.toastError(err.message)
})
},
updateListContributions(pagination) {
this.$apollo
.query({
fetchPolicy: 'no-cache',
query: listContributions,
variables: {
currentPage: pagination.currentPage,
pageSize: pagination.pageSize,
},
})
.then((result) => {
const {
data: { listContributions },
} = result
this.contributionCount = listContributions.contributionCount
this.items = listContributions.contributionList
})
.catch((err) => {
this.toastError(err.message)
})
},
verifyLogin() {
this.$apollo
.query({
query: verifyLogin,
fetchPolicy: 'network-only',
})
.then((result) => {
const {
data: { verifyLogin },
} = result
this.$store.dispatch('login', verifyLogin)
})
.catch(() => {
this.$emit('logout')
})
},
updateContributionForm(item) {
this.form.id = item.id
this.form.date = item.contributionDate
this.form.memo = item.memo
this.form.amount = item.amount
this.updateAmount = item.amount
this.tabIndex = 0
},
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)
},
},
created() {
// verifyLogin is important at this point so that creation is updated on reload if they are deleted in a session in the admin area.
this.verifyLogin()
this.updateListContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.updateListAllContributions({
currentPage: this.currentPage,
pageSize: this.pageSize,
})
this.updateTransactions(0)
},
}
</script>

View File

@ -25,9 +25,11 @@ describe('Send', () => {
const mocks = {
$t: jest.fn((t) => t),
$n: jest.fn((n) => String(n)),
$d: jest.fn((d) => d),
$store: {
state: {
email: 'sender@example.org',
firstName: 'Testy',
},
},
$apollo: {
@ -160,11 +162,15 @@ describe('Send', () => {
})
describe('transaction form link', () => {
const now = new Date().toISOString()
beforeEach(async () => {
apolloMutationMock.mockResolvedValue({
data: {
createTransactionLink: {
link: 'http://localhost/redeem/0123456789',
amount: '56.78',
memo: 'Make the best of the link!',
validUntil: now,
},
},
})
@ -228,18 +234,64 @@ describe('Send', () => {
navigator.clipboard = navigatorClipboard
})
describe('copy with success', () => {
describe('copy link with success', () => {
beforeEach(async () => {
navigatorClipboardMock.mockResolvedValue()
await wrapper.findAll('button').at(0).trigger('click')
await wrapper.findAll('button').at(1).trigger('click')
})
it('should call clipboard.writeText', () => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'http://localhost/redeem/0123456789',
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied')
})
})
describe('copy with error', () => {
describe('copy link with error', () => {
beforeEach(async () => {
navigatorClipboardMock.mockRejectedValue()
await wrapper.findAll('button').at(1).trigger('click')
})
it('toasts error message', () => {
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
})
})
})
describe('copy link and text with success', () => {
const navigatorClipboard = navigator.clipboard
beforeAll(() => {
delete navigator.clipboard
navigator.clipboard = { writeText: navigatorClipboardMock }
})
afterAll(() => {
navigator.clipboard = navigatorClipboard
})
describe('copy link and text with success', () => {
beforeEach(async () => {
navigatorClipboardMock.mockResolvedValue()
await wrapper.findAll('button').at(0).trigger('click')
})
it('should call clipboard.writeText', () => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'http://localhost/redeem/0123456789\n' +
'Testy transaction-link.send_you 56.78 Gradido.\n' +
'"Make the best of the link!"\n' +
'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate',
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied')
})
})
describe('copy link and text with error', () => {
beforeEach(async () => {
navigatorClipboardMock.mockRejectedValue()
await wrapper.findAll('button').at(0).trigger('click')
@ -253,7 +305,7 @@ describe('Send', () => {
describe('close button click', () => {
beforeEach(async () => {
await wrapper.findAll('button').at(2).trigger('click')
await wrapper.findAll('button').at(3).trigger('click')
})
it('Shows the TransactionForm', () => {

View File

@ -41,7 +41,13 @@
></transaction-result-send-error>
</template>
<template #transactionResultLink>
<transaction-result-link :link="link" @on-reset="onReset"></transaction-result-link>
<transaction-result-link
:link="link"
:amount="amount"
:memo="memo"
:validUntil="validUntil"
@on-reset="onReset"
></transaction-result-link>
</template>
</gdd-send>
<hr />
@ -144,7 +150,15 @@ export default {
})
.then((result) => {
this.$emit('set-tunneled-email', null)
this.link = result.data.createTransactionLink.link
const {
data: {
createTransactionLink: { link, amount, memo, validUntil },
},
} = result
this.link = link
this.amount = amount
this.memo = memo
this.validUntil = validUntil
this.transactionData = { ...EMPTY_TRANSACTION_DATA }
this.currentTransactionStep = TRANSACTION_STEPS.transactionResultLink
this.updateTransactions({})

View File

@ -109,7 +109,7 @@ export default {
return this.$route.params.code.search(/^CL-/) === 0
},
itemType() {
// link wurde gelöscht: am, von
// link is deleted: at, from
if (this.linkData.deletedAt) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.redeemedBoxText = this.$t('gdd_per_link.link-deleted', {

View File

@ -50,7 +50,7 @@ describe('router', () => {
})
it('has sixteen routes defined', () => {
expect(routes).toHaveLength(16)
expect(routes).toHaveLength(17)
})
describe('overview', () => {
@ -75,6 +75,17 @@ 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('profile', () => {
it('requires authorization', () => {
expect(routes.find((r) => r.path === '/profile').meta.requiresAuth).toBeTruthy()

View File

@ -38,6 +38,13 @@ const routes = [
requiresAuth: true,
},
},
{
path: '/community',
component: () => import('@/pages/Community.vue'),
meta: {
requiresAuth: true,
},
},
{
path: '/login/:code?',
component: () => import('@/pages/Login.vue'),

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "1.10.1",
"version": "1.11.0",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",