Merge pull request #1135 from gradido/admin_pending_creation

Admin pending creation
This commit is contained in:
Alexander Friedland 2021-12-02 09:39:19 +01:00 committed by GitHub
commit ed7c702d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 506 additions and 101 deletions

View File

@ -441,7 +441,7 @@ jobs:
report_name: Coverage Admin Interface report_name: Coverage Admin Interface
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 49 min_coverage: 50
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################
@ -491,7 +491,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 37 min_coverage: 39
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -43,6 +43,7 @@
"vue-jest": "^3.0.7", "vue-jest": "^3.0.7",
"vue-moment": "^4.1.0", "vue-moment": "^4.1.0",
"vue-router": "^3.5.3", "vue-router": "^3.5.3",
"vue-toasted": "^1.1.28",
"vuex": "^3.6.2", "vuex": "^3.6.2",
"vuex-persistedstate": "^4.1.0" "vuex-persistedstate": "^4.1.0"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -3,6 +3,16 @@ import CreationFormular from './CreationFormular.vue'
const localVue = global.localVue const localVue = global.localVue
const apolloMock = jest.fn().mockResolvedValue({
data: {
verifyLogin: {
name: 'success',
id: 0,
},
},
})
const stateCommitMock = jest.fn()
const mocks = { const mocks = {
$moment: jest.fn(() => { $moment: jest.fn(() => {
return { return {
@ -14,6 +24,12 @@ const mocks = {
}), }),
} }
}), }),
$apollo: {
query: apolloMock,
},
$store: {
commit: stateCommitMock,
},
} }
const propsData = { const propsData = {
@ -39,6 +55,23 @@ describe('CreationFormular', () => {
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy() expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
}) })
describe('server sends back moderator data', () => {
it('called store commit with mocked data', () => {
expect(stateCommitMock).toBeCalledWith('moderator', { name: 'success', id: 0 })
})
})
describe('server throws error for moderator data call', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloMock.mockRejectedValue({ message: 'Ouch!' })
wrapper = Wrapper()
})
it('has called store commit with fake data', () => {
expect(stateCommitMock).toBeCalledWith('moderator', { id: 0, name: 'Test Moderator' })
})
})
describe('radio buttons to selcet month', () => { describe('radio buttons to selcet month', () => {
it('has three radio buttons', () => { it('has three radio buttons', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(3) expect(wrapper.findAll('input[type="radio"]').length).toBe(3)

View File

@ -7,7 +7,6 @@
? 'Einzelschöpfung für ' + item.firstName + ' ' + item.lastName + '' ? 'Einzelschöpfung für ' + item.firstName + ' ' + item.lastName + ''
: 'Mehrfachschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder' : 'Mehrfachschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder'
}} }}
{{ item }}
</h3> </h3>
<div v-show="this.type === 'massCreation' && Object.keys(this.itemsMassCreation).length <= 0"> <div v-show="this.type === 'massCreation' && Object.keys(this.itemsMassCreation).length <= 0">
Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest
@ -24,6 +23,7 @@
<b-form-radio <b-form-radio
v-model="radioSelected" v-model="radioSelected"
:value="beforeLastMonth" :value="beforeLastMonth"
:disabled="creation[0] === 0"
size="lg" size="lg"
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])" @change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
> >
@ -34,6 +34,7 @@
<b-form-radio <b-form-radio
v-model="radioSelected" v-model="radioSelected"
:value="lastMonth" :value="lastMonth"
:disabled="creation[1] === 0"
size="lg" size="lg"
@change="updateRadioSelected(lastMonth, 1, creation[1])" @change="updateRadioSelected(lastMonth, 1, creation[1])"
> >
@ -44,6 +45,7 @@
<b-form-radio <b-form-radio
v-model="radioSelected" v-model="radioSelected"
:value="currentMonth" :value="currentMonth"
:disabled="creation[2] === 0"
size="lg" size="lg"
@change="updateRadioSelected(currentMonth, 2, creation[2])" @change="updateRadioSelected(currentMonth, 2, creation[2])"
> >
@ -52,12 +54,10 @@
</b-col> </b-col>
</b-row> </b-row>
<b-row class="m-4"> <b-row class="m-4" v-show="createdIndex">
<label>Betrag Auswählen</label> <label>Betrag Auswählen</label>
<b-input-group> <div>
<template #append> <b-input-group prepend="GDD" append=".00">
<b-input-group-text><strong class="text-danger">GDD</strong></b-input-group-text>
</template>
<b-form-input <b-form-input
type="number" type="number"
v-model="value" v-model="value"
@ -66,16 +66,17 @@
></b-form-input> ></b-form-input>
</b-input-group> </b-input-group>
<b-input <b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
id="range-2" <b-form-input
class="mt-2"
v-model="value"
type="range" type="range"
v-model="value"
:min="rangeMin" :min="rangeMin"
:max="rangeMax" :max="rangeMax"
step="10" step="10"
@load="checkFormForUpdate('range')" @load="checkFormForUpdate('range')"
></b-input> ></b-form-input>
</b-input-group>
</div>
</b-row> </b-row>
<b-row class="m-4"> <b-row class="m-4">
<label>Text eintragen</label> <label>Text eintragen</label>
@ -125,6 +126,8 @@
</div> </div>
</template> </template>
<script> <script>
import { verifyLogin } from '../graphql/verifyLogin'
import { createPendingCreation } from '../graphql/createPendingCreation'
export default { export default {
name: 'CreationFormular', name: 'CreationFormular',
props: { props: {
@ -163,23 +166,25 @@ export default {
rangeMax: 1000, rangeMax: 1000,
currentMonth: { currentMonth: {
short: this.$moment().format('MMMM'), short: this.$moment().format('MMMM'),
long: this.$moment().format('DD/MM/YYYY'), long: this.$moment().format('YYYY-MM-DD'),
}, },
lastMonth: { lastMonth: {
short: this.$moment().subtract(1, 'month').format('MMMM'), short: this.$moment().subtract(1, 'month').format('MMMM'),
long: this.$moment().subtract(1, 'month').format('DD/MM/YYYY'), long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
}, },
beforeLastMonth: { beforeLastMonth: {
short: this.$moment().subtract(2, 'month').format('MMMM'), short: this.$moment().subtract(2, 'month').format('MMMM'),
long: this.$moment().subtract(2, 'month').format('DD/MM/YYYY'), long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
}, },
submitObj: null, submitObj: null,
isdisabled: true, isdisabled: true,
createdIndex: null,
} }
}, },
methods: { methods: {
// Auswählen eines Zeitraumes // Auswählen eines Zeitraumes
updateRadioSelected(name, index, openCreation) { updateRadioSelected(name, index, openCreation) {
this.createdIndex = index
// Wenn Mehrfachschöpfung // Wenn Mehrfachschöpfung
if (this.type === 'massCreation') { if (this.type === 'massCreation') {
// An Creation.vue emitten und radioSelectedMass aktualisieren // An Creation.vue emitten und radioSelectedMass aktualisieren
@ -230,10 +235,11 @@ export default {
this.submitObj = [ this.submitObj = [
{ {
item: this.itemsMassCreation, item: this.itemsMassCreation,
datum: this.radioSelected, email: this.item.email,
creationDate: this.radioSelected.long,
amount: this.value, amount: this.value,
text: this.text, memo: this.text,
moderator: this.$store.state.moderator, moderator: this.$store.state.moderator.id,
}, },
] ]
alert('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder') alert('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
@ -246,18 +252,13 @@ export default {
} }
if (this.type === 'singleCreation') { if (this.type === 'singleCreation') {
// hinweis das eine einzelne schöpfung ausgeführt wird an (Vorname) this.submitObj = {
alert('SUBMIT CREATION => ' + this.type + ' >> für ' + this.item.firstName + '') email: this.item.email,
// erstellen eines Arrays (submitObj) mit allen Daten creationDate: this.radioSelected.long,
this.submitObj = [ amount: Number(this.value),
{ memo: this.text,
item: this.item, moderator: Number(this.$store.state.moderator.id),
datum: this.radioSelected.long, }
amount: this.value,
text: this.text,
moderator: this.$store.state.moderator,
},
]
if (this.pagetype === 'PageCreationConfirm') { if (this.pagetype === 'PageCreationConfirm') {
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email) // hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
@ -269,22 +270,48 @@ export default {
text: this.text, text: this.text,
}) })
} else { } else {
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email) this.$apollo
alert('EINZEL SCHÖPFUNG ABSENDEN FÜR >> ' + this.item.firstName + '') .mutate({
// $store - offene Schöpfungen hochzählen mutation: createPendingCreation,
variables: this.submitObj,
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
this.$toasted.success(
`Offene schöpfung (${this.value} GDD) für ${this.item.email} wurde gespeichert, liegen zur bestätigung bereit`,
)
this.$store.commit('openCreationsPlus', 1) this.$store.commit('openCreationsPlus', 1)
} this.submitObj = null
} this.createdIndex = null
// das creation Formular reseten
// das absendeergebniss im string ansehen this.$refs.creationForm.reset()
alert(JSON.stringify(this.submitObj)) // Den geschöpften Wert auf o setzen
// das submitObj zurücksetzen this.value = 0
})
.catch((error) => {
this.$toasted.error(error.message)
this.submitObj = null this.submitObj = null
// das creation Formular reseten // das creation Formular reseten
this.$refs.creationForm.reset() this.$refs.creationForm.reset()
// Den geschöpften Wert auf o setzen // Den geschöpften Wert auf o setzen
this.value = 0 this.value = 0
})
}
}
}, },
searchModeratorData() {
this.$apollo
.query({ query: verifyLogin })
.then((result) => {
this.$store.commit('moderator', result.data.verifyLogin)
})
.catch(() => {
this.$store.commit('moderator', { id: 0, name: 'Test Moderator' })
})
},
},
created() {
this.searchModeratorData()
}, },
} }
</script> </script>

