Переглянути джерело

1.0.0:

FavIcon
Schulverwaltung
Groß-/Kleinschreibung Mail bei Login
Geräte bei Event bearbeiten
tags/v1.0.0
akimmig 3 роки тому
джерело
коміт
efeb47feda
15 змінених файлів з 561 додано та 44 видалено
  1. +2
    -2
      client/package.json
  2. BIN
      client/public/favicon.ico
  3. +6
    -0
      client/src/plugins/router.js
  4. +221
    -0
      client/src/views/components/admin/school.vue
  5. +2
    -2
      client/src/views/components/core/AppBar.vue
  6. +33
    -30
      client/src/views/components/core/Footer.vue
  7. +91
    -0
      client/src/views/components/event/dialogs/EditEventapparatus.vue
  8. +44
    -1
      client/src/views/components/event/event.vue
  9. +2
    -1
      client/src/views/components/start.vue
  10. +2
    -2
      server/package.json
  11. +5
    -1
      server/schema.gql
  12. +1
    -1
      server/src/client.ts
  13. +33
    -1
      server/src/event/resolver/event.mutation.ts
  14. +103
    -0
      server/src/organizer/resolver/organizer.mutation.ts
  15. +16
    -3
      server/src/organizer/resolver/organizer.subscription.ts

+ 2
- 2
client/package.json Переглянути файл

@@ -1,6 +1,6 @@
{
"name": "client",
"version": "0.1.0",
"name": "schoolINmotion-client",
"version": "1.0.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",

BIN
client/public/favicon.ico Переглянути файл

Before After

+ 6
- 0
client/src/plugins/router.js Переглянути файл

@@ -62,6 +62,12 @@ const routes = [
name: 'Event',
component: () => import('../views/components/event/event'),
props: true
},
{
path: 'admin/:id',
name: 'Schulverwaltung',
component: () => import('../views/components/admin/school'),
props: true
}
]
}

+ 221
- 0
client/src/views/components/admin/school.vue Переглянути файл

@@ -0,0 +1,221 @@
<template>
<e403 v-if="!isAdmin(id)" />
<v-container
v-else
fluid
tag="section"
>
<h2>Administratoren {{ Organizer.name }}</h2>
<v-data-table
:items="Organizer.admins"
:headers="[
{ text: 'Nachname', value: 'familyName' },
{ text: 'Vorname', value: 'givenName' },
{ text: 'zum Organisator zurückstufen', value: 'makeOrga' },
{ text: 'löschen', value: 'delete' }
]"
:items-per-page="-1"
:mobile-breakpoint="0"
>
<template #item.makeOrga="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="makeOrga(item._id)"
>
<v-icon>far fa-angle-down</v-icon>
</v-btn>
</template>
<template #item.delete="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="del(item._id)"
>
<v-icon>far fa-trash-alt</v-icon>
</v-btn>
</template>
</v-data-table>

<h2>Organisatoren {{ Organizer.name }}</h2>
<v-data-table
:items="Organizer.organizers"
:headers="[
{ text: 'Nachname', value: 'familyName' },
{ text: 'Vorname', value: 'givenName' },
{ text: 'zum Admin hochstufen', value: 'makeAdmin' },
{ text: 'löschen', value: 'delete' }
]"
:items-per-page="-1"
:mobile-breakpoint="0"
>
<template #item.makeAdmin="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="makeAdmin(item._id)"
>
<v-icon>far fa-angle-up</v-icon>
</v-btn>
</template>
<template #item.delete="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="del(item._id)"
>
<v-icon>far fa-trash-alt</v-icon>
</v-btn>
</template>
</v-data-table>

<h2>Ausstehende Registrierungen {{ Organizer.name }}</h2>
<v-data-table
:items="Organizer.pending"
:headers="[
{ text: 'Nachname', value: 'familyName' },
{ text: 'Vorname', value: 'givenName' },
{ text: 'als Organisator hinzufügen', value: 'makeOrga' },
{ text: 'als Admin hinzufügen', value: 'makeAdmin' },
{ text: 'löschen', value: 'delete' }
]"
:items-per-page="-1"
:mobile-breakpoint="0"
>
<template #item.makeOrga="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="makeOrga(item._id)"
>
<v-icon>far fa-angle-up</v-icon>
</v-btn>
</template>
<template #item.makeAdmin="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="makeAdmin(item._id)"
>
<v-icon>far fa-angle-double-up</v-icon>
</v-btn>
</template>
<template #item.delete="{item}">
<v-btn
v-if="item._id !== $store.state.profile._id"
fab
small
@click="del(item._id)"
>
<v-icon>far fa-trash-alt</v-icon>
</v-btn>
</template>
</v-data-table>
</v-container>
</template>

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

