ソースを参照

Zeitslots bearbeiten, Teams bearbeiten

tags/v0.9.1
akimmig 4年前
コミット
7a516ac59e
28個のファイルの変更2138行の追加287行の削除
  1. +455
    -241
      client/package-lock.json
  2. +20
    -18
      client/package.json
  3. +88
    -0
      client/src/filter.js
  4. +4
    -0
      client/src/main.js
  5. +9
    -1
      client/src/plugins/auth.js
  6. +13
    -0
      client/src/plugins/router.js
  7. +3
    -1
      client/src/plugins/store.js
  8. +43
    -1
      client/src/views/components/core/Menu.vue
  9. +559
    -0
      client/src/views/components/event/event.vue
  10. +125
    -0
      client/src/views/components/management/dialogs/EditOrganizer.vue
  11. +16
    -16
      client/src/views/components/management/organizer.vue
  12. +1
    -1
      client/src/views/components/management/person.vue
  13. +103
    -0
      client/src/views/components/orga/dialogs/EditEvent.vue
  14. +164
    -0
      client/src/views/components/orga/event.vue
  15. +37
    -0
      server/schema.gql
  16. +4
    -1
      server/src/event/event.module.ts
  17. +4
    -1
      server/src/event/models/Event.ts
  18. +17
    -0
      server/src/event/models/EventApparatus.ts
  19. +11
    -0
      server/src/event/models/Team.ts
  20. +17
    -1
      server/src/event/models/Timeslot.ts
  21. +253
    -2
      server/src/event/resolver/event.mutation.ts
  22. +47
    -0
      server/src/event/resolver/event.subscription.ts
  23. +10
    -1
      server/src/event/resolver/event.ts
  24. +49
    -0
      server/src/event/resolver/eventapparatus.ts
  25. +2
    -1
      server/src/organizer/organizer.module.ts
  26. +61
    -0
      server/src/organizer/resolver/organizer.mutation.ts
  27. +22
    -0
      server/src/organizer/resolver/organizer.subscription.ts
  28. +1
    -1
      server/src/person/resolver/person.subscription.ts

+ 455
- 241
client/package-lock.json
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 20
- 18
client/package.json ファイルの表示

@@ -10,41 +10,43 @@
"dependencies": {
"@fortawesome/fontawesome-pro": "^5.15.3",
"@mdi/font": "^5.9.55",
"@vue/composition-api": "1.0.0-rc.8",
"@vue/composition-api": "1.0.0-rc.12",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"core-js": "^3.6.5",
"core-js": "^3.15.1",
"jot": "git+https://github.com/joshdata/jot.git",
"js-cookie": "^2.2.1",
"lodash": "^4.17.21",
"subscriptions-transport-ws": "^0.9.18",
"moment": "^2.29.1",
"subscriptions-transport-ws": "^0.9.19",
"uuid": "^3.4.0",
"vue": "^2.6.11",
"vue": "^2.6.14",
"vue-apollo": "^3.0.7",
"vue-cookies": "^1.7.4",
"vue-router": "^3.2.0",
"vuetify": "^2.4.0",
"vue-router": "^3.5.2",
"vuedraggable": "^2.24.3",
"vuetify": "^2.5.5",
"vuetify-image-input": "^19.2.2",
"vuex": "^3.4.0"
"vuex": "^3.6.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/cli-plugin-babel": "~4.5.13",
"@vue/cli-plugin-eslint": "~4.5.13",
"@vue/cli-plugin-router": "~4.5.13",
"@vue/cli-plugin-vuex": "~4.5.13",
"@vue/cli-service": "~4.5.13",
"@vue/eslint-config-standard": "^5.1.2",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.32.0",
"sass": "^1.35.1",
"sass-loader": "^10.0.0",
"vue-cli-plugin-vuetify": "~2.4.0",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.7.0"
"vue-cli-plugin-vuetify": "~2.4.1",
"vue-template-compiler": "^2.6.14",
"vuetify-loader": "^1.7.2"
}
}

+ 88
- 0
client/src/filter.js ファイルの表示

@@ -0,0 +1,88 @@
import Vue from 'vue'
import moment from 'moment'
moment.locale('de')

Vue.filter('ts2date', value => {
if (value) {
return (new Date(parseInt(value))).toISOString()
}
})

Vue.filter('date', value => {
if (value) {
return moment(String(value)).format('DD.MM.YYYY')
}
})

Vue.filter('dateformat', (value, format) => {
if (!format) format = 'DD.MM.YYYY HH:mm'

if (value) {
return moment(String(value)).format(format)
}
})

Vue.filter('ms2time', (value) => {
if (!value) value = 0
value = value / 1000

const min = Math.floor(value / 60)
const s = value - 60 * min

let ret = ''

if (min) {
ret += min + 'm '
if (s < 10) ret += '0'
}
ret += s.toFixed(2) + 's'

return ret
})

Vue.filter('isodate', value => {
if (value) {
return moment(String(value)).format()
}
return null
})

Vue.filter('float2', function (value) {
const float = parseFloat(value)
if (isNaN(float) || float === 0) return ''
return float.toFixed(2)
})

Vue.filter('float2force', function (value) {
const float = parseFloat(value)
if (isNaN(float)) return ''
return float.toFixed(2)
})

Vue.filter('float2_0', function (value) {
const float = parseFloat(value)
if (isNaN(float) || float === 0) return '0.00'
return float.toFixed(2)
})

Vue.filter('float1', function (value) {
const float = parseFloat(value)
if (isNaN(float) || float === 0) return ''
return float.toFixed(1)
})

Vue.filter('int', function (value) {
const int = parseInt(value)
if (isNaN(int) || int === 0) return ''
return int.toString()
})

Vue.filter('int_0', function (value) {
const int = parseInt(value)
if (isNaN(int) || int === 0) return '0'
return int.toString()
})

Vue.filter('nl2br', function (value) {
return value?.replace(/\n/g, '<br/>')
})

+ 4
- 0
client/src/main.js ファイルの表示

@@ -12,6 +12,10 @@ import './plugins/cookies'
import './plugins/compositionAPI'
import './plugins/base'

import './filter'

Vue.component('Draggable', () => import('vuedraggable'))

Vue.component('E403', () => import('./components/e403.vue'))
Vue.component('DateSelector', () => import('./components/DateSelector.vue'))
Vue.component('DateTimeSelector', () => import('./components/DateTimeSelector.vue'))