View File

@ -3,11 +3,19 @@ import NavBar from './NavBar.vue'
const localVue = global.localVue const localVue = global.localVue
const storeDispatchMock = jest.fn()
const routerPushMock = jest.fn()
const mocks = { const mocks = {
$store: { $store: {
state: { state: {
openCreations: 1, openCreations: 1,
token: 'valid-token',
}, },
dispatch: storeDispatchMock,
},
$router: {
push: routerPushMock,
}, },
} }
@ -27,4 +35,34 @@ describe('NavBar', () => {
expect(wrapper.find('.component-nabvar').exists()).toBeTruthy() expect(wrapper.find('.component-nabvar').exists()).toBeTruthy()
}) })
}) })
describe('wallet', () => {
const assignLocationSpy = jest.fn()
beforeEach(async () => {
await wrapper.findAll('a').at(5).trigger('click')
})
it.skip('changes widnow location to wallet', () => {
expect(assignLocationSpy).toBeCalledWith('valid-token')
})
it('dispatches logout to store', () => {
expect(storeDispatchMock).toBeCalledWith('logout')
})
})
describe('logout', () => {
// const assignLocationSpy = jest.fn()
beforeEach(async () => {
await wrapper.findAll('a').at(6).trigger('click')
})
it('redirects to /logout', () => {
expect(routerPushMock).toBeCalledWith('/logout')
})
it('dispatches logout to store', () => {
expect(storeDispatchMock).toBeCalledWith('logout')
})
})
}) })

