FavIcon Schulverwaltung Groß-/Kleinschreibung Mail bei Login Geräte bei Event bearbeitentags/v1.0.0
@@ -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", |
@@ -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 | |||
} | |||
] | |||
} |
@@ -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> |
@@ -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}" |
@@ -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"> | |||
© 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"> | |||
© 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> |
@@ -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> |
@@ -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({ |
@@ -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 { |
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "turnenaufzeit", | |||
"version": "0.0.1", | |||
"name": "schoolINmotion-server", | |||
"version": "1.0.0", | |||
"description": "", | |||
"author": "", | |||
"private": true, |
@@ -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! |
@@ -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) |
@@ -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, |
@@ -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 | |||
} | |||
} |
@@ -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'); | |||
} | |||