save changed marks

This commit is contained in:
walamana 2023-09-18 11:51:41 +02:00
parent 00cca5ca9a
commit 24f14da9b2
16 changed files with 300 additions and 119 deletions

View File

@ -7,5 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
"license": "ISC" "license": "ISC",
"dependencies": {
"rxjs": "^7.8.1"
}
} }

View File

@ -2,5 +2,5 @@ ktor_version=2.3.3
kotlin_version=1.9.0 kotlin_version=1.9.0
logback_version=1.2.11 logback_version=1.2.11
kotlin.code.style=official kotlin.code.style=official
exposed_version=0.41.1 exposed_version=0.43.0
h2_version=2.1.214 h2_version=2.1.214

View File

@ -37,7 +37,7 @@ object MarksDao {
} }
suspend fun setMark(ministrantId: Int, gottesdienstId: Int, value: Int): Boolean = dbQuery { suspend fun setMark(ministrantId: Int, gottesdienstId: Int, value: Int): Boolean = dbQuery {
Marks.insert { Marks.upsert {
it[Marks.mid] = ministrantId it[Marks.mid] = ministrantId
it[Marks.gid] = gottesdienstId it[Marks.gid] = gottesdienstId
it[Marks.value] = value it[Marks.value] = value

View File

@ -27,6 +27,7 @@ data class Ministrant(
@Serializable @Serializable
data class SimplifiedMinistrant( data class SimplifiedMinistrant(
val id: Int, val id: Int,
val username: String,
val firstname: String, val firstname: String,
val lastname: String val lastname: String
) )
@ -44,10 +45,10 @@ object Ministranten : Table() {
} }
object MinistrantenDao { object MinistrantenDao {
private fun resultRowToMinistrant(row: ResultRow, showPasswordHash: Boolean = false) = Ministrant ( private fun resultRowToMinistrant(row: ResultRow, showPasswordHash: Boolean = false) = Ministrant(
row[Ministranten.id], row[Ministranten.id],
row[Ministranten.username], row[Ministranten.username],
if(showPasswordHash) row[Ministranten.passwordHash] else "", if (showPasswordHash) row[Ministranten.passwordHash] else "",
row[Ministranten.firstname], row[Ministranten.firstname],
row[Ministranten.lastname], row[Ministranten.lastname],
Date(row[Ministranten.birthday]), Date(row[Ministranten.birthday]),
@ -59,8 +60,18 @@ object MinistrantenDao {
} }
suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery { suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery {
Ministranten.selectAll().map { row -> Ministranten.slice(
SimplifiedMinistrant(row[Ministranten.id], row[Ministranten.firstname], row[Ministranten.lastname]) Ministranten.id,
Ministranten.username,
Ministranten.firstname,
Ministranten.lastname
).selectAll().map { row ->
SimplifiedMinistrant(
row[Ministranten.id],
row[Ministranten.username],
row[Ministranten.firstname],
row[Ministranten.lastname]
)
} }
} }
@ -70,7 +81,14 @@ object MinistrantenDao {
}.firstOrNull() }.firstOrNull()
} }
suspend fun createMinistrant(username: String, passwordHash: String, firstname: String, lastname: String, birthday: Date, privileges: List<String>) = dbQuery { suspend fun createMinistrant(
username: String,
passwordHash: String,
firstname: String,
lastname: String,
birthday: Date,
privileges: List<String>
) = dbQuery {
val statement = Ministranten.insert { val statement = Ministranten.insert {
it[Ministranten.username] = username it[Ministranten.username] = username
it[Ministranten.passwordHash] = "" it[Ministranten.passwordHash] = ""
@ -95,14 +113,14 @@ object MinistrantenDao {
lastname: String? = null, lastname: String? = null,
birthday: Date? = null, birthday: Date? = null,
privileges: List<String>? = null privileges: List<String>? = null
) = dbQuery{ ) = dbQuery {
Ministranten.update({Ministranten.id eq id}) { Ministranten.update({ Ministranten.id eq id }) {
if(username != null) it[Ministranten.username] = username if (username != null) it[Ministranten.username] = username
if(passwordHash != null) it[Ministranten.passwordHash] = passwordHash if (passwordHash != null) it[Ministranten.passwordHash] = passwordHash
if(firstname != null) it[Ministranten.firstname] = firstname if (firstname != null) it[Ministranten.firstname] = firstname
if(lastname != null) it[Ministranten.lastname] = lastname if (lastname != null) it[Ministranten.lastname] = lastname
if(birthday != null) it[Ministranten.birthday] = birthday.time if (birthday != null) it[Ministranten.birthday] = birthday.time
if(privileges != null) it[Ministranten.privileges] = privileges.joinToString(",") if (privileges != null) it[Ministranten.privileges] = privileges.joinToString(",")
} }
} }
} }

View File

@ -17,6 +17,7 @@ import java.util.*
const val SALT_ROUNDS = 10; const val SALT_ROUNDS = 10;
data class JWTEnvironment( data class JWTEnvironment(
val secret: String, val secret: String,
val issuer: String, val issuer: String,
@ -45,7 +46,7 @@ fun Application.configureSecurity() {
} }
fun Application.getJWTEnvironment(): JWTEnvironment{ fun Application.getJWTEnvironment(): JWTEnvironment {
val secret = environment.config.property("jwt.secret").getString() val secret = environment.config.property("jwt.secret").getString()
val issuer = environment.config.property("jwt.issuer").getString() val issuer = environment.config.property("jwt.issuer").getString()
@ -58,12 +59,12 @@ fun Payload.mid() = getClaim("id").asInt()
object Security { object Security {
fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000*60*60); fun DEFAULT_EXPIRY() = Date(System.currentTimeMillis() + 1000 * 60 * 60);
suspend fun authenticateUser(application: Application, username: String, password: String): Ministrant? { suspend fun authenticateUser(application: Application, username: String, password: String): Ministrant? {
if(username == "admin") { if (username == "admin") {
val adminPw = application.environment.config.property("admin.password").getString() val adminPw = application.environment.config.property("admin.password").getString()
if(adminPw == password) { if (adminPw == password) {
val allMinis = MinistrantenDao.allMinistranten().map { it.username } val allMinis = MinistrantenDao.allMinistranten().map { it.username }
return Ministrant( return Ministrant(
0, "admin", "", "admin", "admin", Date(), allMinis 0, "admin", "", "admin", "admin", Date(), allMinis
@ -75,7 +76,7 @@ object Security {
val ministrant = MinistrantenDao.getMinistrant(username, true) val ministrant = MinistrantenDao.getMinistrant(username, true)
?: return null ?: return null
if(!BCrypt.verifyer().verify(password.toCharArray(), ministrant.passwordHash).verified) { if (!BCrypt.verifyer().verify(password.toCharArray(), ministrant.passwordHash).verified) {
return null return null
} }
@ -95,10 +96,11 @@ object Security {
} }
fun createToken(jwtEnv: JWTEnvironment, ministrant: Ministrant) = JWT.create() fun createToken(jwtEnv: JWTEnvironment, ministrant: Ministrant) = JWT.create()
.withAudience(jwtEnv.audience) .withAudience(jwtEnv.audience)
.withIssuer(jwtEnv.issuer) .withIssuer(jwtEnv.issuer)
.withClaim("username", ministrant.username) .withClaim("username", ministrant.username)
.withClaim("id", ministrant.id) .withClaim("id", ministrant.id)
.withExpiresAt(DEFAULT_EXPIRY()) .withClaim("privileges", ministrant.privileges)
.sign(Algorithm.HMAC256(jwtEnv.secret)) .withExpiresAt(DEFAULT_EXPIRY())
.sign(Algorithm.HMAC256(jwtEnv.secret))
} }

View File

@ -28,6 +28,7 @@ data class AuthenticationRequest(
@Serializable @Serializable
data class AuthenticationResult( data class AuthenticationResult(
val success: Boolean, val success: Boolean,
val token: String? = null,
val privileges: List<String>? = null, val privileges: List<String>? = null,
) )
@ -61,7 +62,7 @@ fun Route.configureAuthenticationRoutes() {
"token=$token; HttpOnly; Expires=$expiry" "token=$token; HttpOnly; Expires=$expiry"
) )
call.respond(AuthenticationResult(true, ministrant.privileges)) call.respond(AuthenticationResult(true, token, ministrant.privileges))
} }
authenticate { authenticate {

View File

@ -8,6 +8,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.server.util.* import io.ktor.server.util.*
fun Route.configureMarksView() { fun Route.configureMarksView() {
route("/marks") { route("/marks") {
get { get {
@ -15,22 +16,26 @@ fun Route.configureMarksView() {
call.respond(data) call.respond(data)
} }
put { put {
val data = call.receive<Mark>() val changedMarks = call.receive<List<Mark>>()
val mark = MarksDao.setMark( for(changedMark in changedMarks) {
data.mid, val mark = MarksDao.setMark(
data.gid, changedMark.mid,
data.value changedMark.gid,
) changedMark.value
)
}
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} }
patch { patch {
// TODO: Access only by admin // TODO: Access only by admin
val data = call.receive<Mark>() val changedMarks = call.receive<List<Mark>>()
val changed = MarksDao.setMark( for(changedMark in changedMarks) {
data.mid, val mark = MarksDao.setMark(
data.gid, changedMark.mid,
data.value changedMark.gid,
) changedMark.value
)
}
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} }
delete { delete {

View File

@ -2,9 +2,23 @@
import {RouterLink, RouterView} from 'vue-router' import {RouterLink, RouterView} from 'vue-router'
import HelloWorld from './components/HelloWorld.vue' import HelloWorld from './components/HelloWorld.vue'
import LoginPanel from "@/components/LoginPanel.vue"; import LoginPanel from "@/components/LoginPanel.vue";
import {ref} from "vue"; import {onMounted, ref} from "vue";
import {Auth} from "@/services/auth";
const showPopup = ref(false) let showPopup = ref(false)
let loggedIn = ref(false)
Auth.loggedInSubject.subscribe((isLoggedIn) => {
loggedIn.value = isLoggedIn
})
function logout(){
Auth.logout()
}
onMounted(() => {
Auth.checkForToken()
})
</script> </script>
@ -14,13 +28,14 @@ const showPopup = ref(false)
Miniplan Miniplan
</div> </div>
<div class="right"> <div class="right">
<button class="flat" @click="showPopup = true"><i>login</i> Einloggen</button> <button v-if="!loggedIn" class="flat" @click="showPopup = true"><i>login</i> Einloggen</button>
<button v-if="loggedIn" class="flat" @click="logout"><i>logout</i> Abmelden</button>
</div> </div>
</nav> </nav>
<RouterView/> <RouterView/>
<div class="popup-container" :class="{show: showPopup}" @click.self="showPopup = false"> <div class="popup-container" :class="{show: showPopup}" @click.self="showPopup = false">
<LoginPanel :active="showPopup"/> <LoginPanel :active="showPopup" @success="showPopup = false"/>
</div> </div>
</template> </template>

View File

@ -2,7 +2,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Input from "@/components/Input.vue"; import Input from "@/components/Input.vue";
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {API} from "@/views/api"; import {API} from "@/services/api";
import {Auth} from "@/services/auth";
const props = defineProps<{ const props = defineProps<{
active: boolean active: boolean
@ -22,7 +23,7 @@ const username = ref("")
const password = ref("") const password = ref("")
async function attemptLogin() { async function attemptLogin() {
let login = await API.login(username.value, password.value) let login = await Auth.login(username.value, password.value)
if(login.success){ if(login.success){
console.log("success", login) console.log("success", login)
emit("success", login) emit("success", login)
@ -36,7 +37,7 @@ async function attemptLogin() {
<span class="title">Anmelden</span> <span class="title">Anmelden</span>
<Input class="input" v-model:value="username" label="Nutzername"/> <Input class="input" v-model:value="username" label="Nutzername"/>
<Input class="input" v-model:value="password" label="Passwort" type="password"/> <Input class="input" v-model:value="password" label="Passwort" type="password"/>
<button><i>login</i>Anmelden</button> <button @click="attemptLogin"><i>login</i>Anmelden</button>
</div> </div>
</template> </template>

View File

@ -1,11 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import {API} from "@/views/api"; import {API} from "@/services/api";
import {onMounted, reactive, ref} from "vue"; import {onMounted, reactive, ref} from "vue";
import type {PlanModel, SimplifiedMinistrant} from "@/models/models"; import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
const props = defineProps<PlanModel>() const props = defineProps<{
gottesdienste: Gottesdienst[],
ministranten: SimplifiedMinistrant[],
marks: Mark[],
editable: string[]
}>()
defineEmits(["toggleMark"]) defineEmits(["toggleMark"])
function getIconForMark(gid, mid) { function getIconForMark(gid, mid) {
@ -89,12 +94,12 @@ function getMark(gid, mid) {
function getMinistrantClasses(mini: SimplifiedMinistrant) { function getMinistrantClasses(mini: SimplifiedMinistrant) {
return { return {
edit: props.editable.includes(mini.id) edit: props.editable.includes(mini.username)
} }
} }
function getMinis() { function getMinis() {
return props.ministranten.filter(m => props.editable.includes(m.id)) return props.ministranten.filter(m => props.editable.includes(m.username))
} }
function getMiniName(mini) { function getMiniName(mini) {
return mini.firstname + " " + mini.lastname return mini.firstname + " " + mini.lastname

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {API} from "@/views/api"; import {API} from "@/services/api";
import {onMounted, reactive, ref} from "vue"; import {onMounted, reactive, ref} from "vue";
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models"; import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {API} from "@/views/api"; import {API} from "@/services/api";
import {onMounted, reactive, ref} from "vue"; import {onMounted, reactive, ref} from "vue";
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models"; import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
@ -10,10 +10,11 @@ const props = defineProps<{
gottesdienste: Gottesdienst[], gottesdienste: Gottesdienst[],
ministranten: SimplifiedMinistrant[] ministranten: SimplifiedMinistrant[]
marks: Mark[], marks: Mark[],
editable: number[] editable: string[]
edit: boolean edit: boolean,
smallMode: boolean
}>() }>()
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit"]) const emit = defineEmits(["toggleMark", "added", "delete", "endEdit", "resetPassword"])
const data = reactive({ const data = reactive({
godi: {} godi: {}
@ -47,15 +48,12 @@ function getIconForMark(gid, mid) {
function getClassForMark(gid, mid) { function getClassForMark(gid, mid) {
const mark = getMark(gid, mid).value const mark = getMark(gid, mid).value
switch (mark) { return {
case -1: minus: mark == -1,
return "minus"; neutral: mark == 0,
case 0: cross: mark == 1,
return "neutral"; showIcon: !props.smallMode
case 1:
return "cross"
} }
return ""
} }
function getHintForMark(gid, mid) { function getHintForMark(gid, mid) {
@ -111,7 +109,7 @@ function getMark(gid, mid) {
function getMinistrantClasses(mini: SimplifiedMinistrant) { function getMinistrantClasses(mini: SimplifiedMinistrant) {
return { return {
edit: props.editable.includes(mini.id) edit: props.editable.includes(mini.username)
} }
} }
</script> </script>
@ -158,7 +156,7 @@ function getMinistrantClasses(mini: SimplifiedMinistrant) {
<tbody> <tbody>
<tr v-for="mini in props.ministranten" class="ministrant" :class="getMinistrantClasses(mini)"> <tr v-for="mini in props.ministranten" class="ministrant" :class="getMinistrantClasses(mini)">
<td class="name">{{ mini.id }} {{ mini.firstname }} {{ mini.lastname }}</td> <td class="name"><i v-if="edit" style="margin-right: 10px" @click="$emit('resetPassword', mini.username)">lock_reset</i>{{ mini.id }} {{ mini.firstname }} {{ mini.lastname }}</td>
<td <td
v-for="godi in props.gottesdienste" v-for="godi in props.gottesdienste"
class="mark" class="mark"
@ -199,7 +197,7 @@ td {
} }
td:first-child, th:first-child { td:first-child, th:first-child {
padding: 6px 60px 6px 12px; padding: 6px 30px 6px 12px;
text-align: left; text-align: left;
} }
@ -216,6 +214,7 @@ td:nth-child(2n), th:nth-child(2n){
height: 20px; height: 20px;
user-select: none; user-select: none;
i { i {
border-radius: 100%; border-radius: 100%;
padding: 1px; padding: 1px;
@ -253,6 +252,14 @@ td:nth-child(2n), th:nth-child(2n){
font-size: 14px; font-size: 14px;
//mix-blend-mode: difference; //mix-blend-mode: difference;
} }
&:not(.showIcon){
padding: 0 !important;
.hint, br{
display: none !important;
}
}
} }
.ministrant.edit { .ministrant.edit {
@ -264,6 +271,11 @@ td:nth-child(2n), th:nth-child(2n){
padding-bottom: 10px; padding-bottom: 10px;
} }
.name{
align-items: center;
height: 100%;
}
.mark{ .mark{
cursor: pointer; cursor: pointer;
&.neutral i { &.neutral i {

View File

@ -8,6 +8,7 @@ export interface Gottesdienst {
export interface SimplifiedMinistrant { export interface SimplifiedMinistrant {
id: number, id: number,
username: string,
firstname: string, firstname: string,
lastname: string lastname: string
} }
@ -21,6 +22,5 @@ export interface Mark {
export interface PlanModel { export interface PlanModel {
gottesdienste: Gottesdienst[], gottesdienste: Gottesdienst[],
ministranten: SimplifiedMinistrant[], ministranten: SimplifiedMinistrant[],
marks: Mark[], marks: Mark[]
editable: number[]
} }

View File

@ -1,9 +1,10 @@
import type {Gottesdienst} from "@/models/models"; import type {Gottesdienst, Mark} from "@/models/models";
import {Auth} from "@/services/auth";
const API_ENDPOINT = "http://0.0.0.0:8080/api" const API_ENDPOINT = "http://0.0.0.0:8080/api"
async function api(endpoint: string, method: string = "GET", body?: any ) { export async function api(endpoint: string, method: string = "GET", body?: any ) {
let isJson = (typeof body == "object") let isJson = (typeof body == "object")
return fetch(API_ENDPOINT + endpoint, { return fetch(API_ENDPOINT + endpoint, {
method: method, method: method,
@ -11,7 +12,8 @@ async function api(endpoint: string, method: string = "GET", body?: any ) {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Content-Type": (isJson ? "application/json" : "text/plain"), "Content-Type": (isJson ? "application/json" : "text/plain"),
"Access-Control-Allow-Origin": "*" "Access-Control-Allow-Origin": "*",
"Authorization": "Bearer " + Auth.getToken()
} }
}) })
} }
@ -67,23 +69,16 @@ export namespace API {
.then(data => data.status == 200) .then(data => data.status == 200)
} }
export async function login(username: string, password: string): Promise<{ export async function setMarks(marks: Mark[]): Promise<boolean> {
success: boolean, return api("/marks", "PATCH", marks)
token?: string .then(res => Promise.resolve(res.status == 200))
}> {
return api("/auth", "POST", {
username, password
}).then(res => res.json() as Promise<{
success: boolean,
token?: string
}>)
.then(res => {
if(res.success) {
console.log("test")
}
return Promise.resolve(res)
})
} }
export async function resetPassword(username: String): Promise<any> {
return api("/auth/reset", "POST", { username })
.then(res => res.json())
}
} }

View File

@ -0,0 +1,85 @@
import {api} from "@/services/api";
import {Subject} from "rxjs";
export namespace Auth {
let loggedIn = false
let user = ""
let privileges: string[] = []
export const loggedInSubject: Subject<boolean> = new Subject<boolean>()
export function isLoggedIn() {
return loggedIn
}
export function getUser() {
return user;
}
export function getPrivileges() {
return privileges
}
function setToken(token: string) {
window.localStorage.setItem("token", token)
}
export function getToken(): string {
return window.localStorage.getItem("token")
}
export async function login(username: string, password: string): Promise<{
success: boolean,
token?: string
}> {
return api("/auth", "POST", {
username, password
}).then(res => res.json() as Promise<{
success: boolean,
token?: string
privileges?: string[]
}>).then(res => {
if(res.success) {
loggedIn = true
user = username;
privileges = res.privileges ?? []
privileges.push(username)
setToken(res.token ?? "")
loggedInSubject.next(true)
}
return Promise.resolve(res)
})
}
export function logout(){
setToken("")
loggedIn = false;
user = "";
privileges = []
loggedInSubject.next(false)
}
export function checkForToken() {
const token = getToken()
if(token && token != ""){
const payload = parseJwt(token)
loggedIn = true
user = payload.username
privileges = payload.privileges
privileges.push(user)
loggedInSubject.next(true)
}
}
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
}

View File

@ -2,23 +2,26 @@
import {onMounted, reactive, ref, toRaw, computed} from "vue"; import {onMounted, reactive, ref, toRaw, computed} from "vue";
import TablePlan from "@/components/TablePlan.vue"; import TablePlan from "@/components/TablePlan.vue";
import {API} from "@/views/api"; import {API} from "@/services/api";
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models"; import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
import MobilePlan from "@/components/MobilePlan.vue"; import MobilePlan from "@/components/MobilePlan.vue";
import PlanActionBar from "@/components/PlanActionBar.vue"; import PlanActionBar from "@/components/PlanActionBar.vue";
import {Auth} from "@/services/auth";
const MAX_WIDTH_MOBILE = 600;
const plan = reactive<{ const plan = reactive<{
gottesdienste: Gottesdienst[], gottesdienste: Gottesdienst[],
ministranten: SimplifiedMinistrant[], ministranten: SimplifiedMinistrant[],
marks: Mark[], marks: Mark[],
editable: number[] editable: string[]
}>({ }>({
gottesdienste: [], gottesdienste: [],
ministranten: [], ministranten: [],
marks: [], marks: [],
editable: [] editable: []
}) })
const mobile = ref(false) const mobile = ref(window.innerWidth <= MAX_WIDTH_MOBILE)
const editedMarks = reactive<Mark[]>([]); const editedMarks = reactive<Mark[]>([]);
const editPlanAdmin = ref(false) const editPlanAdmin = ref(false)
@ -35,7 +38,7 @@ async function addGodi(data, validate) {
let date = Date.parse(data.date + "T" + data.time); let date = Date.parse(data.date + "T" + data.time);
let attendance = data.attendance && data.attendance != "" let attendance = data.attendance && data.attendance != ""
? Date.parse(data.date + "T" + data.attendance) ? Date.parse(data.date + "T" + data.attendance)
: (date - 1000*60*30) : (date - 1000 * 60 * 30)
console.log(date, attendance, data.date + "T" + data.attendance) console.log(date, attendance, data.date + "T" + data.attendance)
let newGodi = await API.addGottesdienst( let newGodi = await API.addGottesdienst(
data.name, data.name,
@ -50,7 +53,7 @@ async function addGodi(data, validate) {
async function deleteGottedienst(id) { async function deleteGottedienst(id) {
let deleted = await API.deleteGottesdienst(id) let deleted = await API.deleteGottesdienst(id)
if(deleted) { if (deleted) {
let index = plan.gottesdienste.findIndex(godi => godi.id == id) let index = plan.gottesdienste.findIndex(godi => godi.id == id)
plan.gottesdienste.splice(index, 1) plan.gottesdienste.splice(index, 1)
} }
@ -62,64 +65,98 @@ onMounted(async () => {
plan.gottesdienste = fetchedPlan.gottesdienste plan.gottesdienste = fetchedPlan.gottesdienste
plan.ministranten = fetchedPlan.ministranten plan.ministranten = fetchedPlan.ministranten
plan.marks = fetchedPlan.marks plan.marks = fetchedPlan.marks
Auth.checkForToken()
}) })
function getMarks(): Mark[]{ Auth.loggedInSubject.subscribe(loggedIn => {
return plan.marks.filter((mark: Mark) => { if (loggedIn) {
let difMark = editedMarks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid) plan.editable = Auth.getPrivileges()
return !difMark if(Auth.getUser() == "admin"){
}).concat(editedMarks); editPlanAdmin.value = true
}
}else {
editPlanAdmin.value = false
plan.editable = []
}
})
window.addEventListener("resize", (ev) => {
mobile.value = window.innerWidth <= MAX_WIDTH_MOBILE
})
function getMarks(): Mark[] {
return plan.marks.filter((mark: Mark) => {
let difMark = editedMarks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid)
return !difMark
}).concat(editedMarks);
} }
function getDif(): Mark[]{ function getDif(): Mark[] {
return editedMarks.filter((mark: Mark) => { return editedMarks.filter((mark: Mark) => {
let sameMark = plan.marks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid) let sameMark = plan.marks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid)
return (!sameMark && mark.value != 0) || (sameMark && mark.value != sameMark.value) return (!sameMark && mark.value != 0) || (sameMark && mark.value != sameMark.value)
}) })
} }
function canEdit(mid) {
return plan.editable.includes(mid) async function saveChanges() {
const saved = await API.setMarks(getDif())
if(saved) {
plan.marks = getMarks()
editedMarks.length = 0
}
}
async function resetPassword(username: string) {
const result = await API.resetPassword(username)
alert("Neues Passwort für " + username + "\n\n" + result.password)
console.log(result)
}
function canEdit(username: string) {
return plan.editable.includes(username)
} }
function toggleMark(gid, mid) { function toggleMark(gid, mid) {
// TODO: track changes // TODO: track changes
if(!canEdit(mid)) return; const username = plan.ministranten.find(m => m.id == mid)?.username
if (!canEdit(username)) return;
let mark = editedMarks.find(m => m.mid == mid && m.gid == gid); let mark = editedMarks.find(m => m.mid == mid && m.gid == gid);
if (mark) { if (mark) {
mark.value = ((mark.value + 2) % 3) - 1 mark.value = ((mark.value + 2) % 3) - 1
} else { } else {
mark = { mark = {
gid, mid, value: 1 gid, mid, value: 1
}
editedMarks.push(mark)
} }
editedMarks.push(mark)
}
} }
</script> </script>
<template> <template>
<main> <main>
<TablePlan <TablePlan
:gottesdienste="plan.gottesdienste" :gottesdienste="sortedGottesdienste"
:ministranten="plan.ministranten" :ministranten="plan.ministranten"
:marks="getMarks()" :marks="getMarks()"
:editable="plan.editable" :editable="plan.editable"
:edit="editPlanAdmin" :edit="editPlanAdmin"
:small-mode="editPlanAdmin"
@added="addGodi" @added="addGodi"
@delete="deleteGottedienst" @delete="deleteGottedienst"
@toggle-mark="toggleMark" @toggle-mark="toggleMark"
@end-edit="editPlanAdmin = false" @end-edit="editPlanAdmin = false"
@reset-password="resetPassword"
class="plan table" class="plan table"
v-if="!mobile"> v-if="!mobile">
</TablePlan> </TablePlan>
<MobilePlan <MobilePlan
:gottesdienste="plan.gottesdienste" :gottesdienste="sortedGottesdienste"
:ministranten="plan.ministranten" :ministranten="plan.ministranten"
:marks="getMarks()" :marks="getMarks()"
:editable="plan.editable" :editable="plan.editable"
@ -134,6 +171,7 @@ function toggleMark(gid, mid) {
:save="getDif().length > 0" :save="getDif().length > 0"
:plan="false" :plan="false"
:godi="true" :godi="true"
@save="saveChanges()"
/> />
@ -146,6 +184,7 @@ function toggleMark(gid, mid) {
.plan { .plan {
padding-bottom: 100px; padding-bottom: 100px;
} }
.plan.table { .plan.table {
width: 100%; width: 100%;
} }