View File

@ -1,12 +1,15 @@
<template> <template>
<div class="component-nabvar"> <div class="component-nabvar">
<b-navbar toggleable="sm" type="dark" variant="success"> <b-navbar toggleable="sm" type="dark" variant="success">
<b-navbar-brand to="/">Adminbereich</b-navbar-brand> <b-navbar-brand to="/">
<img src="img/brand/green.png" class="navbar-brand-img" alt="..." />
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle> <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/">Übersicht |</b-nav-item>
<b-nav-item to="/user">Usersuche |</b-nav-item> <b-nav-item to="/user">Usersuche |</b-nav-item>
<b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item> <b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item>
<b-nav-item <b-nav-item
@ -31,21 +34,6 @@ export default {
name: 'navbar', name: 'navbar',
methods: { methods: {
logout() { logout() {
// TODO
// this.$emit('logout')
/* this.$apollo
.query({
query: logout,
})
.then(() => {
this.$store.dispatch('logout')
this.$router.push('/logout')
})
.catch(() => {
this.$store.dispatch('logout')
if (this.$router.currentRoute.path !== '/logout') this.$router.push('/logout')
})
*/
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.$router.push('/logout') this.$router.push('/logout')
}, },
@ -56,3 +44,8 @@ export default {
}, },
} }
</script> </script>
<style>
.navbar-brand-img {
height: 2rem;
}
</style>

View File

