mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge pull request #1135 from gradido/admin_pending_creation
Admin pending creation
This commit is contained in:
commit
ed7c702d0e
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -441,7 +441,7 @@ jobs:
|
||||
report_name: Coverage Admin Interface
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 49
|
||||
min_coverage: 50
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -491,7 +491,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 37
|
||||
min_coverage: 39
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"vue-jest": "^3.0.7",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-toasted": "^1.1.28",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.1.0"
|
||||
},
|
||||
|
||||
BIN
admin/public/img/brand/green.png
Normal file
BIN
admin/public/img/brand/green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@ -3,6 +3,16 @@ import CreationFormular from './CreationFormular.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
verifyLogin: {
|
||||
name: 'success',
|
||||
id: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const stateCommitMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
@ -14,6 +24,12 @@ const mocks = {
|
||||
}),
|
||||
}
|
||||
}),
|
||||
$apollo: {
|
||||
query: apolloMock,
|
||||
},
|
||||
$store: {
|
||||
commit: stateCommitMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -39,6 +55,23 @@ describe('CreationFormular', () => {
|
||||
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', () => {
|
||||
it('has three radio buttons', () => {
|
||||
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
? 'Einzelschöpfung für ' + item.firstName + ' ' + item.lastName + ''
|
||||
: 'Mehrfachschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder'
|
||||
}}
|
||||
{{ item }}
|
||||
</h3>
|
||||
<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
|
||||
@ -24,6 +23,7 @@
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
:value="beforeLastMonth"
|
||||
:disabled="creation[0] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
|
||||
>
|
||||
@ -34,6 +34,7 @@
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
:value="lastMonth"
|
||||
:disabled="creation[1] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(lastMonth, 1, creation[1])"
|
||||
>
|
||||
@ -44,6 +45,7 @@
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
:value="currentMonth"
|
||||
:disabled="creation[2] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(currentMonth, 2, creation[2])"
|
||||
>
|
||||
@ -52,30 +54,29 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="m-4">
|
||||
<b-row class="m-4" v-show="createdIndex">
|
||||
<label>Betrag Auswählen</label>
|
||||
<b-input-group>
|
||||
<template #append>
|
||||
<b-input-group-text><strong class="text-danger">GDD</strong></b-input-group-text>
|
||||
</template>
|
||||
<b-form-input
|
||||
type="number"
|
||||
v-model="value"
|
||||
:min="rangeMin"
|
||||
:max="rangeMax"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
<div>
|
||||
<b-input-group prepend="GDD" append=".00">
|
||||
<b-form-input
|
||||
type="number"
|
||||
v-model="value"
|
||||
:min="rangeMin"
|
||||
:max="rangeMax"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
|
||||
<b-input
|
||||
id="range-2"
|
||||
class="mt-2"
|
||||
v-model="value"
|
||||
type="range"
|
||||
:min="rangeMin"
|
||||
:max="rangeMax"
|
||||
step="10"
|
||||
@load="checkFormForUpdate('range')"
|
||||
></b-input>
|
||||
<b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
|
||||
<b-form-input
|
||||
type="range"
|
||||
v-model="value"
|
||||
:min="rangeMin"
|
||||
:max="rangeMax"
|
||||
step="10"
|
||||
@load="checkFormForUpdate('range')"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
<label>Text eintragen</label>
|
||||
@ -125,6 +126,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { verifyLogin } from '../graphql/verifyLogin'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
export default {
|
||||
name: 'CreationFormular',
|
||||
props: {
|
||||
@ -163,23 +166,25 @@ export default {
|
||||
rangeMax: 1000,
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
long: this.$moment().format('DD/MM/YYYY'),
|
||||
long: this.$moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
lastMonth: {
|
||||
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: {
|
||||
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,
|
||||
isdisabled: true,
|
||||
createdIndex: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Auswählen eines Zeitraumes
|
||||
updateRadioSelected(name, index, openCreation) {
|
||||
this.createdIndex = index
|
||||
// Wenn Mehrfachschöpfung
|
||||
if (this.type === 'massCreation') {
|
||||
// An Creation.vue emitten und radioSelectedMass aktualisieren
|
||||
@ -230,10 +235,11 @@ export default {
|
||||
this.submitObj = [
|
||||
{
|
||||
item: this.itemsMassCreation,
|
||||
datum: this.radioSelected,
|
||||
email: this.item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
amount: this.value,
|
||||
text: this.text,
|
||||
moderator: this.$store.state.moderator,
|
||||
memo: this.text,
|
||||
moderator: this.$store.state.moderator.id,
|
||||
},
|
||||
]
|
||||
alert('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
|
||||
@ -246,18 +252,13 @@ export default {
|
||||
}
|
||||
|
||||
if (this.type === 'singleCreation') {
|
||||
// hinweis das eine einzelne schöpfung ausgeführt wird an (Vorname)
|
||||
alert('SUBMIT CREATION => ' + this.type + ' >> für ' + this.item.firstName + '')
|
||||
// erstellen eines Arrays (submitObj) mit allen Daten
|
||||
this.submitObj = [
|
||||
{
|
||||
item: this.item,
|
||||
datum: this.radioSelected.long,
|
||||
amount: this.value,
|
||||
text: this.text,
|
||||
moderator: this.$store.state.moderator,
|
||||
},
|
||||
]
|
||||
this.submitObj = {
|
||||
email: this.item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
amount: Number(this.value),
|
||||
memo: this.text,
|
||||
moderator: Number(this.$store.state.moderator.id),
|
||||
}
|
||||
|
||||
if (this.pagetype === 'PageCreationConfirm') {
|
||||
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
|
||||
@ -269,22 +270,48 @@ export default {
|
||||
text: this.text,
|
||||
})
|
||||
} else {
|
||||
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
|
||||
alert('EINZEL SCHÖPFUNG ABSENDEN FÜR >> ' + this.item.firstName + '')
|
||||
// $store - offene Schöpfungen hochzählen
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
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.submitObj = null
|
||||
this.createdIndex = null
|
||||
// das creation Formular reseten
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.submitObj = null
|
||||
// das creation Formular reseten
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// das absendeergebniss im string ansehen
|
||||
alert(JSON.stringify(this.submitObj))
|
||||
// das submitObj zurücksetzen
|
||||
this.submitObj = null
|
||||
// das creation Formular reseten
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
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>
|
||||
|
||||
@ -3,11 +3,19 @@ import NavBar from './NavBar.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const storeDispatchMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
state: {
|
||||
openCreations: 1,
|
||||
token: 'valid-token',
|
||||
},
|
||||
dispatch: storeDispatchMock,
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
},
|
||||
}
|
||||
|
||||
@ -27,4 +35,34 @@ describe('NavBar', () => {
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="component-nabvar">
|
||||
<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-collapse id="nav-collapse" is-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="/creation">Mehrfachschöpfung</b-nav-item>
|
||||
<b-nav-item
|
||||
@ -31,21 +34,6 @@ export default {
|
||||
name: 'navbar',
|
||||
methods: {
|
||||
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.$router.push('/logout')
|
||||
},
|
||||
@ -56,3 +44,8 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.navbar-brand-img {
|
||||
height: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -14,11 +14,11 @@
|
||||
{{ overlayText.text2 }}
|
||||
</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 }}
|
||||
</b-button>
|
||||
<b-button
|
||||
size="lg"
|
||||
size="md"
|
||||
variant="success"
|
||||
class="m-3 text-right"
|
||||
@click="overlayOK(overlayBookmarkType, overlayItem)"
|
||||
@ -39,7 +39,7 @@
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button
|
||||
variant="info"
|
||||
size="lg"
|
||||
size="md"
|
||||
@click="editCreationUserTable(row, row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
@ -49,7 +49,7 @@
|
||||
</template>
|
||||
|
||||
<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-else icon="eye-fill" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
@ -68,6 +68,7 @@
|
||||
:item="row.item"
|
||||
:creationUserData="creationData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
|
||||
<b-button size="sm" @click="row.toggleDetails">
|
||||
@ -93,7 +94,7 @@
|
||||
<b-button
|
||||
variant="danger"
|
||||
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
|
||||
size="lg"
|
||||
size="md"
|
||||
@click="overlayShow('remove', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
@ -105,7 +106,7 @@
|
||||
<b-button
|
||||
variant="success"
|
||||
v-show="type === 'PageCreationConfirm'"
|
||||
size="lg"
|
||||
size="md"
|
||||
@click="overlayShow('confirm', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
@ -232,6 +233,9 @@ export default {
|
||||
...data,
|
||||
}
|
||||
},
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
19
admin/src/graphql/createPendingCreation.js
Normal file
19
admin/src/graphql/createPendingCreation.js
Normal 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
|
||||
)
|
||||
}
|
||||
`
|
||||
11
admin/src/graphql/verifyLogin.js
Normal file
11
admin/src/graphql/verifyLogin.js
Normal file
@ -0,0 +1,11 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const verifyLogin = gql`
|
||||
query {
|
||||
verifyLogin {
|
||||
firstName
|
||||
lastName
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -21,6 +21,7 @@ import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import moment from 'vue-moment'
|
||||
import Toasted from 'vue-toasted'
|
||||
|
||||
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
|
||||
|
||||
@ -62,6 +63,18 @@ Vue.use(moment)
|
||||
|
||||
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)
|
||||
|
||||
new Vue({
|
||||
|
||||
@ -3,12 +3,14 @@ import './main'
|
||||
import CONFIG from './config'
|
||||
|
||||
import Vue from 'vue'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import Vuex from 'vuex'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
import moment from 'vue-moment'
|
||||
|
||||
jest.mock('vue')
|
||||
jest.mock('vue-apollo')
|
||||
jest.mock('vuex')
|
||||
jest.mock('vue-i18n')
|
||||
jest.mock('vue-moment')
|
||||
@ -55,6 +57,10 @@ describe('main', () => {
|
||||
expect(InMemoryCache).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls the VueApollo', () => {
|
||||
expect(VueApollo).toBeCalled()
|
||||
})
|
||||
|
||||
it('calls Vue', () => {
|
||||
expect(Vue).toBeCalled()
|
||||
})
|
||||
@ -63,16 +69,16 @@ describe('main', () => {
|
||||
expect(VueI18n).toBeCalled()
|
||||
})
|
||||
|
||||
it.skip('calls BootstrapVue', () => {
|
||||
expect(BootstrapVue).toBeCalled()
|
||||
it('calls BootstrapVue', () => {
|
||||
expect(Vue.use).toBeCalledWith(BootstrapVue)
|
||||
})
|
||||
|
||||
it.skip('calls IconsPlugin', () => {
|
||||
expect(IconsPlugin).toBeCalled()
|
||||
it('calls IconsPlugin', () => {
|
||||
expect(Vue.use).toBeCalledWith(IconsPlugin)
|
||||
})
|
||||
|
||||
it.skip('calls Moment', () => {
|
||||
expect(moment).toBeCalled()
|
||||
it('calls Moment', () => {
|
||||
expect(Vue.use).toBeCalledWith(moment)
|
||||
})
|
||||
|
||||
it.skip('creates a store', () => {
|
||||
|
||||
@ -32,7 +32,13 @@ export default {
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{ key: 'creation', label: 'Creation' },
|
||||
{
|
||||
key: 'creation',
|
||||
label: 'Creation',
|
||||
formatter: (value, key, item) => {
|
||||
return String(value)
|
||||
},
|
||||
},
|
||||
{ key: 'show_details', label: 'Details' },
|
||||
],
|
||||
searchResult: [],
|
||||
|
||||
@ -18,6 +18,9 @@ export const mutations = {
|
||||
token: (state, token) => {
|
||||
state.token = token
|
||||
},
|
||||
moderator: (state, moderator) => {
|
||||
state.moderator = moderator
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
@ -35,7 +38,7 @@ const store = new Vuex.Store({
|
||||
],
|
||||
state: {
|
||||
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
|
||||
moderator: 'Dertest Moderator',
|
||||
moderator: { name: 'Dertest Moderator', id: 0 },
|
||||
openCreations: 0,
|
||||
},
|
||||
// Syncronous mutation of the state
|
||||
|
||||
@ -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"
|
||||
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:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"jest": "^27.2.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "^2.29.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"nodemailer": "^6.6.5",
|
||||
"random-bigint": "^0.0.1",
|
||||
|
||||
19
backend/src/graphql/arg/CreatePendingCreationArgs.ts
Normal file
19
backend/src/graphql/arg/CreatePendingCreationArgs.ts
Normal 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
|
||||
}
|
||||
0
backend/src/graphql/model/CreatePendingCreation.ts
Normal file
0
backend/src/graphql/model/CreatePendingCreation.ts
Normal file
@ -12,6 +12,7 @@ export class User {
|
||||
*/
|
||||
constructor(json?: any) {
|
||||
if (json) {
|
||||
this.id = json.id
|
||||
this.email = json.email
|
||||
this.firstName = json.first_name
|
||||
this.lastName = json.last_name
|
||||
@ -24,6 +25,9 @@ export class User {
|
||||
}
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { Resolver, Query, Arg, Authorized } from 'type-graphql'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { Resolver, Query, Arg, Args, Authorized, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, Raw } from 'typeorm'
|
||||
import { UserAdmin } from '../model/UserAdmin'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
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()
|
||||
export class AdminResolver {
|
||||
@ -11,18 +16,161 @@ export class AdminResolver {
|
||||
async searchUsers(@Arg('searchText') searchText: string): Promise<UserAdmin[]> {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUsers = await loginUserRepository.findBySearchCriteria(searchText)
|
||||
const users = loginUsers.map((loginUser) => {
|
||||
const user = new UserAdmin()
|
||||
user.firstName = loginUser.firstName
|
||||
user.lastName = loginUser.lastName
|
||||
user.email = loginUser.email
|
||||
user.creation = [
|
||||
(Math.floor(Math.random() * 50) + 1) * 20,
|
||||
(Math.floor(Math.random() * 50) + 1) * 20,
|
||||
(Math.floor(Math.random() * 50) + 1) * 20,
|
||||
]
|
||||
return user
|
||||
})
|
||||
const users = await Promise.all(
|
||||
loginUsers.map(async (loginUser) => {
|
||||
const user = new UserAdmin()
|
||||
user.firstName = loginUser.firstName
|
||||
user.lastName = loginUser.lastName
|
||||
user.email = loginUser.email
|
||||
user.creation = await getUserCreations(loginUser.id)
|
||||
return user
|
||||
}),
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
@ -207,6 +207,7 @@ export class UserResolver {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findByEmail(userEntity.email)
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = userEntity.email
|
||||
user.firstName = userEntity.firstName
|
||||
user.lastName = userEntity.lastName
|
||||
@ -276,6 +277,7 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = email
|
||||
user.firstName = loginUser.firstName
|
||||
user.lastName = loginUser.lastName
|
||||
|
||||
@ -29,7 +29,7 @@ import { elopageWebhook } from '../webhook/elopage'
|
||||
// TODO implement
|
||||
// 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> => {
|
||||
// open mysql connection
|
||||
|
||||
5
backend/src/typeorm/repository/PendingCreation.ts
Normal file
5
backend/src/typeorm/repository/PendingCreation.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { LoginPendingTasksAdmin } from '@entity/LoginPendingTasksAdmin'
|
||||
|
||||
@EntityRepository(LoginPendingTasksAdmin)
|
||||
export class PendingCreationRepository extends Repository<LoginPendingTasksAdmin> {}
|
||||
5
backend/src/typeorm/repository/TransactionCreation.ts
Normal file
5
backend/src/typeorm/repository/TransactionCreation.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { TransactionCreation } from '@entity/TransactionCreation'
|
||||
|
||||
@EntityRepository(TransactionCreation)
|
||||
export class TransactionCreationRepository extends Repository<TransactionCreation> {}
|
||||
@ -4139,6 +4139,11 @@ module-alias@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
25
database/entity/0005-admin_tables/LoginPendingTasksAdmin.ts
Normal file
25
database/entity/0005-admin_tables/LoginPendingTasksAdmin.ts
Normal 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
|
||||
}
|
||||
1
database/entity/LoginPendingTasksAdmin.ts
Normal file
1
database/entity/LoginPendingTasksAdmin.ts
Normal file
@ -0,0 +1 @@
|
||||
export { LoginPendingTasksAdmin } from './0005-admin_tables/LoginPendingTasksAdmin'
|
||||
@ -12,6 +12,7 @@ import { TransactionSendCoin } from './TransactionSendCoin'
|
||||
import { User } from './User'
|
||||
import { UserSetting } from './UserSetting'
|
||||
import { UserTransaction } from './UserTransaction'
|
||||
import { LoginPendingTasksAdmin } from './LoginPendingTasksAdmin'
|
||||
|
||||
export const entities = [
|
||||
Balance,
|
||||
@ -28,4 +29,5 @@ export const entities = [
|
||||
User,
|
||||
UserSetting,
|
||||
UserTransaction,
|
||||
LoginPendingTasksAdmin,
|
||||
]
|
||||
|
||||
29
database/migrations/0005-admin_tables.ts
Normal file
29
database/migrations/0005-admin_tables.ts
Normal 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\`;`)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user