mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 503-Transaction-List-Pagination-Pages-clickable
This commit is contained in:
commit
6c953af2f3
@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
|
||||
import CreationTransactionList from './CreationTransactionList.vue'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -46,11 +46,11 @@ const propsData = {
|
||||
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
||||
}
|
||||
|
||||
describe('CreationTransactionListFormular', () => {
|
||||
describe('CreationTransactionList', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CreationTransactionListFormular, { localVue, mocks, propsData })
|
||||
return mount(CreationTransactionList, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="component-creation-transaction-list">
|
||||
{{ $t('transactionlist.title') }}
|
||||
<div class="h3">{{ $t('transactionlist.title') }}</div>
|
||||
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||
</div>
|
||||
</template>
|
||||
129
admin/src/components/Tables/OpenCreationsTable.spec.js
Normal file
129
admin/src/components/Tables/OpenCreationsTable.spec.js
Normal file
@ -0,0 +1,129 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import OpenCreationsTable from './OpenCreationsTable.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({})
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
{
|
||||
id: 4,
|
||||
firstName: 'Bob',
|
||||
lastName: 'der Baumeister',
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 300,
|
||||
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
moderator: 1,
|
||||
creation: [700, 1000, 1000],
|
||||
__typename: 'PendingCreation',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 210,
|
||||
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
moderator: 1,
|
||||
creation: [790, 1000, 1000],
|
||||
__typename: 'PendingCreation',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
firstName: 'Stephen',
|
||||
lastName: 'Hawking',
|
||||
email: 'stephen@hawking.uk',
|
||||
amount: 330,
|
||||
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||
date: '2022-01-01T00:00:00.000Z',
|
||||
moderator: 1,
|
||||
creation: [670, 1000, 1000],
|
||||
__typename: 'PendingCreation',
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{ key: 'bookmark', label: 'delete' },
|
||||
{ key: 'email', label: 'e_mail' },
|
||||
{ key: 'firstName', label: 'firstname' },
|
||||
{ key: 'lastName', label: 'lastname' },
|
||||
{
|
||||
key: 'amount',
|
||||
label: 'creation',
|
||||
formatter: (value) => {
|
||||
return value + ' GDD'
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: 'text' },
|
||||
{
|
||||
key: 'date',
|
||||
label: 'date',
|
||||
formatter: (value) => {
|
||||
return value
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: 'moderator' },
|
||||
{ key: 'edit_creation', label: 'edit' },
|
||||
{ key: 'confirm', label: 'save' },
|
||||
],
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 0,
|
||||
name: 'test moderator',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('OpenCreationsTable', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(OpenCreationsTable, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class .open-creations-table', () => {
|
||||
expect(wrapper.find('div.open-creations-table').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has a table with three rows', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
|
||||
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('show edit details', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tr').at(1).find('.bi-pencil-square').trigger('click')
|
||||
})
|
||||
|
||||
it.skip('has a component element with name EditCreationFormular', () => {
|
||||
expect(wrapper.findComponent({ name: 'EditCreationFormular' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it.skip('renders the component component-edit-creation-formular', () => {
|
||||
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="component-open-creations-table">
|
||||
<div class="open-creations-table">
|
||||
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button
|
||||
|
||||
@ -69,10 +69,9 @@
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<creation-transaction-list-formular
|
||||
v-if="!row.item.deletedAt"
|
||||
:userId="row.item.userId"
|
||||
/>
|
||||
<creation-transaction-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
|
||||
<transaction-link-list :userId="row.item.userId" />
|
||||
|
||||
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
||||
</b-card>
|
||||
</template>
|
||||
@ -82,7 +81,8 @@
|
||||
<script>
|
||||
import CreationFormular from '../CreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionListFormular from '../CreationTransactionListFormular.vue'
|
||||
import CreationTransactionList from '../CreationTransactionList.vue'
|
||||
import TransactionLinkList from '../TransactionLinkList.vue'
|
||||
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
||||
|
||||
export default {
|
||||
@ -90,7 +90,8 @@ export default {
|
||||
components: {
|
||||
CreationFormular,
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionListFormular,
|
||||
CreationTransactionList,
|
||||
TransactionLinkList,
|
||||
DeletedUserFormular,
|
||||
},
|
||||
props: {
|
||||
|
||||
140
admin/src/components/TransactionLinkList.spec.js
Normal file
140
admin/src/components/TransactionLinkList.spec.js
Normal file
@ -0,0 +1,140 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TransactionLinkList from './TransactionLinkList.vue'
|
||||
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn()
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 8,
|
||||
linkList: [
|
||||
{
|
||||
amount: '19.99',
|
||||
code: '62ef8236ace7217fbd066c5a',
|
||||
createdAt: '2022-03-24T17:43:09.000Z',
|
||||
deletedAt: null,
|
||||
holdAvailableAmount: '20.51411720068412022949',
|
||||
id: 36,
|
||||
memo: 'Kein Trick, keine Zauberrei,\nbei Gradidio sei dabei!',
|
||||
redeemedAt: null,
|
||||
validUntil: '2022-04-07T17:43:09.000Z',
|
||||
},
|
||||
{
|
||||
amount: '19.99',
|
||||
code: '2b603f36521c617fbd066cef',
|
||||
createdAt: '2022-03-24T17:43:09.000Z',
|
||||
deletedAt: null,
|
||||
holdAvailableAmount: '20.51411720068412022949',
|
||||
id: 37,
|
||||
memo: 'Kein Trick, keine Zauberrei,\nbei Gradidio sei dabei!',
|
||||
redeemedAt: null,
|
||||
validUntil: '2022-04-07T17:43:09.000Z',
|
||||
},
|
||||
{
|
||||
amount: '19.99',
|
||||
code: '0bb789b5bd5b717fbd066eb5',
|
||||
createdAt: '2022-03-24T17:43:09.000Z',
|
||||
deletedAt: '2022-03-24T17:43:09.000Z',
|
||||
holdAvailableAmount: '20.51411720068412022949',
|
||||
id: 40,
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
redeemedAt: '2022-04-07T14:43:09.000Z',
|
||||
validUntil: '2022-04-07T17:43:09.000Z',
|
||||
},
|
||||
{
|
||||
amount: '19.99',
|
||||
code: '2d4a763e516b317fbd066a85',
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
deletedAt: null,
|
||||
holdAvailableAmount: '20.51411720068412022949',
|
||||
id: 33,
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
redeemedAt: null,
|
||||
validUntil: '2022-01-15T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const propsData = {
|
||||
userId: 42,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
}
|
||||
|
||||
describe('TransactionLinkList', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(TransactionLinkList, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
userId: 42,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('has 4 items in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('has pagination buttons', () => {
|
||||
expect(wrapper.findComponent({ name: 'BPagination' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('next page', () => {
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
|
||||
})
|
||||
|
||||
it('calls the API again', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
userId: 42,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('server response with error', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
105
admin/src/components/TransactionLinkList.vue
Normal file
105
admin/src/components/TransactionLinkList.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="transaction-link-list">
|
||||
<div v-if="items.length > 0">
|
||||
<div class="h3">{{ $t('transactionlink.form_header') }}</div>
|
||||
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||
</div>
|
||||
<b-pagination
|
||||
pills
|
||||
size="lg"
|
||||
v-model="currentPage"
|
||||
:per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
></b-pagination>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
|
||||
export default {
|
||||
name: 'TransactionLinkList',
|
||||
props: {
|
||||
userId: { type: Number, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
perPage: 5,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getListTransactionLinks() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.perPage,
|
||||
userId: this.userId,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.rows = result.data.listTransactionLinksAdmin.linkCount
|
||||
this.items = result.data.listTransactionLinksAdmin.linkList
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return [
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: this.$t('transactionlink.created'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: this.$t('transactionlist.amount'),
|
||||
formatter: (value, key, item) => {
|
||||
return `${value} GDD`
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: this.$t('transactionlist.memo') },
|
||||
{
|
||||
key: 'validUntil',
|
||||
label: this.$t('transactionlink.valid_until'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: 'status',
|
||||
formatter: (value, key, item) => {
|
||||
// deleted
|
||||
if (item.deletedAt) return this.$t('deleted') + ': ' + this.$d(new Date(item.deletedAt))
|
||||
// redeemed
|
||||
if (item.redeemedAt)
|
||||
return this.$t('redeemed') + ': ' + this.$d(new Date(item.redeemedAt))
|
||||
// expired
|
||||
if (new Date() > new Date(item.validUntil))
|
||||
return this.$t('expired') + ': ' + this.$d(new Date(item.validUntil))
|
||||
// open
|
||||
return this.$t('open')
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getListTransactionLinks()
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.getListTransactionLinks()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
20
admin/src/graphql/listTransactionLinksAdmin.js
Normal file
20
admin/src/graphql/listTransactionLinksAdmin.js
Normal file
@ -0,0 +1,20 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const listTransactionLinksAdmin = gql`
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 5, $userId: Int!) {
|
||||
listTransactionLinksAdmin(currentPage: $currentPage, pageSize: $pageSize, userId: $userId) {
|
||||
linkCount
|
||||
linkList {
|
||||
id
|
||||
amount
|
||||
holdAvailableAmount
|
||||
memo
|
||||
code
|
||||
createdAt
|
||||
validUntil
|
||||
redeemedAt
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -27,6 +27,7 @@
|
||||
"edit": "Bearbeiten",
|
||||
"enabled": "aktiviert",
|
||||
"error": "Fehler",
|
||||
"expired": "abgelaufen",
|
||||
"e_mail": "E-Mail",
|
||||
"firstname": "Vorname",
|
||||
"footer": {
|
||||
@ -56,6 +57,7 @@
|
||||
"user_search": "Nutzersuche"
|
||||
},
|
||||
"not_open_creations": "Keine offenen Schöpfungen",
|
||||
"open": "offen",
|
||||
"open_creations": "Offene Schöpfungen",
|
||||
"overlay": {
|
||||
"confirm": {
|
||||
@ -66,6 +68,7 @@
|
||||
"yes": "Ja, Schöpfung bestätigen und speichern!"
|
||||
}
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"remove": "Entfernen",
|
||||
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
|
||||
"remove_all": "alle Nutzer entfernen",
|
||||
@ -73,6 +76,11 @@
|
||||
"status": "Status",
|
||||
"success": "Erfolg",
|
||||
"text": "Text",
|
||||
"transactionlink": {
|
||||
"created": "Erstellt",
|
||||
"form_header": "Transaktion-Links",
|
||||
"valid_until": "Gültig bis"
|
||||
},
|
||||
"transactionlist": {
|
||||
"amount": "Betrag",
|
||||
"balanceDate": "Schöpfungsdatum",
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"edit": "Edit",
|
||||
"enabled": "enabled",
|
||||
"error": "Error",
|
||||
"expired": "expired",
|
||||
"e_mail": "E-mail",
|
||||
"firstname": "Firstname",
|
||||
"footer": {
|
||||
@ -56,6 +57,7 @@
|
||||
"user_search": "User search"
|
||||
},
|
||||
"not_open_creations": "No open creations",
|
||||
"open": "open",
|
||||
"open_creations": "Open creations",
|
||||
"overlay": {
|
||||
"confirm": {
|
||||
@ -66,6 +68,7 @@
|
||||
"yes": "Yes, confirm and save creation!"
|
||||
}
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"remove": "Remove",
|
||||
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
||||
"remove_all": "Remove all users",
|
||||
@ -73,6 +76,11 @@
|
||||
"status": "Status",
|
||||
"success": "Success",
|
||||
"text": "Text",
|
||||
"transactionlink": {
|
||||
"created": "Created",
|
||||
"form_header": "Transaction links",
|
||||
"valid_until": "Valid until"
|
||||
},
|
||||
"transactionlist": {
|
||||
"amount": "Amount",
|
||||
"balanceDate": "Creation date",
|
||||
|
||||
@ -35,4 +35,5 @@ export enum RIGHTS {
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
||||
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
|
||||
}
|
||||
|
||||
13
backend/src/graphql/arg/TransactionLinkFilters.ts
Normal file
13
backend/src/graphql/arg/TransactionLinkFilters.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class TransactionLinkFilters {
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||
withDeleted?: boolean
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||
withExpired?: boolean
|
||||
|
||||
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||
withRedeemed?: boolean
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User } from './User'
|
||||
@ -52,3 +52,12 @@ export class TransactionLink {
|
||||
@Field(() => User, { nullable: true })
|
||||
redeemedBy: User | null
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionLinkResult {
|
||||
@Field(() => Int)
|
||||
linkCount: number
|
||||
|
||||
@Field(() => [TransactionLink])
|
||||
linkList: TransactionLink[]
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import {
|
||||
ObjectLiteral,
|
||||
getConnection,
|
||||
In,
|
||||
MoreThan,
|
||||
FindOperator,
|
||||
} from '@dbTools/typeorm'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { PendingCreation } from '@model/PendingCreation'
|
||||
@ -21,6 +23,8 @@ import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
|
||||
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionRepository } from '@repository/Transaction'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||
@ -32,6 +36,7 @@ import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Decay } from '@model/Decay'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||
import { Order } from '@enum/Order'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
|
||||
@ -369,6 +374,41 @@ export class AdminResolver {
|
||||
const user = await dbUser.findOneOrFail({ id: userId })
|
||||
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS_ADMIN])
|
||||
@Query(() => TransactionLinkResult)
|
||||
async listTransactionLinksAdmin(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
@Args()
|
||||
filters: TransactionLinkFilters,
|
||||
@Arg('userId', () => Int) userId: number,
|
||||
): Promise<TransactionLinkResult> {
|
||||
const user = await dbUser.findOneOrFail({ id: userId })
|
||||
const where: {
|
||||
userId: number
|
||||
redeemedBy?: number | null
|
||||
validUntil?: FindOperator<Date> | null
|
||||
} = {
|
||||
userId,
|
||||
}
|
||||
if (!filters.withRedeemed) where.redeemedBy = null
|
||||
if (!filters.withExpired) where.validUntil = MoreThan(new Date())
|
||||
const [transactionLinks, count] = await dbTransactionLink.findAndCount({
|
||||
where,
|
||||
withDeleted: filters.withDeleted,
|
||||
order: {
|
||||
createdAt: order,
|
||||
},
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
|
||||
return {
|
||||
linkCount: count,
|
||||
linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CreationMap {
|
||||
|
||||
@ -34,6 +34,9 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
|
||||
const MEMO_MAX_CHARS = 255
|
||||
const MEMO_MIN_CHARS = 5
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
memo: string,
|
||||
@ -45,6 +48,14 @@ export const executeTransaction = async (
|
||||
throw new Error('Sender and Recipient are the same.')
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(sender.id, amount.mul(-1), receivedCallDate)
|
||||
|
||||
@ -62,11 +62,8 @@ describe('TransactionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('is selected: "send"', () => {
|
||||
describe('send GDD', () => {
|
||||
beforeEach(async () => {
|
||||
// await wrapper.setData({
|
||||
// selected: 'send',
|
||||
// })
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
@ -78,15 +75,18 @@ describe('TransactionForm', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ balance: 100.0 })
|
||||
})
|
||||
|
||||
describe('transaction form show because balance 100,0 GDD', () => {
|
||||
it('has no warning message ', () => {
|
||||
expect(wrapper.find('.errors').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('has a reset button', () => {
|
||||
expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe(
|
||||
'reset',
|
||||
)
|
||||
})
|
||||
|
||||
it('has a submit button', () => {
|
||||
expect(wrapper.find('.test-buttons').findAll('button').at(1).attributes('type')).toBe(
|
||||
'submit',
|
||||
@ -121,6 +121,12 @@ describe('TransactionForm', () => {
|
||||
expect(wrapper.find('span.errors').text()).toBe('validations.messages.email')
|
||||
})
|
||||
|
||||
it('flushes an error message when email is the email of logged in user', async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue('user@example.org')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('form.validation.is-not')
|
||||
})
|
||||
|
||||
it('trims the email after blur', async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ')
|
||||
await wrapper.find('#input-group-1').find('input').trigger('blur')
|
||||
@ -195,6 +201,41 @@ describe('TransactionForm', () => {
|
||||
expect(wrapper.find('span.errors').text()).toBe('validations.messages.min')
|
||||
})
|
||||
|
||||
it('flushes an error message when memo is more than 255 characters', async () => {
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue(`
|
||||
Es ist ein König in Thule, der trinkt
|
||||
Champagner, es geht ihm nichts drüber;
|
||||
Und wenn er seinen Champagner trinkt,
|
||||
Dann gehen die Augen ihm über.
|
||||
|
||||
Die Ritter sitzen um ihn her,
|
||||
Die ganze Historische Schule;
|
||||
Ihm aber wird die Zunge schwer,
|
||||
Es lallt der König von Thule:
|
||||
|
||||
„Als Alexander, der Griechenheld,
|
||||
Mit seinem kleinen Haufen
|
||||
Erobert hatte die ganze Welt,
|
||||
Da gab er sich ans Saufen.
|
||||
|
||||
Ihn hatten so durstig gemacht der Krieg
|
||||
Und die Schlachten, die er geschlagen;
|
||||
Er soff sich zu Tode nach dem Sieg,
|
||||
Er konnte nicht viel vertragen.
|
||||
|
||||
Ich aber bin ein stärkerer Mann
|
||||
Und habe mich klüger besonnen:
|
||||
Wie jener endete, fang ich an,
|
||||
Ich hab mit dem Trinken begonnen.
|
||||
|
||||
Im Rausche wird der Heldenzug
|
||||
Mir später weit besser gelingen;
|
||||
Dann werde ich, taumelnd von Krug zu Krug,
|
||||
Die ganze Welt bezwingen.“`)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('validations.messages.max')
|
||||
})
|
||||
|
||||
it('flushes no error message when memo is valid', async () => {
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
|
||||
await flushPromises()
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
:rules="{
|
||||
required: true,
|
||||
min: 5,
|
||||
max: 150,
|
||||
max: 255,
|
||||
}"
|
||||
:name="$t('form.message')"
|
||||
v-slot="{ errors }"
|
||||
|
||||
@ -10,9 +10,20 @@
|
||||
</b-col>
|
||||
<b-col cols="7">
|
||||
<div class="gdd-transaction-list-item-name">
|
||||
<b-link v-if="linkedUser && linkedUser.email" @click.stop="tunnelEmail">
|
||||
{{ itemText }}
|
||||
</b-link>
|
||||
<div v-if="linkedUser && linkedUser.email">
|
||||
<b-link @click.stop="tunnelEmail">
|
||||
{{ itemText }}
|
||||
</b-link>
|
||||
<span v-if="transactionLinkId">
|
||||
{{ $t('via_link') }}
|
||||
<b-icon
|
||||
icon="link45deg"
|
||||
variant="muted"
|
||||
class="m-mb-1"
|
||||
:title="$t('gdd_per_link.redeemed-title')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<span v-else>{{ itemText }}</span>
|
||||
</div>
|
||||
</b-col>
|
||||
@ -35,6 +46,11 @@ export default {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
transactionLinkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tunnelEmail() {
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
<div class="date-row">
|
||||
<b-row>
|
||||
<b-col cols="5">
|
||||
<div class="text-right">{{ diffNow ? $t('gdd_per_link.expired') : $t('form.date') }}</div>
|
||||
<div class="text-right">
|
||||
{{ diffNow ? $t('gdd_per_link.valid_until') : $t('form.date') }}
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="7">
|
||||
<div class="gdd-transaction-list-item-date">
|
||||
|
||||
@ -13,7 +13,12 @@
|
||||
|
||||
<b-col cols="11">
|
||||
<!-- Amount / Name || Text -->
|
||||
<amount-and-name-row :amount="amount" :linkedUser="linkedUser" v-on="$listeners" />
|
||||
<amount-and-name-row
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:transactionLinkId="transactionLinkId"
|
||||
/>
|
||||
|
||||
<!-- Nachricht Memo -->
|
||||
<memo-row :memo="memo" />
|
||||
@ -86,6 +91,10 @@ export default {
|
||||
type: Date,
|
||||
required: true,
|
||||
},
|
||||
transactionLinkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -13,7 +13,12 @@
|
||||
|
||||
<b-col cols="11">
|
||||
<!-- Amount / Name -->
|
||||
<amount-and-name-row :amount="amount" :linkedUser="linkedUser" v-on="$listeners" />
|
||||
<amount-and-name-row
|
||||
v-on="$listeners"
|
||||
:amount="amount"
|
||||
:linkedUser="linkedUser"
|
||||
:transactionLinkId="transactionLinkId"
|
||||
/>
|
||||
|
||||
<!-- Memo -->
|
||||
<memo-row :memo="memo" />
|
||||
@ -87,6 +92,10 @@ export default {
|
||||
type: Date,
|
||||
required: true,
|
||||
},
|
||||
transactionLinkId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@ -70,6 +70,7 @@ export const transactionsQuery = gql`
|
||||
linkedUser {
|
||||
email
|
||||
}
|
||||
transactionLinkId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +100,6 @@
|
||||
"decay-14-day": "Vergänglichkeit für 14 Tage",
|
||||
"delete-the-link": "Den Link löschen?",
|
||||
"deleted": "Der Link wurde gelöscht!",
|
||||
"expired": "Abgelaufen",
|
||||
"has-account": "Du besitzt bereits ein Gradido Konto",
|
||||
"header": "Gradidos versenden per Link",
|
||||
"link-copied": "Link wurde in die Zwischenablage kopiert",
|
||||
@ -116,8 +115,10 @@
|
||||
"redeem-text": "Willst du den Betrag jetzt einlösen?",
|
||||
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
|
||||
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
|
||||
"redeemed-title": "eingelöst",
|
||||
"to-login": "Log dich ein",
|
||||
"to-register": "Registriere ein neues Konto"
|
||||
"to-register": "Registriere ein neues Konto",
|
||||
"valid_until": "Gültig bis"
|
||||
},
|
||||
"gdt": {
|
||||
"calculation": "Berechnung der GradidoTransform",
|
||||
@ -249,5 +250,6 @@
|
||||
},
|
||||
"transaction-link": {
|
||||
"send_you": "sendet dir"
|
||||
}
|
||||
},
|
||||
"via_link": "über einen Link"
|
||||
}
|
||||
|
||||
@ -100,7 +100,6 @@
|
||||
"decay-14-day": "Decay for 14 days",
|
||||
"delete-the-link": "Delete the link?",
|
||||
"deleted": "The link was deleted!",
|
||||
"expired": "Expired",
|
||||
"has-account": "You already have a Gradido account",
|
||||
"header": "Send Gradidos via link",
|
||||
"link-copied": "Link copied to clipboard",
|
||||
@ -116,8 +115,10 @@
|
||||
"redeem-text": "Do you want to redeem the amount now?",
|
||||
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
|
||||
"redeemed-at": "The link was already redeemed on {date}.",
|
||||
"redeemed-title": "redeemed",
|
||||
"to-login": "Log in",
|
||||
"to-register": "Register a new account"
|
||||
"to-register": "Register a new account",
|
||||
"valid_until": "Valid until"
|
||||
},
|
||||
"gdt": {
|
||||
"calculation": "Calculation of GradidoTransform",
|
||||
@ -249,5 +250,6 @@
|
||||
},
|
||||
"transaction-link": {
|
||||
"send_you": "wants to send you"
|
||||
}
|
||||
},
|
||||
"via_link": "via Link"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user