@ -14,11 +14,11 @@
{{ overlayText.text2 }} {{ overlayText.text2 }}
</p> </p>
<b-button size="lg" variant="danger" class="m-3" @click="overlayCancel"> <b-button size="md" variant="danger" class="m-3" @click="overlayCancel">
{{ overlayText.button_cancel }} {{ overlayText.button_cancel }}
</b-button> </b-button>
<b-button <b-button
size="lg" size="md"
variant="success" variant="success"
class="m-3 text-right" class="m-3 text-right"
@click="overlayOK(overlayBookmarkType, overlayItem)" @click="overlayOK(overlayBookmarkType, overlayItem)"
@ -39,7 +39,7 @@
<template #cell(edit_creation)="row"> <template #cell(edit_creation)="row">
<b-button <b-button
variant="info" variant="info"
size="lg" size="md"
@click="editCreationUserTable(row, row.item)" @click="editCreationUserTable(row, row.item)"
class="mr-2" class="mr-2"
> >
@ -49,7 +49,7 @@
</template> </template>
<template #cell(show_details)="row"> <template #cell(show_details)="row">
<b-button variant="info" size="lg" @click="row.toggleDetails" class="mr-2"> <b-button variant="info" size="md" @click="row.toggleDetails" class="mr-2">
<b-icon v-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon> <b-icon v-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon>
<b-icon v-else icon="eye-fill" aria-label="Help"></b-icon> <b-icon v-else icon="eye-fill" aria-label="Help"></b-icon>
</b-button> </b-button>
@ -68,6 +68,7 @@
:item="row.item" :item="row.item"
:creationUserData="creationData" :creationUserData="creationData"
@update-creation-data="updateCreationData" @update-creation-data="updateCreationData"
@update-user-data="updateUserData"
/> />
<b-button size="sm" @click="row.toggleDetails"> <b-button size="sm" @click="row.toggleDetails">
@ -93,7 +94,7 @@
<b-button <b-button
variant="danger" variant="danger"
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'" v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
size="lg" size="md"
@click="overlayShow('remove', row.item)" @click="overlayShow('remove', row.item)"
class="mr-2" class="mr-2"
> >
@ -105,7 +106,7 @@
<b-button <b-button
variant="success" variant="success"
v-show="type === 'PageCreationConfirm'" v-show="type === 'PageCreationConfirm'"
size="lg" size="md"
@click="overlayShow('confirm', row.item)" @click="overlayShow('confirm', row.item)"
class="mr-2" class="mr-2"
> >
@ -232,6 +233,9 @@ export default {
...data, ...data,
} }
}, },
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
}, },
} }
</script> </script>

View File

@ -0,0 +1,19 @@
import gql from 'graphql-tag'
export const createPendingCreation = gql`
mutation (
$email: String!
$amount: Int!
$memo: String!
$creationDate: String!
$moderator: Int!
) {
createPendingCreation(
email: $email
amount: $amount
memo: $memo
creationDate: $creationDate
moderator: $moderator
)
}
`

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const verifyLogin = gql`
query {
verifyLogin {
firstName
lastName
id
}
}
`

View File