const query = `
_id name
admins { _id givenName familyName }
organizers { _id givenName familyName }
pending { _id givenName familyName }
`

export default {
name: 'school',

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

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

data: () => ({
Organizer: {},
EventFind: [],
filter: ''
}),

methods: {
makeOrga (p) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $person: UUID!) {
OrganizerUpdateMakeOrganizer(id: $id, person: $person) { _id }
}`,
variables: {
id: this.id,
person: p
}
})
},
makeAdmin (p) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $person: UUID!) {
OrganizerUpdateMakeAdmin(id: $id, person: $person) { _id }
}`,
variables: {
id: this.id,
person: p
}
})
},
async del (p) {
if (await this.$root.$children[0].$refs.confirm.open('Löschen', 'Diese Person wirklich aus Ihrer Schule löschen?')) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $person: UUID!) {
OrganizerUpdateDeletePerson(id: $id, person: $person) { _id }
}`,
variables: {
id: this.id,
person: p
}
})
}
}
},

apollo: {
Organizer: {
query: gql`query($organizer: UUID!) {
Organizer(id: $organizer) { ${query} }
}`,
variables () {
return {
organizer: this.id
}
}
},
$subscribe: {
OrganizerUpdated: {
query: gql`subscription($organizer: UUID!) { OrganizerUpdated(organizer: $organizer) { ${query} } }`,
variables () {
return {
organizer: this.id
}
}
}
}
}
}
</script>

<style scoped>

</style>

+ 2
- 2
client/src/views/components/core/AppBar.vue Переглянути файл

@@ -75,7 +75,7 @@
flat
nav
>
<v-hover v-slot="{ hover }">
<!--v-hover v-slot="{ hover }">
<v-list-item
to="/profile"
:class="{red: hover}"
@@ -83,7 +83,7 @@
Profil
</v-list-item>
</v-hover>
<v-divider class="mb-2 mt-2" />
<v-divider class="mb-2 mt-2" /-->
<v-hover v-slot="{ hover }">
<v-list-item
:class="{red: hover}"

+ 33
- 30
client/src/views/components/core/Footer.vue Переглянути файл

@@ -1,39 +1,38 @@
<template>
<v-footer
<v-container
id="dashboard-core-footer"
class="v-footer"
>
<v-container>
<v-row
align="center"
no-gutters
<v-row
align="center"
no-gutters
>
<v-col
v-for="(link, i) in links"
:key="i"
class="text-center mb-sm-0 mb-5"
cols="auto"
>
<v-col
v-for="(link, i) in links"
:key="i"
class="text-center mb-sm-0 mb-5"
cols="auto"
>
<a
:href="'/#'+link.to"
class="mr-0 grey--text text--darken-3"
rel="noopener"
v-text="link.text"
/>
</v-col>
<a
:href="'/#'+link.to"
class="mr-0 grey--text text--darken-3"
rel="noopener"
v-text="link.text"
/>
</v-col>

<v-spacer class="hidden-sm-and-down" />
<v-spacer class="hidden-sm-and-down" />

<v-col
cols="12"
md="auto"
>
<div class="body-1 font-weight-light pt-6 pt-md-0 text-center">
&copy; 2021 IT Kimmig
</div>
</v-col>
</v-row>
</v-container>
</v-footer>
<v-col
cols="12"
md="auto"
>
<div class="body-1 font-weight-light pt-6 pt-md-0 text-center">
&copy; 2021 IT Kimmig
</div>
</v-col>
</v-row>
</v-container>
</template>

<script>
@@ -66,4 +65,8 @@ export default {
font-weight: 500
text-decoration: none
text-transform: uppercase

.v-footer
a
padding-left: 0px
</style>

+ 91
- 0
client/src/views/components/event/dialogs/EditEventapparatus.vue Переглянути файл

@@ -0,0 +1,91 @@
<template>
<base-material-dialog
:value="value"
icon="far fa-pencil"
title="Gerät bearbeiten"
color="rgb(255, 4, 29)"
:actions="['save', 'cancel']"
@save="save"
@close="close"
@esc="close"
>
<v-row>
<v-col cols="12">
<v-text-field
v-model="e"
label="Elemente"
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="b"
label="Bonus"
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="m"
label="Malus"
/>
</v-col>
</v-row>
</base-material-dialog>
</template>

<script>
export default {
name: 'EditEventapparatus',

props: {
value: {
type: Boolean,
default: false
},
id: {
type: String
},
elements: {
type: Number,
default: 3
},
bonus: {
type: Number,
default: 3.00
},
malus: {
type: Number,
default: 5.00
}
},

data: () => ({
e: '',
b: '',
m: ''
}),

methods: {
save () {
this.$emit('save', { id: this.id, elements: parseInt(this.e), bonus: parseFloat(this.b.replaceAll(',', '.')), malus: parseFloat(this.m.replaceAll(',', '.')) })
this.close()
},
close () {
this.$emit('input', false)
}
},

watch: {
value () {
if (this.value) {
this.e = `${this.elements}`
this.b = `${this.bonus}`
this.m = `${this.malus}`
}
}
}
}
</script>

<style scoped>

</style>

+ 44
- 1
client/src/views/components/event/event.vue Переглянути файл

@@ -101,7 +101,10 @@
v-if="!hasresults"
>
<v-spacer />
<v-btn icon>
<v-btn
icon
@click="openapparatusedit(a)"
>
<v-icon>far fa-pencil</v-icon>
</v-btn>
<v-btn
@@ -565,6 +568,14 @@
</v-card>
</v-tab-item>
</v-tabs-items>
<edit-eventapparatus
v-model="apparatusdialog.open"
:id="apparatusdialog.id"
:elements="apparatusdialog.elements"
:bonus="apparatusdialog.bonus"
:malus="apparatusdialog.malus"
@save="({id, elements, bonus, malus}) => saveApparatus(id, elements, bonus, malus)"
/>
</v-container>
</template>

@@ -579,6 +590,10 @@ const query = `_id name date _organizer organizer { name } apparatus { _apparatu
export default {
name: 'Event',

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

props: {
id: {
type: String,
@@ -590,6 +605,13 @@ export default {
Event: null,
ApparatusFind: [],
tab: 0,
apparatusdialog: {
open: false,
id: null,
elements: null,
bonus: null,
malus: null
},
ts: {
headers: [
{
@@ -783,6 +805,27 @@ export default {
this.Event.apparatus = r.data.EventOrderApparatus.apparatus
})
},
openapparatusedit (a) {
this.apparatusdialog.elements = a.elements
this.apparatusdialog.bonus = a.bonus
this.apparatusdialog.malus = a.malus
this.apparatusdialog.id = a._apparatus
this.apparatusdialog.open = true
},
saveApparatus (id, e, b, m) {
this.$apollo.mutate({
mutation: gql`mutation($id: UUID!, $apparatus: UUID!, $elements: Int!, $bonus: Float!, $malus: Float!) {
EventUpdateApparatus(id: $id, apparatus: $apparatus, elements: $elements, bonus: $bonus, malus: $malus) { ${query} }
}`,
variables: {
id: this.id,
apparatus: id,
elements: e,
bonus: b,
malus: m
}
})
},
addTimeslots () {
if (this.newtimeslots.length > 0) {
this.$apollo.mutate({

+ 2
- 1
client/src/views/components/start.vue Переглянути файл

@@ -21,10 +21,11 @@ export default {

<style scoped>
img {
max-width: calc(100% - 48px);
width: 400px;
height: auto;
float: right;
margin: 0px 24px;
margin: 0px 24px 24px 24px;
}

h2 {

+ 2
- 2
server/package.json Переглянути файл

@@ -1,6 +1,6 @@
{
"name": "turnenaufzeit",
"version": "0.0.1",
"name": "schoolINmotion-server",
"version": "1.0.0",
"description": "",
"author": "",
"private": true,

+ 5
- 1
server/schema.gql Переглянути файл

@@ -121,10 +121,14 @@ type Mutation {
OrganizerUpdate(ort: String, plz: Int, name: String, id: UUID!): Organizer!
OrganizerCreate(ort: String, plz: Int, name: String!): Organizer!
OrganizerDelete(id: UUID!): UUID!
OrganizerUpdateMakeOrganizer(person: UUID!, id: UUID!): Organizer!
OrganizerUpdateMakeAdmin(person: UUID!, id: UUID!): Organizer!
OrganizerUpdateDeletePerson(person: UUID!, id: UUID!): Organizer!
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!
EventUpdateApparatus(malus: Float, bonus: Float, elements: Int, 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!
@@ -158,7 +162,7 @@ type Subscription {
PersonDeleted: UUID!
ApparatusUpdated: Apparatus
ApparatusDeleted: UUID!
OrganizerUpdated: Organizer!
OrganizerUpdated(organizer: UUID): Organizer!
OrganizerDeleted: UUID!
EventUpdated(organizer: UUID, id: UUID): Event!
EventDeleted(organizer: UUID!): UUID!

+ 1
- 1
server/src/client.ts Переглянути файл

@@ -48,7 +48,7 @@ export class Client {

if (data.email && data.passwort) {
const entries: any[] = (await Promise.all(
(await db.fetch('person', { 'email': data.email } ))
(await db.fetch('person', { 'email': { $regex: data.email, $options: 'i' } } ))
.map(async (e) => ({
...e,
authOK: await checkPassword(data.passwort, e.passwort)

+ 33
- 1
server/src/event/resolver/event.mutation.ts Переглянути файл

@@ -1,4 +1,4 @@
import {Args, Context, Int, Mutation, Resolver} from '@nestjs/graphql';
import {Args, Context, Float, Int, Mutation, Resolver} from '@nestjs/graphql';
import { Event } from '../models/Event';
import { Client } from '../../client';
import { EventService } from '../event.service';
@@ -121,6 +121,38 @@ export class EventResolverM {
}

@Mutation(() => Event, { nullable: false })
async EventUpdateApparatus(
@Context('client') client, Client,
@Args('id', { type: () => UUID, nullable: false }) id: UUID,
@Args('apparatus', { type: () => UUID, nullable: false }) apparatus: UUID,
@Args('elements', { type: () => Int, nullable: true }) elements?: number,
@Args('bonus', { type: () => Float, nullable: true }) bonus?: number,
@Args('malus', { type: () => Float, nullable: true }) malus?: 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);
}

const set: any = {
'apparatus.$[apparatus]': {
_apparatus: apparatus
}
}

if (elements !== undefined && elements !== null) set['apparatus.$[apparatus]'].elements = elements
if (bonus !== undefined && bonus !== null) set['apparatus.$[apparatus]'].bonus = bonus
if (malus !== undefined && malus !== null) set['apparatus.$[apparatus]'].malus = malus

const neu: Event = await this.service.update(client, tmp._id, { $set: set }, { arrayFilters: [ { '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,

+ 103
- 0
server/src/organizer/resolver/organizer.mutation.ts Переглянути файл

@@ -89,4 +89,107 @@ export class OrganizerResolverM {

return id;
}

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

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

if (tmp._admins.find(a => a === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_admins': person },
$push: { '_organizers': person }
}
)
} else if (tmp._pending.find(p => p === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_pending': person },
$push: { '_organizers': person }
}
)
} else {
throw new HttpException('Person not found', 404)
}

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

return tmp
}

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

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

if (tmp._organizers.find(o => o === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_organizers': person },
$push: { '_admins': person }
}
)
} else if (tmp._pending.find(p => p === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_pending': person },
$push: { '_admins': person }
}
)
} else {
throw new HttpException('Person not found', 404)
}

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

return tmp
}

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

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

if (tmp._admins.find(a => a === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_admins': person },
}
)
} else if (tmp._organizers.find(o => o === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_organizers': person },
}
)
} else if (tmp._pending.find(p => p === person)) {
tmp = await this.service.update(client, id,
{
$pull: { '_pending': person },
}
)
} else {
throw new HttpException('Person not found', 404)
}

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

return tmp
}
}

+ 16
- 3
server/src/organizer/resolver/organizer.subscription.ts Переглянути файл

@@ -1,4 +1,4 @@
import {Resolver, Subscription} from '@nestjs/graphql'
import {Args, Resolver, Subscription} from '@nestjs/graphql'
import {Organizer} from '../models/Organizer'
import {pubsub} from '../../main'
import {UUID} from '../../global/scalars/UUID'
@@ -10,8 +10,21 @@ export class OrganizerResolverS {
private readonly service: OrganizerService
) {}

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

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

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


Завантаження…
Відмінити
Зберегти