+ 9
- 1
client/src/plugins/auth.js ファイルの表示

@@ -5,5 +5,13 @@ export const useAuth = (context) => {
return context.root.$store.getters.isMaster
})

return { isMaster }
const isAdmin = (id) => {
return context.root.$store.getters.isAdmin(id)
}

const isOrga = (id) => {
return context.root.$store.getters.isOrga(id)
}

return { isMaster, isAdmin, isOrga }
}

+ 13
- 0
client/src/plugins/router.js ファイルの表示

@@ -28,12 +28,25 @@ const routes = [
path: 'management/organizer',
name: 'Veranstalter bearbeiten',
component: () => import('../views/components/management/organizer')
},
{
path: 'orga/:id',
name: 'Übersicht Events',
component: () => import('../views/components/orga/event'),
props: true
},
{
path: 'event/:id',
name: 'Event',
component: () => import('../views/components/event/event'),
props: true
}
]
}
]

const router = new VueRouter({
mode: 'history',
routes
})


+ 3
- 1
client/src/plugins/store.js ファイルの表示

@@ -35,6 +35,8 @@ export default new Vuex.Store({
getters: {
profile: (state) => state.profile || {},
isMaster: (state) => !!state.profile?.master,
isLogin: (state) => !!state.profile?.token
isLogin: (state) => !!state.profile?.token,
isAdmin: (state) => (id) => !!state.profile?.adminOf?.find(a => a._id === id),
isOrga: (state) => (id) => !!state.profile?.adminOf?.find(a => a._id === id) || !!state.profile?.organizerOf?.find(a => a._id === id)
}
})

+ 43
- 1
client/src/views/components/core/Menu.vue ファイルの表示

@@ -32,6 +32,42 @@
]
}"
/>
<base-item
v-for="a in adminOf"
:key="'admin-'+a._id"
:item="{
// group: '/admin',
icon: 'fa-user-crown',
title: 'Verwaltung',
subtitle: a.name,
to: '/admin/' + a._id
/* children: [
{
title: 'Nutzer verwalten',
icon: 'fa-users',
to: 'person'
}
] */
}"
/>
<base-item
v-for="o in organizerOf"
:key="'orga-'+o._id"
:item="{
// group: '/orga',
icon: 'fa-calendar-edit',
title: 'Organisation',
subtitle: o.name,
to: '/orga/' + o._id
/* children: [
{
title: 'Events verwalten',
icon: 'fa-calendar-day',
to: 'event/' + o._id
}
] */
}"
/>
</div>
</template>

@@ -42,7 +78,13 @@ export default {
name: 'Menu',

computed: {
...mapGetters(['profile', 'isMaster'])
...mapGetters(['profile', 'isMaster']),
adminOf () {
return this.profile?.adminOf
},
organizerOf () {
return [...(this.profile?.adminOf || []), ...(this.profile?.organizerOf || [])]
}
}
}
</script>

+ 559
- 0
client/src/views/components/event/event.vue ファイルの表示