@ -21,6 +21,7 @@ import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import moment from 'vue-moment' import moment from 'vue-moment'
import Toasted from 'vue-toasted'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI }) const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
@ -62,6 +63,18 @@ Vue.use(moment)
Vue.use(VueApollo) Vue.use(VueApollo)
Vue.use(Toasted, {
position: 'top-center',
duration: 5000,
fullWidth: true,
action: {
text: 'x',
onClick: (e, toastObject) => {
toastObject.goAway(0)
},
},
})
addNavigationGuards(router, store) addNavigationGuards(router, store)
new Vue({ new Vue({

View File

@ -3,12 +3,14 @@ import './main'
import CONFIG from './config' import CONFIG from './config'
import Vue from 'vue' import Vue from 'vue'
import VueApollo from 'vue-apollo'
import Vuex from 'vuex' import Vuex from 'vuex'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import moment from 'vue-moment' import moment from 'vue-moment'
jest.mock('vue') jest.mock('vue')
jest.mock('vue-apollo')
jest.mock('vuex') jest.mock('vuex')
jest.mock('vue-i18n') jest.mock('vue-i18n')
jest.mock('vue-moment') jest.mock('vue-moment')
@ -55,6 +57,10 @@ describe('main', () => {
expect(InMemoryCache).toBeCalled() expect(InMemoryCache).toBeCalled()
}) })
it('calls the VueApollo', () => {
expect(VueApollo).toBeCalled()
})
it('calls Vue', () => { it('calls Vue', () => {
expect(Vue).toBeCalled() expect(Vue).toBeCalled()
}) })
@ -63,16 +69,16 @@ describe('main', () => {
expect(VueI18n).toBeCalled() expect(VueI18n).toBeCalled()
}) })
it.skip('calls BootstrapVue', () => { it('calls BootstrapVue', () => {
expect(BootstrapVue).toBeCalled() expect(Vue.use).toBeCalledWith(BootstrapVue)
}) })
it.skip('calls IconsPlugin', () => { it('calls IconsPlugin', () => {
expect(IconsPlugin).toBeCalled() expect(Vue.use).toBeCalledWith(IconsPlugin)
}) })
it.skip('calls Moment', () => { it('calls Moment', () => {
expect(moment).toBeCalled() expect(Vue.use).toBeCalledWith(moment)
}) })
it.skip('creates a store', () => { it.skip('creates a store', () => {

View File

@ -32,7 +32,13 @@ export default {
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
{ key: 'firstName', label: 'Firstname' }, { key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' }, { key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' }, {
key: 'creation',
label: 'Creation',
formatter: (value, key, item) => {
return String(value)
},
},
{ key: 'show_details', label: 'Details' }, { key: 'show_details', label: 'Details' },
], ],
searchResult: [], searchResult: [],

View File

@ -18,6 +18,9 @@ export const mutations = {
token: (state, token) => { token: (state, token) => {
state.token = token state.token = token
}, },
moderator: (state, moderator) => {
state.moderator = moderator
},
} }
export const actions = { export const actions = {
@ -35,7 +38,7 @@ const store = new Vuex.Store({
], ],
state: { state: {
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null, token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
moderator: 'Dertest Moderator', moderator: { name: 'Dertest Moderator', id: 0 },
openCreations: 0, openCreations: 0,
}, },
// Syncronous mutation of the state // Syncronous mutation of the state

View File

@ -12524,6 +12524,11 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue-toasted@^1.1.28:
version "1.1.28"
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
vue@^2.6.11: vue@^2.6.11:
version "2.6.14" version "2.6.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"

View File

@ -29,6 +29,7 @@
"jest": "^27.2.4", "jest": "^27.2.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"moment": "^2.29.1",
"mysql2": "^2.3.0", "mysql2": "^2.3.0",
"nodemailer": "^6.6.5", "nodemailer": "^6.6.5",
"random-bigint": "^0.0.1", "random-bigint": "^0.0.1",

View File

@ -0,0 +1,19 @@
import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export default class CreatePendingCreationArgs {
@Field(() => String)
email: string
@Field(() => Int)
amount: number
@Field(() => String)
memo: string
@Field(() => String)
creationDate: string
@Field(() => Int)
moderator: number
}

View File

@ -12,6 +12,7 @@ export class User {
*/ */
constructor(json?: any) { constructor(json?: any) {
if (json) { if (json) {
this.id = json.id
this.email = json.email this.email = json.email
this.firstName = json.first_name this.firstName = json.first_name
this.lastName = json.last_name this.lastName = json.last_name
@ -24,6 +25,9 @@ export class User {
} }
} }
@Field(() => Number)
id: number
@Field(() => String) @Field(() => String)
email: string email: string

View File

@ -1,8 +1,13 @@
import { Resolver, Query, Arg, Authorized } from 'type-graphql' import { Resolver, Query, Arg, Args, Authorized, Mutation } from 'type-graphql'
import { getCustomRepository } from 'typeorm' import { getCustomRepository, Raw } from 'typeorm'
import { UserAdmin } from '../model/UserAdmin' import { UserAdmin } from '../model/UserAdmin'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser' import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { RIGHTS } from '../../auth/RIGHTS' import { RIGHTS } from '../../auth/RIGHTS'
import { TransactionCreationRepository } from '../../typeorm/repository/TransactionCreation'
import { PendingCreationRepository } from '../../typeorm/repository/PendingCreation'
import { UserRepository } from '../../typeorm/repository/User'
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
import moment from 'moment'
@Resolver() @Resolver()
export class AdminResolver { export class AdminResolver {
@ -11,18 +16,161 @@ export class AdminResolver {
async searchUsers(@Arg('searchText') searchText: string): Promise<UserAdmin[]> { async searchUsers(@Arg('searchText') searchText: string): Promise<UserAdmin[]> {
const loginUserRepository = getCustomRepository(LoginUserRepository) const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUsers = await loginUserRepository.findBySearchCriteria(searchText) const loginUsers = await loginUserRepository.findBySearchCriteria(searchText)
const users = loginUsers.map((loginUser) => { const users = await Promise.all(
loginUsers.map(async (loginUser) => {
const user = new UserAdmin() const user = new UserAdmin()
user.firstName = loginUser.firstName user.firstName = loginUser.firstName
user.lastName = loginUser.lastName user.lastName = loginUser.lastName
user.email = loginUser.email user.email = loginUser.email
user.creation = [ user.creation = await getUserCreations(loginUser.id)
(Math.floor(Math.random() * 50) + 1) * 20,
(Math.floor(Math.random() * 50) + 1) * 20,
(Math.floor(Math.random() * 50) + 1) * 20,
]
return user return user
}) }),
)
return users return users
} }
@Mutation(() => [Number])
async createPendingCreation(
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
): Promise<number[]> {
const userRepository = getCustomRepository(UserRepository)
const user = await userRepository.findByEmail(email)
const creations = await getUserCreations(user.id)
const creationDateObj = new Date(creationDate)
if (isCreationValid(creations, amount, creationDateObj)) {
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const loginPendingTaskAdmin = pendingCreationRepository.create()
loginPendingTaskAdmin.userId = user.id
loginPendingTaskAdmin.amount = BigInt(amount * 10000)
loginPendingTaskAdmin.created = new Date()
loginPendingTaskAdmin.date = creationDateObj
loginPendingTaskAdmin.memo = memo
loginPendingTaskAdmin.moderator = moderator
pendingCreationRepository.save(loginPendingTaskAdmin)
}
return await getUserCreations(user.id)
}
}
async function getUserCreations(id: number): Promise<number[]> {
const dateNextMonth = moment().add(1, 'month').format('YYYY-MM') + '-01'
const dateMonth = moment().format('YYYY-MM') + '-01'
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM') + '-01'
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM') + '-01'
const transactionCreationRepository = getCustomRepository(TransactionCreationRepository)
const createdAmountBeforeLastMonth = await transactionCreationRepository
.createQueryBuilder('transaction_creations')
.select('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateBeforeLastMonth,
enddate: dateLastMonth,
}),
})
.getRawOne()
const createdAmountLastMonth = await transactionCreationRepository
.createQueryBuilder('transaction_creations')
.select('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateLastMonth,
enddate: dateMonth,
}),
})
.getRawOne()
const createdAmountMonth = await transactionCreationRepository
.createQueryBuilder('transaction_creations')
.select('SUM(transaction_creations.amount)', 'sum')
.where('transaction_creations.state_user_id = :id', { id })
.andWhere({
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateMonth,
enddate: dateNextMonth,
}),
})
.getRawOne()
const pendingCreationRepository = getCustomRepository(PendingCreationRepository)
const pendingAmountMounth = await pendingCreationRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
.where('login_pending_tasks_admin.userId = :id', { id })
.andWhere({
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateMonth,
enddate: dateNextMonth,
}),
})
.getRawOne()
const pendingAmountLastMounth = await pendingCreationRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
.where('login_pending_tasks_admin.userId = :id', { id })
.andWhere({
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateLastMonth,
enddate: dateMonth,
}),
})
.getRawOne()
const pendingAmountBeforeLastMounth = await pendingCreationRepository
.createQueryBuilder('login_pending_tasks_admin')
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
.where('login_pending_tasks_admin.userId = :id', { id })
.andWhere({
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
date: dateBeforeLastMonth,
enddate: dateLastMonth,
}),
})
.getRawOne()
// COUNT amount from 2 tables
const usedCreationBeforeLastMonth =
(Number(createdAmountBeforeLastMonth.sum) + Number(pendingAmountBeforeLastMounth.sum)) / 10000
const usedCreationLastMonth =
(Number(createdAmountLastMonth.sum) + Number(pendingAmountLastMounth.sum)) / 10000
const usedCreationMonth =
(Number(createdAmountMonth.sum) + Number(pendingAmountMounth.sum)) / 10000
return [
1000 - usedCreationBeforeLastMonth,
1000 - usedCreationLastMonth,
1000 - usedCreationMonth,
]
}
function isCreationValid(creations: number[], amount: number, creationDate: Date) {
const dateMonth = moment().format('YYYY-MM')
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM')
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM')
const creationDateMonth = moment(creationDate).format('YYYY-MM')
let openCreation
switch (creationDateMonth) {
case dateMonth:
openCreation = creations[2]
break
case dateLastMonth:
openCreation = creations[1]
break
case dateBeforeLastMonth:
openCreation = creations[0]
break
default:
throw new Error('CreationDate is not in last three months')
}
if (openCreation < amount) {
throw new Error(`Open creation (${openCreation}) is less than amount (${amount})`)
}
return true
} }

View File

@ -207,6 +207,7 @@ export class UserResolver {
const loginUserRepository = getCustomRepository(LoginUserRepository) const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findByEmail(userEntity.email) const loginUser = await loginUserRepository.findByEmail(userEntity.email)
const user = new User() const user = new User()
user.id = userEntity.id
user.email = userEntity.email user.email = userEntity.email
user.firstName = userEntity.firstName user.firstName = userEntity.firstName
user.lastName = userEntity.lastName user.lastName = userEntity.lastName
@ -276,6 +277,7 @@ export class UserResolver {
} }
const user = new User() const user = new User()
user.id = userEntity.id
user.email = email user.email = email
user.firstName = loginUser.firstName user.firstName = loginUser.firstName
user.lastName = loginUser.lastName user.lastName = loginUser.lastName

View File

@ -29,7 +29,7 @@ import { elopageWebhook } from '../webhook/elopage'
// TODO implement // TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
const DB_VERSION = '0004-login_server_data' const DB_VERSION = '0005-admin_tables'
const createServer = async (context: any = serverContext): Promise<any> => { const createServer = async (context: any = serverContext): Promise<any> => {
// open mysql connection // open mysql connection

View File

@ -0,0 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginPendingTasksAdmin } from '@entity/LoginPendingTasksAdmin'
@EntityRepository(LoginPendingTasksAdmin)
export class PendingCreationRepository extends Repository<LoginPendingTasksAdmin> {}

View File

@ -0,0 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'
import { TransactionCreation } from '@entity/TransactionCreation'
@EntityRepository(TransactionCreation)
export class TransactionCreationRepository extends Repository<TransactionCreation> {}

View File

@ -4139,6 +4139,11 @@ module-alias@^2.2.2:
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==
moment@^2.29.1:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
ms@2.0.0: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"

View File

@ -0,0 +1,25 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity('login_pending_tasks_admin')
export class LoginPendingTasksAdmin extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ unsigned: true, nullable: false })
userId: number
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
created: Date
@Column({ type: 'datetime', nullable: false })
date: Date
@Column({ length: 256, nullable: true, default: null })
memo: string
@Column({ type: 'bigint', nullable: false })
amount: BigInt
@Column()
moderator: number
}