@@ -0,0 +1,559 @@
<template>
<v-container
fluid
tag="section"
>
<v-tabs
v-model="tab"
background-color="primary lighten-1"
fixed-tabs
>
<v-tab
href="#apparatus"
>
Geräte
</v-tab>
<v-tab
href="#timeslot"
>
Zeitslots
</v-tab>
<v-tab
href="#result"
>
Ergebnisse
</v-tab>
</v-tabs>
<v-tabs-items
v-model="tab"
>
<v-tab-item
value="apparatus"
>
<v-card
flat
class="ma-0"
>
<v-card-title class="text-h3">
Geräte
<v-spacer />
<v-menu>
<template #activator="{on, attrs}">
<v-btn
icon
v-bind="attrs"
v-on="on"
>
<v-icon>
fa-plus
</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="a in geraete"
:key="a._id"
@click="addApparatus(a._id)"
>
<v-list-item-title>{{ a.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-title>
<v-card-text>
<draggable
:list="[]"
handle=".handle"
tag="div"
class="row row-dense"
v-bind="dragOptions"
@end="orderApparatus"
>
<v-col
v-for="(a) in (Event || {}).apparatus"
:key="a.name"
cols="6"
lg="3"
class="item"
>
<v-card>
<v-card-title class="text-h4">
<v-icon class="handle mr-4">far fa-bars</v-icon>
{{ a.apparatus.name }}
</v-card-title>
<v-card-text>
<p><b>Elemente: </b> {{ a.elements }}</p>
<p><b>Bonus: </b> {{ a.bonus | float2_0 }}</p>
<p><b>Malus: </b> {{ a.malus | float2_0 }}</p>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn icon>
<v-icon>far fa-pencil</v-icon>
</v-btn>
<v-btn
icon
@click="deleteApparatus(a._apparatus)"
>
<v-icon>far fa-trash</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</draggable>
</v-card-text>
</v-card>
</v-tab-item>
<v-tab-item
value="timeslot"
>
<v-card
flat
>
<v-btn
v-if="ts.selected.length > 0"
absolute
top
right
fab
small
style="margin-right:60px;"
@click="deleteTimeslots"
>
<v-icon>
fa-minus
</v-icon>
</v-btn>
<v-btn
absolute
top
right
fab
small
@click="ts.dialog.open = true"
>
<v-icon>
fa-plus
</v-icon>
</v-btn>
<v-data-table
:headers="ts.headers"
:items="timeslots"
sort-by="time"
:items-per-page="-1"
mobile-breakpoint="0"
>
<template #header.select>
<v-checkbox
@change="(v) => !v ? ts.selected = [] : ts.selected = timeslots.map(t => t._id)"
/>
</template>
<template #item.select="{item}">
<v-checkbox
v-if="!item.team"
v-model="ts.selected"
:value="item._id"
multiple
/>
</template>
<template #item.time="{item}">
{{ `1900-01-01 ${item.time}` | dateformat('HH:mm') }}
</template>
<template #item.end="{item}">
{{ `1900-01-01 ${addMinutes(item.time, item.duration)}` | dateformat('HH:mm') }}
</template>
<template #item.team="{item}">
<v-btn
v-if="item.team"
text
x-small
style="float: left;"
@click="deleteTeam(item._id)"
>
<v-icon small>
far fa-trash-alt
</v-icon>
</v-btn>
<base-edit-dialog
:value="item.team ? item.team.name : ''"
label="Team hinzufügen"
@input="(v) => addTeam(item._id, v)"
/>
</template>
</v-data-table>
</v-card>
<base-material-dialog
v-model="ts.dialog.open"
title="Zeitslots hinzufügen"
color="rgb(255, 4, 29)"
:actions="['save','cancel']"
@save="addTimeslots"
@close="ts.dialog.open = false"
@esc="ts.dialog.open = false"
>
<v-row>
<v-col
cols="12"
sm="6"
md="3"
>
<v-menu
ref="beginn"
v-model="ts.dialog.beginnmenu"
:close-on-content-click="false"
:nudge-right="40"
:return-value.sync="ts.dialog.beginn"
transition="scale-transition"
offset-y
max-width="290px"
min-width="290px"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="ts.dialog.beginn"
label="Erster Zeitslot"
prepend-icon="far fa-clock"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<v-time-picker
v-if="ts.dialog.beginnmenu"
v-model="ts.dialog.beginn"
full-width
format="24hr"
@click:minute="$refs.beginn.save(`${ts.dialog.beginn}:00`)"
></v-time-picker>
</v-menu>
</v-col>
<v-col
cols="12"
sm="6"
md="3"
>
<v-menu
ref="ende"
v-model="ts.dialog.endemenue"
:close-on-content-click="false"
:nudge-right="40"
:return-value.sync="ts.dialog.ende"
transition="scale-transition"
offset-y
max-width="290px"
min-width="290px"
>
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="ts.dialog.ende"
label="Bis"
prepend-icon="far fa-clock"
readonly
v-bind="attrs"
v-on="on"
></v-text-field>
</template>
<v-time-picker
v-if="ts.dialog.endemenue"
v-model="ts.dialog.ende"
full-width
format="24hr"
@click:minute="$refs.ende.save(`${ts.dialog.ende}:00`)"
></v-time-picker>
</v-menu>
</v-col>
<v-col
cols="12"
sm="6"
md="3"
>
<v-text-field
v-model="ts.dialog.duration"
label="Dauer eines Zeitslots (in min)"
type="number"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="3"
>
<v-text-field
v-model="ts.dialog.pause"
label="Pause nach einem Zeitslot (in min)"
type="number"
/>
</v-col>
</v-row>

<h3>Es werden folgende {{ newtimeslots.length }} Zeitslots angelegt:</h3>
<v-data-table
:headers="ts.dialog.headers"
:items="newtimeslots"
:items-per-page="-1"
>
<template #item.time="{item}">
{{ `1900-01-01 ${item.time}` | dateformat('HH:mm') }}
</template>
<template #item.end="{item}">
{{ `1900-01-01 ${addMinutes(item.time, item.duration)}` | dateformat('HH:mm') }}
</template>
</v-data-table>
</base-material-dialog>
</v-tab-item>
<v-tab-item
value="result"
>
Ergebnisse
</v-tab-item>
</v-tabs-items>
</v-container>
</template>

<script>
import gql from 'graphql-tag'
import moment from 'moment'

const query = '_id name date _organizer apparatus { _apparatus apparatus { name logo } elements bonus malus } timeslots { _id time duration team { _id name } }'

export default {
name: 'Event',

props: {
id: {
type: String,
required: true
}
},

data: () => ({
Event: null,
ApparatusFind: [],
tab: 'timeslot',
ts: {
headers: [
{
text: '',
value: 'select',
sortable: false,
width: 75
},
{
text: 'Beginn',
value: 'time',
sortable: false
},
{
text: 'Ende',
value: 'end',
sortable: false
},
{
text: 'Team',
value: 'team',
sortable: false
},
{
text: 'Ergebnis',
value: 'result',
sortable: false
}
],
selected: [],
dialog: {
open: false,
beginn: '09:00:00',
ende: '12:00:00',
duration: 4,
pause: 1,
beginnmenue: false,
endemenue: false,
headers: [
{
text: 'Beginn',
value: 'time',
sortable: false
},
{
text: 'Ende',
value: 'end',
sortable: false
}
]
}
}
}),

computed: {
dragOptions () {
return {
animation: 200,
disabled: false,
ghostClass: 'ghost'
}
},
geraete () {
return this.ApparatusFind?.slice()?.sort((a, b) => a.name < b.name ? -1 : 1)?.filter(g => !this.Event?.apparatus.find(a => a._apparatus === g._id)) || []
},
timeslots () {
return this.Event?.timeslots || []
},
newtimeslots () {
const neu = []

if (this.ts.dialog.duration <= 0) return neu

for (let t = this.ts.dialog.beginn; t < this.ts.dialog.ende; t = moment(`1900-01-01 ${t}`).add(parseInt(this.ts.dialog.duration) + parseInt(this.ts.dialog.pause), 'minutes').format('HH:mm:ss')) {
neu.push(t)
}

return neu.filter(n => !this.timeslots.find(o => o.time === n && o.duration === parseInt(this.ts.dialog.duration))).map(t => ({ time: t, duration: this.ts.dialog.duration }))
}
},

methods: {
addApparatus (id) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $apparatus: UUID!) {
EventAddApparatus(id: $id, apparatus: $apparatus) { ${query} }
}`,
variables: {
id: this.id,
apparatus: id
}
})
},
deleteApparatus (id) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $apparatus: UUID!) {
EventDeleteApparatus(id: $id, apparatus: $apparatus) { ${query} }
}`,
variables: {
id: this.id,
apparatus: id
}
})
},
orderApparatus (a) {
const src = a.oldIndex
const target = a.newIndex

if (src === target) return

this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $src: Int!, $target: Int!) {
EventOrderApparatus(id: $id, src: $src, target: $target) { ${query} }
}`,
variables: {
id: this.id,
src,
target
}
}).then((r) => {
this.Event.apparatus = r.data.EventOrderApparatus.apparatus
})
},
addTimeslots () {
if (this.timeslots.length > 0) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $timeslots: [ITimeslot!]!) {
EventAddTimeslots(id: $id, timeslots: $timeslots) { ${query} }
}`,
variables: {
id: this.id,
timeslots: this.newtimeslots
}
}).then((r) => {
this.Event.timeslots = r.data.EventAddTimeslots.timeslots
})
}

this.ts.dialog.open = false
},
deleteTimeslots () {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $timeslots: [UUID!]!) {
EventDeleteTimeslots(id: $id, timeslots: $timeslots) { ${query} }
}`,
variables: {
id: this.id,
timeslots: this.ts.selected
}
}).then((r) => {
this.Event.timeslots = r.data.EventDeleteTimeslots.timeslots
this.ts.selected = []
})
},
addMinutes (time, min) {
return moment(`1900-01-01 ${time}`).add(min, 'minutes').format('HH:mm:ss')
},
addTeam (timeslot, name) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $timeslot: UUID!, $name: String!) {
EventAddTeam(id: $id, timeslot: $timeslot, name: $name) { ${query} }
}`,
variables: {
id: this.id,
timeslot,
name
}
}).then((r) => {
this.Event.timeslots = r.data.EventAddTeam.timeslots
this.ts.selected = []
})
},
async deleteTeam (timeslot) {
if (!this.timeslots.find(t => t._id === timeslot)?.team?.ergebnis || await this.$root.$children[0].$refs.confirm.open('Löschen', 'Bei diesem Team sind bereits Ergebnisse hinterlegt, wirklich löschen?')) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $timeslot: UUID!) {
EventDeleteTeam(id: $id, timeslot: $timeslot) { ${query} }
}`,
variables: {
id: this.id,
timeslot
}
}).then((r) => {
this.Event.timeslots = r.data.EventDeleteTeam.timeslots
this.ts.selected = []
})
}
}
},

apollo: {
Event: {
query: gql`query($event: UUID!) { Event(id: $event) { ${query} } }`,
variables () {
return {
event: this.id
}
},
subscribeToMore: {
document: gql`subscription($id: UUID!) { EventUpdated(id: $id) { ${query} } }`,
variables () {
return {
id: this.id
}
}
}
},
ApparatusFind: {
query: gql`query { ApparatusFind { _id name }}`
}
}
}
</script>

<style scoped>
.apparatus {
width: calc(25% - 20px);
margin: 10px;
float: left;
}

.draggable:after {
display: block;
content: '';
clear: both;
}
</style>

+ 125
- 0
client/src/views/components/management/dialogs/EditOrganizer.vue ファイルの表示

@@ -0,0 +1,125 @@
<template>
<base-material-dialog
:value="value"
icon="mdi-home"
title="Person bearbeiten"
:sub-title="id ? id : 'NEU'"
color="rgb(255, 4, 29)"
:actions="[doc && isMaster ? 'del' : '', isMaster ? 'save' : '', 'cancel']"
@del="del"
@save="save"
@close="close"
@esc="close"
>
<v-row v-if="!isMaster">
Kein Zugriff! {{ isMaster }}
</v-row>
<v-row v-else>
<v-col
cols="12"
sm="6"
>
<v-text-field
v-model="data.name"
label="Name"
/>
</v-col>
<v-col
cols="4"
sm="2"
>
<v-text-field
v-model="data.plz"
label="PLZ"
/>
</v-col>
<v-col
cols="8"
sm="4"
>
<v-text-field
v-model="data.ort"
label="Ort"
/>
</v-col>
<v-col
cols="6"
>
<h3>Administratoren:</h3>
{{ admins }}
</v-col>
<v-col
cols="6"
>
<h3>Veranstalter:</h3>
{{ organizers }}
</v-col>
</v-row>
</base-material-dialog>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import { useEditDialog } from '@/plugins/editdialog'

export default {
name: 'EditOrganizer',
props: {
value: {
type: Boolean,
required: true
},
id: {
type: String,
default: null
}
},

setup (props, context) {
return {
...useAuth(context),
...useEditDialog(props, context, 'Organizer(id: $id) { _id name plz ort admins { _id givenName familyName } organizers { _id givenName familyName } }', ['name', 'plz', 'ort'])
}
},

computed: {
admins () {
return this.data.admins?.map(e => e.familyName + ', ' + e.givenName)?.join('<br>')
},
organizers () {
return this.data.organizers?.map(e => e.familyName + ', ' + e.givenName)?.join('<br>')
}
},

methods: {
update () {
return {
mutation: `mutation($id: UUID!, $name: String, $plz: Int, $ort: String) {
OrganizerUpdate(id: $id, name: $name, plz: $plz, ort: $ort) {
_id name plz ort
}
}`,
variables: {
plz: parseInt(this.data.plz) || null
}
}
},
create () {
return {
mutation: `mutation($name: String!, $plz: Int, $ort: String) {
OrganizerCreate(name: $name, plz: $plz, ort: $ort) {
_id name plz ort
}
}`,
variables: {
plz: parseInt(this.data.plz) || null
}
}
}
}
}
</script>

<style lang="sass">
$dialog-elevation: 0
</style>

+ 16
- 16
client/src/views/components/management/organizer.vue ファイルの表示

@@ -38,23 +38,23 @@
@click:row="open"
/>
</v-card>
<!--edit-people
<edit-organizer
:id="dialog.id"
v-model="dialog.open"
/-->
/>
</v-container>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import gql from 'graphql-tag'
// import { updateQuery, deleteQuery } from '@/graphql'
import { updateQuery, deleteQuery } from '@/plugins/graphql'

export default {
name: 'People',

components: {
// EditPeople: () => import('./dialogs/EditPeople'),
EditOrganizer: () => import('./dialogs/EditOrganizer')
},

setup (props, context) {
@@ -91,20 +91,20 @@ export default {

apollo: {
OrganizerFind: {
query: gql`query { OrganizerFind { _id name plz ort } }`
/* subscribeToMore: {
document: gql`subscription { PersonUpdated { _id vorname nachname geburtstag dtbid gymnet { id } email { email typ } tel { nummer typ } adresse { strasse hausnummer plz ort land typ } turnportal { typ gueltigvon gueltigbis lastcheck sperre festgeturnt verein vermerk } } }`,
updateQuery: updateQuery('PersonList', 'PersonUpdated')
} */
}
/* $subscribe: {
query: gql`query { OrganizerFind { _id name plz ort admins { _id givenName familyName } organizers { _id givenName familyName } } }`,
subscribeToMore: {
document: gql`subscription { OrganizerUpdated { _id name plz ort admins { _id givenName familyName } organizers { _id givenName familyName } } }`,
updateQuery: updateQuery('OrganizerFind', 'OrganizerUpdated')
}
},
$subscribe: {
PersonDeleted: {
query: gql`subscription { PersonDeleted }`,
query: gql`subscription { OrganizerDeleted }`,
result (id) {
deleteQuery('PersonList', 'PersonUpdated', this.PersonList, id)
},
},
}, */
deleteQuery('OrganizerFind', 'OrganizerDeleted', this.OrganizerFind, id)
}
}
}
},

methods: {

+ 1
- 1
client/src/views/components/management/person.vue ファイルの表示

@@ -89,7 +89,7 @@ export default {
query: gql`query { PersonFind { _id familyName givenName adminOf { name plz ort } organizerOf { name plz ort } } }`,
subscribeToMore: {
document: gql`subscription { PersonUpdated { _id familyName givenName adminOf { name plz ort } organizerOf { name plz ort } } }`,
updateQuery: updateQuery('PersonList', 'PersonUpdated')
updateQuery: updateQuery('PersonFind', 'PersonUpdated')
}
},
$subscribe: {

+ 103
- 0
client/src/views/components/orga/dialogs/EditEvent.vue ファイルの表示

@@ -0,0 +1,103 @@
<template>
<base-material-dialog
:value="value"
icon="mdi-home"
title="Event bearbeiten"
:sub-title="id ? id : 'NEU'"
color="rgb(255, 4, 29)"
:actions="[doc && orga ? 'del' : '', !id || orga ? 'save' : '', 'cancel']"
@del="del"
@save="save"
@close="close"
@esc="close"
>
<v-row v-if="id && (!data || !data._organizer)">
Warte auf Daten...
</v-row>
<v-row v-else-if="id && !isOrga(data._organizer)">
Kein Zugriff!
</v-row>
<v-row v-else>
<v-col
cols="12"
>
<date-selector
v-model="data.date"
label="Datum"
/>
</v-col>
<v-col
cols="12"
>
<v-text-field
v-model="data.name"
label="Name"
/>
</v-col>
</v-row>
</base-material-dialog>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import { useEditDialog } from '@/plugins/editdialog'

export default {
name: 'EditEvent',
props: {
value: {
type: Boolean,
required: true
},
id: {
type: String,
default: null
},
organizer: {
type: String,
required: true
}
},

setup (props, context) {
return {
...useAuth(context),
...useEditDialog(props, context, 'Event(id: $id) { _id name date _organizer }', ['name', 'date'])
}
},

computed: {
orga () {
return this.isOrga(this.data?._organizer)
}
},

methods: {
update () {
return {
mutation: `mutation($id: UUID!, $name: String, $date: Date) {
EventUpdate(id: $id, name: $name, date: $date) {
_id name date
}
}`
}
},
create () {
return {
mutation: `mutation($name: String!, $date: Date!, $organizer: UUID!) {
EventCreate(name: $name, date: $date, organizer: $organizer) {
_id name date _organizer
}
}`,
variables: {
organizer: this.organizer
}
}
}
}
}
</script>

<style lang="sass">
$dialog-elevation: 0
</style>

+ 164
- 0
client/src/views/components/orga/event.vue ファイルの表示

@@ -0,0 +1,164 @@
<template>
<e403 v-if="!isOrga(id)" />
<v-container
v-else
fluid
tag="section"
>
<v-card
flat
>
<v-btn
absolute
top
right
fab
small
@click="open(null)"
>
<v-icon>
fa-plus
</v-icon>
</v-btn>

<v-col>
<v-text-field
v-model="filter"
label="Filter"
/>
</v-col>

<v-data-table
:headers="headers"
:items="EventFind"
sort-by="date"
sort-desc
:items-per-page="15"
mobile-breakpoint="0"
:search="filter"
@click:row="open"
>
<template #item.date="{item}">
{{ item.date | date }}
</template>
<template #item.open="{item}">
<v-btn
fab
small
@click.stop="start(item)"
>
<v-icon>far fa-play</v-icon>
</v-btn>
</template>
</v-data-table>
</v-card>
<edit-event
:id="dialog.id"
v-model="dialog.open"
:organizer="id"
/>
</v-container>
</template>

<script>
import { useAuth } from '@/plugins/auth'
import gql from 'graphql-tag'
import { deleteQuery, updateQuery } from '@/plugins/graphql'

export default {
name: 'event',

components: {
EditEvent: () => import('./dialogs/EditEvent')
},

setup (props, context) {
return {
...useAuth(context)
}
},

props: {
id: {
type: String,
required: true
}
},

data: () => ({
EventFind: [],
headers: [
{
text: 'Datum',
value: 'date',
sortable: false
},
{
text: 'Name',
value: 'name',
sortable: false
},
{
text: 'Öffnen',
value: 'open',
sortable: false,
width: 100,
align: 'center'
}
],
dialog: {
open: false,
id: null
},
filter: ''
}),

apollo: {
EventFind: {
query: gql`query($organizer: UUID!) { EventFind(organizer: $organizer) { _id name date _organizer } }`,
variables () {
return {
organizer: this.id
}
},
subscribeToMore: {
document: gql`subscription($organizer: UUID!) { EventUpdated(organizer: $organizer) { _id name date _organizer } }`,
variables () {
return {
organizer: this.id
}
},
updateQuery: updateQuery('EventFind', 'EventUpdated')
}
},
$subscribe: {
EventDeleted: {
query: gql`subscription($organizer: UUID!) { EventDeleted(organizer: $organizer) }`,
variables () {
return {
organizer: this.id
}
},
result (id) {
deleteQuery('EventFind', 'EventDeleted', this.EventFind, id)
}
}
}
},

methods: {
open (item) {
this.dialog.open = true
this.dialog.id = item?._id
},
start (item) {
if (!item?._id) return
this.$router.push(`/event/${item._id}`)
}
}
}
</script>

<style scoped>

</style>

+ 37
- 0
server/schema.gql ファイルの表示

@@ -43,18 +43,34 @@ type Apparatus {
logo: String
}

type Team {
_id: UUID!
name: String!
}

type Timeslot {
_id: UUID!
time: Time!
duration: Int!
team: Team
}

"""A time string HH:MM:SS"""
scalar Time

type EventApparatus {
_apparatus: UUID!
elements: Int!
bonus: Int!
malus: Int!
apparatus: Apparatus!
}

type Event {
_id: UUID!
name: String
date: Date!
apparatus: [EventApparatus!]!
timeslots: [Timeslot!]!
_organizer: UUID!
organizer: Organizer!
@@ -86,7 +102,24 @@ type Mutation {
ApparatusCreate(logo: String, name: String!): Apparatus!
ApparatusDelete(id: UUID!): UUID!
OrganizerRegister(ort: String, plz: Int, name: String!): Organizer!
OrganizerUpdate(ort: String, plz: Int, name: String, id: UUID!): Organizer!
OrganizerCreate(ort: String, plz: Int, name: String!): Organizer!
OrganizerDelete(id: UUID!): UUID!
EventCreate(name: String, date: Date!, organizer: UUID!): Event!
EventUpdate(date: Date, name: String, id: UUID!): Event!
EventAddApparatus(apparatus: UUID!, id: UUID!): Event!
EventDeleteApparatus(apparatus: UUID!, id: UUID!): Event!
EventOrderApparatus(target: Int!, src: Int!, id: UUID!): Event!
EventAddTimeslots(timeslots: [ITimeslot!]!, id: UUID!): Event!
EventDeleteTimeslots(timeslots: [UUID!]!, id: UUID!): Event!
EventAddTeam(name: String!, timeslot: UUID!, id: UUID!): Event!
EventDeleteTeam(timeslot: UUID!, id: UUID!): Event!
EventDelete(id: UUID!): UUID!
}

input ITimeslot {
time: Time!
duration: Int!
}

type Subscription {
@@ -94,4 +127,8 @@ type Subscription {
PersonDeleted: UUID!
ApparatusUpdated: Apparatus
ApparatusDeleted: UUID!
OrganizerUpdated: Organizer!
OrganizerDeleted: UUID!
EventUpdated(organizer: UUID, id: UUID): Event!
EventDeleted(organizer: UUID!): UUID!
}

+ 4
- 1
server/src/event/event.module.ts ファイルの表示

@@ -3,12 +3,15 @@ import {EventResolver} from './resolver/event'
import {EventService} from './event.service'
import {EventResolverQ} from './resolver/event.query'
import {EventResolverM} from './resolver/event.mutation'
import {EventResolverS} from './resolver/event.subscription'
import {EventApparatusResolver} from './resolver/eventapparatus'

@Module({
providers: [
EventResolverQ, EventResolverM,
EventResolverQ, EventResolverM, EventResolverS,
EventService,
EventResolver,
EventApparatusResolver,
],
})
export class EventModule {}

+ 4
- 1
server/src/event/models/Event.ts ファイルの表示

@@ -2,7 +2,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { UUID } from '../../global/scalars/UUID';
import { Date } from '../../global/scalars/Date'
import { Timeslot } from './Timeslot'
import { Organizer } from '../../organizer/models/Organizer'
import {EventApparatus} from './EventApparatus'

@ObjectType()
export class Event {
@@ -15,6 +15,9 @@ export class Event {
@Field(() => Date, { nullable: false })
date: Date

@Field(() => [EventApparatus], { nullable: false })
apparatus: EventApparatus[]

@Field(() => [Timeslot], { nullable: false })
timeslots: Timeslot[]


+ 17
- 0
server/src/event/models/EventApparatus.ts ファイルの表示

@@ -0,0 +1,17 @@
import {Field, Int, ObjectType} from '@nestjs/graphql';
import { UUID } from '../../global/scalars/UUID';

@ObjectType()
export class EventApparatus {
@Field(() => UUID,{ nullable: false })
_apparatus: UUID

@Field(() => Int, { nullable: false })
elements: number

@Field(() => Int, { nullable: false })
bonus: number

@Field(() => Int, { nullable: false })
malus: number
}

+ 11
- 0
server/src/event/models/Team.ts ファイルの表示

@@ -0,0 +1,11 @@
import {Field, ObjectType} from '@nestjs/graphql';
import { UUID } from '../../global/scalars/UUID';

@ObjectType()
export class Team {
@Field(() => UUID,{ nullable: false })
_id: UUID

@Field(() => String,{ nullable: false })
name: string
}

+ 17
- 1
server/src/event/models/Timeslot.ts ファイルの表示

@@ -1,6 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
import {Field, InputType, Int, ObjectType} from '@nestjs/graphql';
import { UUID } from '../../global/scalars/UUID';
import { Time } from '../../global/scalars/Time'
import {Team} from './Team'

@ObjectType()
export class Timeslot {
@@ -9,4 +10,19 @@ export class Timeslot {

@Field(() => Time, { nullable: false })
time: Time

@Field(() => Int, { nullable: false })
duration: number

@Field(() => Team, { nullable: true })
team?: Team
}

@InputType()
export class ITimeslot {
@Field(() => Time, { nullable: false })
time: Time

@Field(() => Int, { nullable: false })
duration: number
}

+ 253
- 2
server/src/event/resolver/event.mutation.ts ファイルの表示

@@ -1,4 +1,4 @@
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import {Args, Context, Int, Mutation, Resolver} from '@nestjs/graphql';
import { Event } from '../models/Event';
import { Client } from '../../client';
import { EventService } from '../event.service';
@@ -6,6 +6,9 @@ import { HttpException } from '@nestjs/common';
import {Date} from '../../global/scalars/Date'
import {UUID} from '../../global/scalars/UUID'
import {OrganizerService} from '../../organizer/organizer.service'
import {pubsub} from '../../main'
import {ITimeslot} from '../models/Timeslot'
import { v4 as uuid } from 'uuid'

@Resolver(() => Event)
export class EventResolverM {
@@ -31,6 +34,254 @@ export class EventResolverM {
throw new HttpException('Organizer-ID not found!', 404);
}

return this.service.create(client, date, organizer, name);
const neu = await this.service.create(client, date, organizer, name);

pubsub.publish('EventUpdated', { EventUpdated: neu });

return neu;
}

@Mutation(() => Event, { nullable: false })
async EventUpdate(
@Context('client') client, Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('name', { type: () => String, nullable: true }) name: string,
@Args('date', { type: () => Date, nullable: true }) date: Date
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

const set: {
name?: string,
date?: Date,
} = {};

if (name !== undefined && name !== tmp.name) set.name = name;
if (date !== undefined && date !== tmp.date) set.date = date;

if (Object.values(set).length === 0) return tmp;

const neu = await this.service.update(client, tmp._id, {$set: set});
pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventAddApparatus(
@Context('client') client, Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('apparatus', { type: () => UUID, nullable: false }) apparatus: UUID,
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

let neu: Event;

if (!tmp.apparatus) {
neu = await this.service.update(client, tmp._id, {$set: {apparatus: [{ _apparatus: apparatus, elements: 3, bonus: 3.00, malus: 5.00 }]}});
} else {
neu = await this.service.update(client, tmp._id, {$push: { apparatus: { _apparatus: apparatus, elements: 3, bonus: 3.00, malus: 5.00 }}})
}
pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventDeleteApparatus(
@Context('client') client, Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('apparatus', { type: () => UUID, nullable: false }) apparatus: UUID,
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

let neu: Event;

if (!tmp.apparatus) {
neu = tmp;
} else {
neu = await this.service.update(client, tmp._id, {$pull: { apparatus: { _apparatus: apparatus }}})
}
pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventOrderApparatus(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('src', { type: () => Int, nullable: false }) src: number,
@Args('target', { type: () => Int, nullable: false }) target: number,
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

let neu: Event;

if (!tmp.apparatus) {
neu = tmp;
} else {
const app = tmp.apparatus.splice(src, 1)
if (app?.length === 1) tmp.apparatus.splice(target, 0, app[0])
neu = await this.service.update(client, tmp._id, {$set: { apparatus: tmp.apparatus }})
}
pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventAddTimeslots(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('timeslots', { type: () => [ITimeslot], nullable: false}) timeslots: ITimeslot[],
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

let neu: Event;

if (!tmp.timeslots) {
neu = await this.service.update(client, tmp._id, {$set: {timeslots: timeslots.map(t => ({_id: uuid(), ...t }))}});
} else {
timeslots = timeslots.filter(n => !tmp.timeslots.find(o => o.time === n.time && o.duration === n.duration));
neu = await this.service.update(client, tmp._id, {$push: { timeslots: { $each: timeslots.map(t => ({_id: uuid(), ...t })) }}});
}

pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventDeleteTimeslots(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('timeslots', { type: () => [UUID], nullable: false}) timeslots: UUID[],
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

let neu: Event;

if (!tmp.timeslots) {
neu = tmp;
} else {
neu = await this.service.update(client, tmp._id, {$pull: { timeslots: { _id: { $in: timeslots } }}});
}

pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventAddTeam(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('timeslot', { type: () => UUID, nullable: false }) timeslot: UUID,
@Args('name', { type: () => String, nullable: false }) name: string
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

if (!tmp?.timeslots?.find(t => t._id === timeslot)) {
throw new HttpException('Timeslot-ID not found', 404);
}

let neu: Event;

if (tmp.timeslots.find(t => t._id === timeslot).team) {
neu = await this.service.update(
client,
tmp._id,
{ $set: { 'timeslots.$[ts].team,name': name }},
{ arrayFilters: [{ 'ts._id': timeslot }]}
);
} else {
neu = await this.service.update(
client,
tmp._id,
{ $set: { 'timeslots.$[ts].team': { _id: uuid(), name }}},
{ arrayFilters: [{ 'ts._id': timeslot }]}
);
}

pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => Event, { nullable: false })
async EventDeleteTeam(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('timeslot', { type: () => UUID, nullable: false }) timeslot: UUID,
): Promise<Event> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

if (!tmp?.timeslots?.find(t => t._id === timeslot)) {
throw new HttpException('Timeslot-ID not found', 404);
} else if (!tmp.timeslots.find(t => t._id === timeslot).team) {
throw new HttpException('Timeslot empty', 404);
}

const neu: Event = await this.service.update(
client,
tmp._id,
{ $unset: { 'timeslots.$[ts].team': '' }},
{ arrayFilters: [{ 'ts._id': timeslot }]}
);

pubsub.publish('EventUpdated', { EventUpdated: neu });
return neu;
}

@Mutation(() => UUID, { nullable: false })
async EventDelete(
@Context('client') client, Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
): Promise<UUID> {
const tmp = await this.service.findOneById(id);
if (!tmp) throw new HttpException('Event-ID not found!', 404);

if (!client.isMaster() && !client.isOrganizer(tmp._organizer)) {
throw new HttpException('Access denied', 403);
}

this.service.delete(client, id);

pubsub.publish('EventDeleted', { EventDeleted: id, organizer: tmp._organizer });

return id;
}
}

+ 47
- 0
server/src/event/resolver/event.subscription.ts ファイルの表示

@@ -0,0 +1,47 @@
import {Args, Resolver, Subscription} from '@nestjs/graphql'
import {Event} from '../models/Event'
import {pubsub} from '../../main'
import {UUID} from '../../global/scalars/UUID'
import {EventService} from '../event.service'

@Resolver(() => Event)
export class EventResolverS {
constructor(
private readonly service: EventService
) {}

@Subscription(() => Event, {
nullable: false,
filter: async (payload, variables, context) => {
let ret = true;

if (ret && !!variables.organizer)
ret = payload?.EventUpdated?._organizer === variables?.organizer
&& (context?.client?.isMaster() || context?.client?.isAdmin(variables.organizer));

if (ret && !!variables.id)
ret = payload?.EventUpdated?._id === variables?.id

return ret;
}
})
async EventUpdated(
@Args('id', { type: () => UUID, nullable: true }) id: UUID,
@Args('organizer', { type: () => UUID, nullable: true }) organizer: UUID,
) {
return pubsub.asyncIterator('EventUpdated');
}

@Subscription(() => UUID, {
nullable: false,
filter: async (payload, variables, context) => {
return payload?.organizer === variables?.organizer
&& (context?.client?.isMaster() || context?.client?.isAdmin(variables.organizer))
}
})
async EventDeleted(
@Args('organizer', { type: () => UUID, nullable: false }) organizer: UUID
) {
return pubsub.asyncIterator('EventDeleted');
}
}

+ 10
- 1
server/src/event/resolver/event.ts ファイルの表示

@@ -6,6 +6,7 @@ import {Date} from '../../global/scalars/Date'
import {Timeslot} from '../models/Timeslot'
import {Organizer} from '../../organizer/models/Organizer'
import {OrganizerService} from '../../organizer/organizer.service'
import {EventApparatus} from '../models/EventApparatus'

@Resolver(() => Event)
export class EventResolver {
@@ -33,7 +34,15 @@ export class EventResolver {
return parent.date;
}

@ResolveField(() => [Timeslot], { nullable: true })
@ResolveField(() => [EventApparatus], { nullable: false })
async apparatus(
@Context('client') client: Client,
@Parent() parent: Event
): Promise<EventApparatus[]> {
return parent.apparatus || [];
}

@ResolveField(() => [Timeslot], { nullable: false })
async timeslots(
@Context('client') client: Client,
@Parent() parent: Event

+ 49
- 0
server/src/event/resolver/eventapparatus.ts ファイルの表示

@@ -0,0 +1,49 @@
import {Context, Float, Int, Parent, ResolveField, Resolver} from '@nestjs/graphql';
import { Client } from '../../client';
import { UUID } from '../../global/scalars/UUID';
import {EventApparatus} from '../models/EventApparatus'
import {Apparatus} from '../../apparatus/models/Apparatus'
import {ApparatusService} from '../../apparatus/apparatus.service'

@Resolver(() => EventApparatus)
export class EventApparatusResolver {
@ResolveField(() => UUID, { nullable: false })
async _apparatus(
@Context('client') client: Client,
@Parent() parent: EventApparatus
): Promise<UUID> {
return parent._apparatus as UUID;
}

@ResolveField(() => Apparatus, { nullable: false })
async apparatus(
@Context('client') client: Client,
@Parent() parent: EventApparatus
): Promise<Apparatus> {
return new ApparatusService().findOneById(parent._apparatus);
}

@ResolveField(() => Int, { nullable: false })
async elements(
@Context('client') client: Client,
@Parent() parent: EventApparatus
): Promise<number> {
return parent.elements;
}

@ResolveField(() => Float, { nullable: false })
async bonus(
@Context('client') client: Client,
@Parent() parent: EventApparatus
): Promise<number> {
return parent.bonus;
}

@ResolveField(() => Float, { nullable: false })
async malus(
@Context('client') client: Client,
@Parent() parent: EventApparatus
): Promise<number> {
return parent.malus;
}
}

+ 2
- 1
server/src/organizer/organizer.module.ts ファイルの表示

@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
import { OrganizerResolverQ } from './resolver/organizer.query';
import { OrganizerResolverM } from './resolver/organizer.mutation';
import { OrganizerResolverS } from './resolver/organizer.subscription'
import { OrganizerService } from './organizer.service';
import { OrganizerResolver } from './resolver/organizer';

@Module({
providers: [
OrganizerResolverQ, OrganizerResolverM,
OrganizerResolverQ, OrganizerResolverM, OrganizerResolverS,
OrganizerService,
OrganizerResolver,
],

+ 61
- 0
server/src/organizer/resolver/organizer.mutation.ts ファイルの表示

@@ -3,6 +3,8 @@ import { Organizer } from '../models/Organizer';
import { Client } from '../../client';
import { OrganizerService } from '../organizer.service';
import { HttpException } from '@nestjs/common';
import {UUID} from '../../global/scalars/UUID'
import {pubsub} from '../../main'

@Resolver(() => Organizer)
export class OrganizerResolverM {
@@ -28,4 +30,63 @@ export class OrganizerResolverM {
return this.service.create(client, name, plz, ort);
return tmp[0];
}

@Mutation(() => Organizer, { nullable: false })
async OrganizerUpdate(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('name', { nullable: true }) name?: string,
@Args('plz', { type: () => Int, nullable: true }) plz?: number,
@Args('ort', { nullable: true }) ort?: string
): Promise<Organizer> {
if (!client.isMaster() && !client.isAdmin(id)) throw new HttpException('Access denied', 403)

const tmp = await this.service.findOneById(id);

const set: {
name?: string,
plz?: number,
ort?: string,
} = {};

if (name !== undefined && name !== tmp.name) set.name = name
if (plz !== undefined && plz !== tmp.plz) set.plz = plz
if (ort !== undefined && ort !== tmp.ort) set.ort = ort

if (Object.values(set).length === 0) return tmp;

const neu = await this.service.update(client, tmp._id, {$set: set});
pubsub.publish('OrganizerUpdated', { OrganizerUpdated: neu });
return neu;
}

@Mutation(() => Organizer, { nullable: false })
async OrganizerCreate(
@Context('client') client: Client,
@Args('name') name: string,
@Args('plz', { type: () => Int, nullable: true }) plz?: number,
@Args('ort', { nullable: true }) ort?: string,
): Promise<Organizer> {
if (!client.isMaster()) throw new HttpException('Access denied', 403);

const neu = await this.service.insert(client, { name, plz, ort });

pubsub.publish('OrganizerUpdated', { OrganizerUpdated: neu });

return neu;
}

@Mutation(() => UUID, { nullable: false })
async OrganizerDelete(
@Context('client') client: Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID
): Promise<UUID> {
if (!client.isMaster() && !client.isAdmin(id)) throw new HttpException('Access denied', 403);

this.service.delete(client, id);

pubsub.publish('OrganizerDeleted', { OrganizerDeleted: id });

return id;
}
}

+ 22
- 0
server/src/organizer/resolver/organizer.subscription.ts ファイルの表示

@@ -0,0 +1,22 @@
import {Resolver, Subscription} from '@nestjs/graphql'
import {Organizer} from '../models/Organizer'
import {pubsub} from '../../main'
import {UUID} from '../../global/scalars/UUID'
import {OrganizerService} from '../organizer.service'

@Resolver(() => Organizer)
export class OrganizerResolverS {
constructor(
private readonly service: OrganizerService
) {}

@Subscription(() => Organizer, { nullable: false })
async OrganizerUpdated() {
return pubsub.asyncIterator('OrganizerUpdated');
}

@Subscription(() => UUID, { nullable: false })
async OrganizerDeleted() {
return pubsub.asyncIterator('OrganizerDeleted');
}
}

+ 1
- 1
server/src/person/resolver/person.subscription.ts ファイルの表示

@@ -18,6 +18,6 @@ export class PersonResolverS {

@Subscription(() => UUID, { nullable: false })
async PersonDeleted() {
return pubsub.asyncIterator('PersonDeleted')
return pubsub.asyncIterator('PersonDeleted');
}
}

読み込み中…
キャンセル
保存