View File

@ -0,0 +1 @@
export { LoginPendingTasksAdmin } from './0005-admin_tables/LoginPendingTasksAdmin'

View File

@ -12,6 +12,7 @@ import { TransactionSendCoin } from './TransactionSendCoin'
import { User } from './User' import { User } from './User'
import { UserSetting } from './UserSetting' import { UserSetting } from './UserSetting'
import { UserTransaction } from './UserTransaction' import { UserTransaction } from './UserTransaction'
import { LoginPendingTasksAdmin } from './LoginPendingTasksAdmin'
export const entities = [ export const entities = [
Balance, Balance,
@ -28,4 +29,5 @@ export const entities = [
User, User,
UserSetting, UserSetting,
UserTransaction, UserTransaction,
LoginPendingTasksAdmin,
] ]

View File

@ -0,0 +1,29 @@
/* MIGRATION FOR ADMIN INTERFACE
*
* This migration is special since it takes into account that
* the database can be setup already but also may not be.
* Therefore you will find all `CREATE TABLE` statements with
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
* This ensures compatibility for existing or non-existing
* databases.
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE \`login_pending_tasks_admin\` (
\`id\` int UNSIGNED NOT NULL AUTO_INCREMENT,
\`userId\` int UNSIGNED DEFAULT 0,
\`created\` datetime NOT NULL,
\`date\` datetime NOT NULL,
\`memo\` text DEFAULT NULL,
\`amount\` bigint(20) NOT NULL,
\`moderator\` int UNSIGNED DEFAULT 0,
PRIMARY KEY (\`id\`)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4;
`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`DROP TABLE \`login_pending_tasks_admin\`;`)
}