This commit is contained in:
parent
24f14da9b2
commit
dde21c3ac5
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
public/node_modules
|
||||||
|
public/dist
|
||||||
27
.gitea/workflows/deploy.yaml
Normal file
27
.gitea/workflows/deploy.yaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: "Deploy Miniplan"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: git.walamana.de
|
||||||
|
username: ${{vars.actions_user}}
|
||||||
|
password: ${{secrets.PACKAGES_TOKEN}}
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t git.walamana.de/walamana/miniplan .
|
||||||
|
|
||||||
|
- name: Push Docker image to registry
|
||||||
|
run: docker push git.walamana.de/walamana/miniplan
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
.directory
|
.directory
|
||||||
.idea
|
.idea
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
FROM gradle:latest as build_backend
|
||||||
|
|
||||||
|
COPY --chown=gradle:gradle /private/minis-backend /home/gradle/src
|
||||||
|
WORKDIR /home/gradle/src
|
||||||
|
RUN gradle buildFatJar --no-daemon
|
||||||
|
|
||||||
|
FROM node:18 as build_frontend
|
||||||
|
|
||||||
|
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
COPY --chown=node:node /public ./
|
||||||
|
|
||||||
|
USER node
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build-only
|
||||||
|
|
||||||
|
|
||||||
|
FROM openjdk:latest as miniplan
|
||||||
|
LABEL authors="walamana"
|
||||||
|
|
||||||
|
RUN mkdir -p /app/public
|
||||||
|
COPY --from=build_backend /home/gradle/src/build/libs/*.jar /app/backend.jar
|
||||||
|
COPY --from=build_frontend /home/node/app/dist /app/public
|
||||||
|
COPY /private/minis-backend/application_docker.conf /app/application.conf
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "backend.jar", "-config=application.conf"]
|
||||||
12
Dockerfile.old
Normal file
12
Dockerfile.old
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM openjdk:latest
|
||||||
|
LABEL authors="walamana"
|
||||||
|
|
||||||
|
RUN mkdir /miniplan
|
||||||
|
|
||||||
|
COPY ./private/minis-backend/build/libs/minis-backend-all.jar /miniplan/backend.jar
|
||||||
|
COPY ./private/minis-backend/application_docker.conf /miniplan/application.conf
|
||||||
|
ADD ./public/dist /miniplan/public
|
||||||
|
|
||||||
|
WORKDIR /miniplan
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "backend.jar", "-config=application.conf"]
|
||||||
4
docker-compose.yml
Normal file
4
docker-compose.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
name: "Miniplan"
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
3
private/minis-backend/.dockerignore
Normal file
3
private/minis-backend/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
build
|
||||||
|
local
|
||||||
|
public
|
||||||
13
private/minis-backend/.env
Normal file
13
private/minis-backend/.env
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
KTOR_DEPLOYMENT_PORT=8080
|
||||||
|
KTOR_DEVELOPMENT=false
|
||||||
|
JWT_SECRET=def
|
||||||
|
JWT_ISSUER=localhost
|
||||||
|
JWT_AUDIENCE=localhost
|
||||||
|
JWT_REALM=miniplan
|
||||||
|
DATABASE_URL=jdbc:h2:file:./db
|
||||||
|
DATABASE_DRIVER=org.h2.Driver
|
||||||
|
DATABASE_USER=abc
|
||||||
|
DATABASE_PASSWORD=abc
|
||||||
|
ADMIN_PASSWORD=123
|
||||||
|
FRONTEND_PATH=./public
|
||||||
|
APPLICATION_NAME=Miniplan\ Hl.\ Familie
|
||||||
5
private/minis-backend/.gitignore
vendored
5
private/minis-backend/.gitignore
vendored
@ -34,3 +34,8 @@ out/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
local/
|
||||||
|
db.mv.db
|
||||||
25
private/minis-backend/Dockerfile
Normal file
25
private/minis-backend/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM gradle:latest as build_backend
|
||||||
|
|
||||||
|
COPY --chownn=gradle:gradle . /home/gradle/src
|
||||||
|
WORKDIR /home/gradle/src
|
||||||
|
RUN gradle buildFatJar --no-daemon
|
||||||
|
|
||||||
|
FROM node:18 as build_frontend
|
||||||
|
|
||||||
|
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
COPY --chown=node:node /pub
|
||||||
|
|
||||||
|
|
||||||
|
FROM openjdk:latest
|
||||||
|
LABEL authors="walamana"
|
||||||
|
|
||||||
|
RUN mkdir /miniplan
|
||||||
|
|
||||||
|
COPY ./build/libs/minis-backend-all.jar /miniplan/backend.jar
|
||||||
|
COPY ./application_docker.conf /miniplan/application.conf
|
||||||
|
ADD ./../../public/dist /miniplan/public
|
||||||
|
|
||||||
|
WORKDIR /miniplan
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "backend.jar", "-config=application.conf"]
|
||||||
@ -1,7 +1,7 @@
|
|||||||
ktor {
|
ktor {
|
||||||
development = true
|
development = true
|
||||||
deployment {
|
deployment {
|
||||||
port = 8080
|
port = "${PORT}"
|
||||||
}
|
}
|
||||||
application {
|
application {
|
||||||
modules = [ de.walamana.ApplicationKt.module ]
|
modules = [ de.walamana.ApplicationKt.module ]
|
||||||
@ -9,12 +9,17 @@ ktor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jwt {
|
jwt {
|
||||||
secret = ${SECRET}
|
secret = "${SECRET}"
|
||||||
issuer = "http://0.0.0.0:8080/"
|
issuer = "http://0.0.0.0:8080/"
|
||||||
audience = "http://0.0.0.0:8080/"
|
audience = "http://0.0.0.0:8080/"
|
||||||
realm = "mini-data"
|
realm = "mini-data"
|
||||||
}
|
}
|
||||||
|
|
||||||
admin {
|
database {
|
||||||
password = ${ADMIN}
|
url = "jdbc:h2:file:./db"
|
||||||
|
driver = "org.h2.Driver"
|
||||||
|
}
|
||||||
|
|
||||||
|
admin {
|
||||||
|
password = "${ADMIN}"
|
||||||
}
|
}
|
||||||
27
private/minis-backend/application_docker.conf
Normal file
27
private/minis-backend/application_docker.conf
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
ktor {
|
||||||
|
development = ${KTOR_DEVELOPMENT}
|
||||||
|
deployment {
|
||||||
|
port = ${KTOR_DEPLOYMENT_PORT}
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
modules = [ de.walamana.ApplicationKt.module ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt {
|
||||||
|
secret = ${JWT_SECRET}
|
||||||
|
issuer = ${JWT_ISSUER}
|
||||||
|
audience = ${JWT_AUDIENCE}
|
||||||
|
realm = ${JWT_REALM}
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
url = ${DATABASE_URL}
|
||||||
|
driver = ${DATABASE_DRIVER}
|
||||||
|
user = ${DATABASE_USER}
|
||||||
|
password = ${DATABASE_PASSWORD}
|
||||||
|
}
|
||||||
|
|
||||||
|
admin {
|
||||||
|
password = ${ADMIN_PASSWORD}
|
||||||
|
}
|
||||||
30
private/minis-backend/application_docker_copy.conf
Normal file
30
private/minis-backend/application_docker_copy.conf
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
ktor {
|
||||||
|
deployment {
|
||||||
|
port = ${KTOR_DEPLOYMENT_PORT}
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
modules = [ de.walamana.ApplicationKt.module ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt {
|
||||||
|
secret = "${JWT_SECRET}"
|
||||||
|
issuer = "${JWT_ISSUER}"
|
||||||
|
audience = "${JWT_AUDIENCE}"
|
||||||
|
realm = "${JWT_REALM}"
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
url = "jdbc:postgresql://localhost:5432/miniplan"
|
||||||
|
driver = "org.postgresql.Driver"
|
||||||
|
user = "miniplan"
|
||||||
|
password = "${DB_PASSWORD}"
|
||||||
|
}
|
||||||
|
|
||||||
|
admin {
|
||||||
|
password = "${ADMIN}"
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
name = "${APPLICATION_NAME}"
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ plugins {
|
|||||||
kotlin("jvm") version "1.9.0"
|
kotlin("jvm") version "1.9.0"
|
||||||
id("io.ktor.plugin") version "2.3.3"
|
id("io.ktor.plugin") version "2.3.3"
|
||||||
kotlin("plugin.serialization") version "1.9.0"
|
kotlin("plugin.serialization") version "1.9.0"
|
||||||
|
id("com.palantir.docker") version "0.35.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "de.walamana"
|
group = "de.walamana"
|
||||||
@ -35,12 +36,19 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
|
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
|
||||||
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||||
implementation("com.h2database:h2:$h2_version")
|
|
||||||
implementation("io.ktor:ktor-server-netty-jvm")
|
implementation("io.ktor:ktor-server-netty-jvm")
|
||||||
implementation("ch.qos.logback:logback-classic:$logback_version")
|
implementation("ch.qos.logback:logback-classic:$logback_version")
|
||||||
|
|
||||||
|
// implementation("org.postgresql:postgresql:42.7.2")
|
||||||
|
implementation("com.h2database:h2:$h2_version")
|
||||||
|
|
||||||
|
implementation("io.github.cdimascio:dotenv-kotlin:6.4.1")
|
||||||
implementation("at.favre.lib:bcrypt:0.10.2")
|
implementation("at.favre.lib:bcrypt:0.10.2")
|
||||||
|
|
||||||
testImplementation("io.ktor:ktor-server-tests-jvm")
|
testImplementation("io.ktor:ktor-server-tests-jvm")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
docker {
|
||||||
|
name = "${project.name}:${project.version}"
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
ktor_version=2.3.3
|
ktor_version=2.3.3
|
||||||
kotlin_version=1.9.0
|
kotlin_version=1.9.0
|
||||||
logback_version=1.2.11
|
logback_version=1.4.14
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
exposed_version=0.43.0
|
exposed_version=0.43.0
|
||||||
h2_version=2.1.214
|
h2_version=2.2.224
|
||||||
|
|||||||
1
private/minis-backend/public
Symbolic link
1
private/minis-backend/public
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
./../../public/dist/
|
||||||
@ -2,8 +2,10 @@ package de.walamana
|
|||||||
|
|
||||||
import de.walamana.plugins.*
|
import de.walamana.plugins.*
|
||||||
import de.walamana.views.*
|
import de.walamana.views.*
|
||||||
|
import io.github.cdimascio.dotenv.dotenv
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.engine.*
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.http.content.*
|
||||||
import io.ktor.server.netty.*
|
import io.ktor.server.netty.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
@ -12,7 +14,14 @@ import io.ktor.server.routing.*
|
|||||||
// .start(wait = true)
|
// .start(wait = true)
|
||||||
//}
|
//}
|
||||||
|
|
||||||
fun main(args: Array<String>): Unit = EngineMain.main(args)
|
fun main(args: Array<String>): Unit {
|
||||||
|
dotenv {
|
||||||
|
ignoreIfMissing = true
|
||||||
|
ignoreIfMalformed = true
|
||||||
|
systemProperties = true
|
||||||
|
}
|
||||||
|
EngineMain.main(args)
|
||||||
|
}
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
configureSecurity()
|
configureSecurity()
|
||||||
@ -21,6 +30,9 @@ fun Application.module() {
|
|||||||
configureDatabases()
|
configureDatabases()
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
|
singlePageApplication {
|
||||||
|
vue(System.getenv("FRONTEND_PATH"))
|
||||||
|
}
|
||||||
route("/api") {
|
route("/api") {
|
||||||
configureMinistrantenRoutes()
|
configureMinistrantenRoutes()
|
||||||
configureGottesdiensteRoutes()
|
configureGottesdiensteRoutes()
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
package de.walamana.models
|
||||||
|
|
||||||
|
import de.walamana.plugins.DateAsLong
|
||||||
|
import de.walamana.plugins.dbQuery
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GottesdienstGroup (
|
||||||
|
val id: Int,
|
||||||
|
val from: DateAsLong,
|
||||||
|
val to: DateAsLong
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
object GottesdienstGroups : Table() {
|
||||||
|
val id = integer("id").autoIncrement()
|
||||||
|
val from = long("date_from")
|
||||||
|
val to = long("date_to")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
object GottesdienstGroupDao {
|
||||||
|
|
||||||
|
fun ResultRow.toPlan() = GottesdienstGroup(
|
||||||
|
this[GottesdienstGroups.id],
|
||||||
|
Date(this[GottesdienstGroups.from]),
|
||||||
|
Date(this[GottesdienstGroups.to])
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun getPlans(amount: Int = 10, offset: Long = 0) = dbQuery {
|
||||||
|
GottesdienstGroups.selectAll()
|
||||||
|
.orderBy(GottesdienstGroups.from to SortOrder.DESC)
|
||||||
|
.limit(amount, offset)
|
||||||
|
.map { it.toPlan() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createPlan(from: Date, to: Date) = dbQuery {
|
||||||
|
GottesdienstGroups.insert {
|
||||||
|
it[GottesdienstGroups.from] = from.time
|
||||||
|
it[GottesdienstGroups.to] = to.time
|
||||||
|
}.resultedValues?.firstOrNull()?.toPlan()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updatePlan(group: GottesdienstGroup): Boolean = dbQuery {
|
||||||
|
GottesdienstGroups.upsert {
|
||||||
|
it[id]= group.id
|
||||||
|
it[from] = group.from.time
|
||||||
|
it[to] = group.to.time
|
||||||
|
}.resultedValues?.isNotEmpty() ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deletePlan(id: Int) = dbQuery {
|
||||||
|
GottesdienstGroups.deleteWhere { GottesdienstGroups.id eq id } > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,8 +22,9 @@ object Gottesdienste : Table() {
|
|||||||
val date = long("date")
|
val date = long("date")
|
||||||
val attendance = long("attendance")
|
val attendance = long("attendance")
|
||||||
val planId = integer("planId")
|
val planId = integer("planId")
|
||||||
|
.references(GottesdienstGroups.id, onDelete = ReferenceOption.CASCADE)
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id, planId)
|
override val primaryKey = PrimaryKey(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
object GottesdiensteDao {
|
object GottesdiensteDao {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import de.walamana.plugins.dbQuery
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Mark(
|
data class Mark(
|
||||||
@ -14,9 +15,9 @@ data class Mark(
|
|||||||
|
|
||||||
object Marks : Table() {
|
object Marks : Table() {
|
||||||
val mid = integer("mid")
|
val mid = integer("mid")
|
||||||
.references(Ministranten.id);
|
.references(Ministranten.id, onDelete = ReferenceOption.CASCADE);
|
||||||
val gid = integer("gid")
|
val gid = integer("gid")
|
||||||
.references(Gottesdienste.id);
|
.references(Gottesdienste.id, onDelete = ReferenceOption.CASCADE);
|
||||||
val value = integer("value");
|
val value = integer("value");
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(mid, gid)
|
override val primaryKey = PrimaryKey(mid, gid)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import java.util.*
|
|||||||
data class Ministrant(
|
data class Ministrant(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val username: String,
|
val username: String,
|
||||||
val passwordHash: String,
|
val passwordHash: String? = "",
|
||||||
val firstname: String,
|
val firstname: String,
|
||||||
val lastname: String,
|
val lastname: String,
|
||||||
val birthday: DateAsLong,
|
val birthday: DateAsLong,
|
||||||
@ -56,7 +56,12 @@ object MinistrantenDao {
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun allMinistranten(): List<Ministrant> = dbQuery {
|
suspend fun allMinistranten(): List<Ministrant> = dbQuery {
|
||||||
Ministranten.selectAll().map(::resultRowToMinistrant)
|
Ministranten.selectAll()
|
||||||
|
.orderBy(
|
||||||
|
Ministranten.lastname to SortOrder.ASC,
|
||||||
|
Ministranten.firstname to SortOrder.ASC
|
||||||
|
)
|
||||||
|
.map(::resultRowToMinistrant)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery {
|
suspend fun simplifiedMinistranten(): List<SimplifiedMinistrant> = dbQuery {
|
||||||
@ -65,7 +70,12 @@ object MinistrantenDao {
|
|||||||
Ministranten.username,
|
Ministranten.username,
|
||||||
Ministranten.firstname,
|
Ministranten.firstname,
|
||||||
Ministranten.lastname
|
Ministranten.lastname
|
||||||
).selectAll().map { row ->
|
).selectAll()
|
||||||
|
.orderBy(
|
||||||
|
Ministranten.lastname to SortOrder.ASC,
|
||||||
|
Ministranten.firstname to SortOrder.ASC
|
||||||
|
)
|
||||||
|
.map { row ->
|
||||||
SimplifiedMinistrant(
|
SimplifiedMinistrant(
|
||||||
row[Ministranten.id],
|
row[Ministranten.id],
|
||||||
row[Ministranten.username],
|
row[Ministranten.username],
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.walamana.plugins
|
package de.walamana.plugins
|
||||||
|
|
||||||
|
import de.walamana.models.GottesdienstGroups
|
||||||
import de.walamana.models.Gottesdienste
|
import de.walamana.models.Gottesdienste
|
||||||
import de.walamana.models.Marks
|
import de.walamana.models.Marks
|
||||||
import de.walamana.models.Ministranten
|
import de.walamana.models.Ministranten
|
||||||
@ -11,16 +12,25 @@ import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransacti
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
fun Application.configureDatabases() {
|
fun Application.configureDatabases() {
|
||||||
val driverClassName = "org.h2.Driver"
|
val driverClassName = environment.config.property("database.driver").getString()
|
||||||
val jdbcURL = "jdbc:h2:file:./build/db"
|
val jdbcURL = environment.config.property("database.url").getString()
|
||||||
val database = Database.connect(jdbcURL, driverClassName)
|
val user = environment.config.propertyOrNull("database.user")?.getString()
|
||||||
|
val password = environment.config.propertyOrNull("database.password")?.getString()
|
||||||
|
|
||||||
|
println("Using database driver $driverClassName")
|
||||||
|
|
||||||
|
val database = if(user != null && password != null )
|
||||||
|
Database.connect(jdbcURL, driverClassName, user, password)
|
||||||
|
else
|
||||||
|
Database.connect(jdbcURL, driverClassName)
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(Gottesdienste)
|
|
||||||
SchemaUtils.create(Ministranten)
|
SchemaUtils.create(Ministranten)
|
||||||
|
SchemaUtils.create(GottesdienstGroups)
|
||||||
|
SchemaUtils.create(Gottesdienste)
|
||||||
SchemaUtils.create(Marks)
|
SchemaUtils.create(Marks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun <T> dbQuery(block: suspend () -> T): T =
|
suspend fun <T> dbQuery(block: suspend Transaction.() -> T): T =
|
||||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||||
@ -62,10 +62,15 @@ 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? {
|
||||||
|
println("Username $username password $password")
|
||||||
|
|
||||||
if (username == "admin") {
|
if (username == "admin") {
|
||||||
|
println("Test")
|
||||||
val adminPw = application.environment.config.property("admin.password").getString()
|
val adminPw = application.environment.config.property("admin.password").getString()
|
||||||
|
println("Test $adminPw")
|
||||||
if (adminPw == password) {
|
if (adminPw == password) {
|
||||||
val allMinis = MinistrantenDao.allMinistranten().map { it.username }
|
val allMinis = MinistrantenDao.allMinistranten().map { it.username }
|
||||||
|
println(allMinis)
|
||||||
return Ministrant(
|
return Ministrant(
|
||||||
0, "admin", "", "admin", "admin", Date(), allMinis
|
0, "admin", "", "admin", "admin", Date(), allMinis
|
||||||
)
|
)
|
||||||
|
|||||||
@ -21,6 +21,7 @@ fun Application.configureSerialization() {
|
|||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,9 +13,14 @@ data class Plan (
|
|||||||
|
|
||||||
object PlanDao {
|
object PlanDao {
|
||||||
|
|
||||||
suspend fun constructPlan(planId: Int): Plan {
|
suspend fun constructPlan(planId: Int, isAnonymous: Boolean): Plan {
|
||||||
val gottesdienste = GottesdiensteDao.getGottesdiensteForPlan(planId)
|
val gottesdienste = GottesdiensteDao.getGottesdiensteForPlan(planId)
|
||||||
val ministranten = MinistrantenDao.simplifiedMinistranten()
|
var ministranten = MinistrantenDao.simplifiedMinistranten()
|
||||||
|
if(isAnonymous) {
|
||||||
|
ministranten = ministranten.map {
|
||||||
|
SimplifiedMinistrant(it.id, it.username, it.firstname, if(it.lastname.isNotEmpty()) it.lastname[0].toString() + "." else "")
|
||||||
|
}
|
||||||
|
}
|
||||||
val marks = MarksDao.allMarksForPlan(planId)
|
val marks = MarksDao.allMarksForPlan(planId)
|
||||||
|
|
||||||
return Plan(gottesdienste, ministranten, marks)
|
return Plan(gottesdienste, ministranten, marks)
|
||||||
|
|||||||
@ -81,9 +81,12 @@ fun Route.configureAuthenticationRoutes() {
|
|||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
val request = call.receive<PasswordResetRequest>()
|
val request = call.receive<PasswordResetRequest>()
|
||||||
val newPassword = BCrypt.withDefaults()
|
var newPassword = "";
|
||||||
.hash(Math.random().toInt() * 3 + 4, Math.random().toString().toCharArray())
|
for(i in 0..2) {
|
||||||
.toString().substring(3..10);
|
newPassword += BCrypt.withDefaults()
|
||||||
|
.hash(Math.random().toInt() * 3 + 4, (Math.random().toString() + Math.random().toString()).toCharArray())
|
||||||
|
};
|
||||||
|
newPassword = newPassword.substring(3..10)
|
||||||
|
|
||||||
Security.setPassword(request.username, newPassword)
|
Security.setPassword(request.username, newPassword)
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package de.walamana.views
|
|||||||
|
|
||||||
import de.walamana.models.Ministrant
|
import de.walamana.models.Ministrant
|
||||||
import de.walamana.models.MinistrantenDao
|
import de.walamana.models.MinistrantenDao
|
||||||
|
import de.walamana.models.SimplifiedMinistrant
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
@ -17,9 +18,10 @@ fun Route.configureMinistrantenRoutes() {
|
|||||||
}
|
}
|
||||||
put {
|
put {
|
||||||
val data = call.receive<Ministrant>()
|
val data = call.receive<Ministrant>()
|
||||||
|
val passwordHash = data.passwordHash ?: return@put
|
||||||
val ministrant = MinistrantenDao.createMinistrant(
|
val ministrant = MinistrantenDao.createMinistrant(
|
||||||
data.username,
|
data.username,
|
||||||
data.passwordHash,
|
passwordHash,
|
||||||
data.firstname,
|
data.firstname,
|
||||||
data.lastname,
|
data.lastname,
|
||||||
data.birthday,
|
data.birthday,
|
||||||
@ -29,16 +31,27 @@ fun Route.configureMinistrantenRoutes() {
|
|||||||
}
|
}
|
||||||
patch {
|
patch {
|
||||||
// TODO: Access only by admin
|
// TODO: Access only by admin
|
||||||
|
val simple = false
|
||||||
|
if(simple) {
|
||||||
|
val data = call.receive<SimplifiedMinistrant>()
|
||||||
|
val changed = MinistrantenDao.updateMinistrant(
|
||||||
|
id = data.id,
|
||||||
|
username = data.username,
|
||||||
|
firstname = data.firstname,
|
||||||
|
lastname = data.lastname
|
||||||
|
)
|
||||||
|
}else{
|
||||||
val data = call.receive<Ministrant>()
|
val data = call.receive<Ministrant>()
|
||||||
val changed = MinistrantenDao.updateMinistrant(
|
val changed = MinistrantenDao.updateMinistrant(
|
||||||
data.id,
|
data.id,
|
||||||
data.username,
|
data.username,
|
||||||
data.passwordHash,
|
null,
|
||||||
data.firstname,
|
data.firstname,
|
||||||
data.lastname,
|
data.lastname,
|
||||||
data.birthday,
|
data.birthday,
|
||||||
data.privileges
|
data.privileges
|
||||||
)
|
)
|
||||||
|
}
|
||||||
call.respond(HttpStatusCode.OK)
|
call.respond(HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
delete {
|
delete {
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
package de.walamana.views
|
package de.walamana.views
|
||||||
|
|
||||||
|
import de.walamana.models.GottesdienstGroup
|
||||||
|
import de.walamana.models.GottesdienstGroupDao
|
||||||
|
import de.walamana.models.GottesdienstGroups
|
||||||
import de.walamana.service.PlanDao
|
import de.walamana.service.PlanDao
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.auth.jwt.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
@ -10,9 +15,34 @@ import io.ktor.server.util.*
|
|||||||
fun Route.configurePlanRoutes() {
|
fun Route.configurePlanRoutes() {
|
||||||
route("/plan") {
|
route("/plan") {
|
||||||
get {
|
get {
|
||||||
|
val principal = call.principal<JWTPrincipal>()
|
||||||
|
val isAnonymous = principal == null
|
||||||
val id = call.parameters.getOrFail("id").toInt()
|
val id = call.parameters.getOrFail("id").toInt()
|
||||||
val plan = PlanDao.constructPlan(id);
|
val plan = PlanDao.constructPlan(id, isAnonymous = isAnonymous);
|
||||||
call.respond(plan)
|
call.respond(plan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
route("/groups") {
|
||||||
|
get {
|
||||||
|
val offset = call.parameters.get("offset")?.toLong() ?: 0
|
||||||
|
val amount = call.parameters.get("amount")?.toInt() ?: 10
|
||||||
|
val groups = GottesdienstGroupDao.getPlans(amount, offset)
|
||||||
|
call.respond(groups)
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
val group = call.receive<GottesdienstGroup>()
|
||||||
|
val res = GottesdienstGroupDao.createPlan(group.from, group.to) ?: TODO("fail")
|
||||||
|
call.respond(res)
|
||||||
|
}
|
||||||
|
patch {
|
||||||
|
val group = call.receive<GottesdienstGroup>()
|
||||||
|
val res = GottesdienstGroupDao.updatePlan(group) ?: TODO("fail")
|
||||||
|
call.respond(res)
|
||||||
|
}
|
||||||
|
delete {
|
||||||
|
val id = call.parameters.getOrFail("id").toInt()
|
||||||
|
val res = GottesdienstGroupDao.deletePlan(id)
|
||||||
|
call.respond(hashMapOf("success" to res))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -8,8 +8,8 @@ import io.ktor.server.testing.*
|
|||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
|
|
||||||
class ApplicationTest {
|
class ApplicationTest {
|
||||||
@Test
|
// @Test
|
||||||
fun testRoot() = testApplication {
|
// fun testRoot() = testApplication {
|
||||||
// application {
|
// application {
|
||||||
// configureRouting()
|
// configureRouting()
|
||||||
// }
|
// }
|
||||||
@ -17,5 +17,5 @@ class ApplicationTest {
|
|||||||
// assertEquals(HttpStatusCode.OK, status)
|
// assertEquals(HttpStatusCode.OK, status)
|
||||||
// assertEquals("Hello World!", bodyAsText())
|
// assertEquals("Hello World!", bodyAsText())
|
||||||
// }
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import HelloWorld from './components/HelloWorld.vue'
|
|||||||
import LoginPanel from "@/components/LoginPanel.vue";
|
import LoginPanel from "@/components/LoginPanel.vue";
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import {Auth} from "@/services/auth";
|
import {Auth} from "@/services/auth";
|
||||||
|
import DialogHost from "@/components/dialog/DialogHost.vue";
|
||||||
|
|
||||||
let showPopup = ref(false)
|
let showPopup = ref(false)
|
||||||
let loggedIn = ref(false)
|
let loggedIn = ref(false)
|
||||||
@ -16,6 +17,10 @@ function logout(){
|
|||||||
Auth.logout()
|
Auth.logout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function print(){
|
||||||
|
window.print()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
Auth.checkForToken()
|
Auth.checkForToken()
|
||||||
})
|
})
|
||||||
@ -23,11 +28,12 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav>
|
<nav class="no-print">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
Miniplan
|
Miniplan App
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
|
<button class="flat" @click="print"><i>print</i>Drucken</button>
|
||||||
<button v-if="!loggedIn" 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>
|
<button v-if="loggedIn" class="flat" @click="logout"><i>logout</i> Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
@ -37,6 +43,8 @@ onMounted(() => {
|
|||||||
<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" @success="showPopup = false"/>
|
<LoginPanel :active="showPopup" @success="showPopup = false"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DialogHost/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@ -54,6 +62,9 @@ nav {
|
|||||||
.right {
|
.right {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
@media print{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-container {
|
.popup-container {
|
||||||
@ -77,11 +88,14 @@ nav {
|
|||||||
|
|
||||||
&.show {
|
&.show {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
@import './base.css';
|
@import 'base';
|
||||||
|
@import "transitions";
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -26,6 +27,9 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
--color-btn-background: #d7eaf3;
|
||||||
|
--color-btn-text: #0e2c48;
|
||||||
|
--color-btn-border: #bed4e0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -34,10 +38,10 @@ button {
|
|||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: #d7eaf3;
|
background: var(--color-btn-background);
|
||||||
color: #0e2c48;
|
color: var(--color-btn-text);
|
||||||
border: 1px solid #bed4e0;
|
border: 1px solid var(--color-btn-border);
|
||||||
transition: 100ms border-color;
|
transition: 100ms border-color, 100ms box-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.flat {
|
button.flat {
|
||||||
@ -45,20 +49,40 @@ button.flat {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.red {
|
||||||
|
--color-btn-background: #f3d7d7;
|
||||||
|
--color-btn-text: #480e0e;
|
||||||
|
--color-btn-border: #dbb0b0;
|
||||||
|
}
|
||||||
|
|
||||||
button i {
|
button i {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: #0e2c48;
|
color: var(--color-btn-text);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.icon{
|
||||||
|
padding-right: 10px;
|
||||||
|
i{
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
button.flat:hover{
|
button.flat:hover{
|
||||||
border-color: #e5e5e5;
|
border-color: #e5e5e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(.flat):hover {
|
button:not(.flat):hover {
|
||||||
background: #e4eff6;
|
box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(.flat):active {
|
/*button:not(.flat):active {*/
|
||||||
background: #d0e3f1;
|
/* background: #d0e3f1;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1
public/src/assets/transitions.less
Normal file
1
public/src/assets/transitions.less
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
212
public/src/components/GroupView.vue
Normal file
212
public/src/components/GroupView.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {onMounted, reactive, ref, watch} from "vue";
|
||||||
|
import Input from "@/components/Input.vue";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
groups: any[],
|
||||||
|
admin: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(["change", "new", "delete", "edit"])
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const prev = ref<number | null>(null)
|
||||||
|
const cur = ref(0)
|
||||||
|
const next = ref<number | null>(null)
|
||||||
|
|
||||||
|
const newPlan = reactive({
|
||||||
|
id: 0,
|
||||||
|
from: null,
|
||||||
|
to: null
|
||||||
|
})
|
||||||
|
|
||||||
|
// watch(props, (value, oldValue, onCleanup) => {
|
||||||
|
// setToCurrentDate()
|
||||||
|
// })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setToCurrentDate()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getGroups() {
|
||||||
|
if (newPlan.id != 0) {
|
||||||
|
return [newPlan].concat(props.groups)
|
||||||
|
} else {
|
||||||
|
return props.groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToCurrentDate() {
|
||||||
|
const date = new Date().getTime()
|
||||||
|
let g = props.groups.findIndex(g => date >= g.from && date <= g.to)
|
||||||
|
if (g == -1) {
|
||||||
|
prev.value = 1;
|
||||||
|
cur.value = 0;
|
||||||
|
next.value = null
|
||||||
|
g = props.groups[0]
|
||||||
|
} else {
|
||||||
|
prev.value = g + 1
|
||||||
|
cur.value = g
|
||||||
|
next.value = g - 1
|
||||||
|
}
|
||||||
|
emit("change", get(cur.value)?.id ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forward() {
|
||||||
|
prev.value--;
|
||||||
|
cur.value--;
|
||||||
|
next.value--;
|
||||||
|
|
||||||
|
// emit("change", get(cur.value).id)
|
||||||
|
router.push("/" + get(cur.value).id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function back() {
|
||||||
|
prev.value++;
|
||||||
|
cur.value++;
|
||||||
|
next.value++;
|
||||||
|
// emit("change", get(cur.value).id)
|
||||||
|
router.push("/" + get(cur.value).id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function two(s) {
|
||||||
|
return (s < 10 ? "0" : "") + s
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(time) {
|
||||||
|
const date = new Date(time)
|
||||||
|
return two(date.getDate()) + ". " + getNameOfMonth(date.getMonth())
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateShort(time) {
|
||||||
|
const date = new Date(time)
|
||||||
|
return two(date.getDate()) + "." + two(date.getMonth() + 1) + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNameOfMonth(month): string {
|
||||||
|
switch (month) {
|
||||||
|
case 0:
|
||||||
|
return "Januar";
|
||||||
|
case 1:
|
||||||
|
return "Februar";
|
||||||
|
case 2:
|
||||||
|
return "März";
|
||||||
|
case 3:
|
||||||
|
return "April";
|
||||||
|
case 4:
|
||||||
|
return "Mai";
|
||||||
|
case 5:
|
||||||
|
return "Juni";
|
||||||
|
case 6:
|
||||||
|
return "Juli";
|
||||||
|
case 7:
|
||||||
|
return "August";
|
||||||
|
case 8:
|
||||||
|
return "September";
|
||||||
|
case 9:
|
||||||
|
return "Oktober";
|
||||||
|
case 10:
|
||||||
|
return "November";
|
||||||
|
case 11:
|
||||||
|
return "Dezember";
|
||||||
|
}
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(i) {
|
||||||
|
if (i < 0) return null;
|
||||||
|
return getGroups()[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function index(id) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateToValueString(time) {
|
||||||
|
const date = new Date(time)
|
||||||
|
return date.getFullYear() + "-" + two(date.getMonth() + 1) + "-" + two(date.getDate())
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bar">
|
||||||
|
<div class="controls">
|
||||||
|
<button class="flat left" v-if="prev != null && prev <= groups.length - 1" @click="back">
|
||||||
|
<i>arrow_left_alt</i>{{ formatDateShort(get(prev).from) }} - {{ formatDateShort(get(prev).to) }}
|
||||||
|
</button>
|
||||||
|
<div class="width: 100%"/>
|
||||||
|
<button class="flat right" v-if="next != null && next > 0" @click="forward">
|
||||||
|
{{ formatDateShort(get(next).from) }} - {{ formatDateShort(get(next).to) }}<i>arrow_right_alt</i>
|
||||||
|
</button>
|
||||||
|
<button class="flat right" v-if="(next == null || next <= 0) && admin" style="margin-right: 20px"
|
||||||
|
@click="$emit('new')">
|
||||||
|
<i>add</i> Neuer plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<template v-if="groups.length > 0">
|
||||||
|
<span style="z-index: 1">Miniplan vom {{ formatDate(get(cur).from) }} bis {{ formatDate(get(cur).to) }}</span>
|
||||||
|
<span v-if="admin" style="display: flex; align-items: center; z-index: 1">
|
||||||
|
<button class="icon flat" @click="$emit('delete', get(cur).id)"><i>delete</i></button>
|
||||||
|
<button class="icon flat" @click="$emit('edit', get(cur).id)"><i>edit</i></button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else>
|
||||||
|
Keine Gottesdienstgruppen vorhanden
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
display: flex;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1px solid #d7d5d5;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left, .right{
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
i {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin: 0 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,29 +1,71 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
value: any,
|
value: any,
|
||||||
label?: string,
|
label?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
type?: string
|
type?: string,
|
||||||
}>()
|
dateFormat?: "string" | "number",
|
||||||
|
focus?: boolean
|
||||||
|
}>(), {
|
||||||
|
dateFormat: "string"
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits(["update:value"])
|
const emit = defineEmits(["update:value"])
|
||||||
|
const inputEl = ref<HTMLInputElement | undefined>()
|
||||||
const focus = ref(false)
|
const focus = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(props.focus) {
|
||||||
|
inputEl.value?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function update($event) {
|
||||||
|
if(props.type == "date" && props.dateFormat == "number") {
|
||||||
|
console.log($event.target.value)
|
||||||
|
emit("update:value", new Date($event.target.value).getTime())
|
||||||
|
}else{
|
||||||
|
emit('update:value', $event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zeros(val) {
|
||||||
|
return val < 10 ? "0" + val : val + ""
|
||||||
|
}
|
||||||
|
function leading(val) {
|
||||||
|
let leading = ""
|
||||||
|
if(val < 10) leading = "000"
|
||||||
|
else if(val < 100) leading = "00"
|
||||||
|
else if(val < 1000) leading = "0"
|
||||||
|
return leading + val
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue(){
|
||||||
|
if(props.type == "date" && props.dateFormat == "number") {
|
||||||
|
console.log(props.value)
|
||||||
|
const date = new Date(props.value)
|
||||||
|
return leading(date.getFullYear()) + "-" + zeros(date.getMonth() + 1) + "-" + zeros(date.getDate())
|
||||||
|
}else{
|
||||||
|
return props.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<label v-if="label" :class="{up: props.value != '' || focus, focus}">{{ label }}</label>
|
<label v-if="label" :class="{up: props.value != '' || focus || type == 'date', focus}">{{ label }}</label>
|
||||||
<input
|
<input
|
||||||
:value="value"
|
:value="getValue()"
|
||||||
@input="$emit('update:value', $event.target.value)"
|
@input="update"
|
||||||
:type="props.type ? props.type : 'text'"
|
:type="props.type ? props.type : 'text'"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@focusin="focus = true"
|
@focusin="focus = true"
|
||||||
@focusout="focus = false">
|
@focusout="focus = false"
|
||||||
|
ref="inputEl">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
<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, watch} from "vue";
|
||||||
import {API} from "@/services/api";
|
import {API} from "@/services/api";
|
||||||
import {Auth} from "@/services/auth";
|
import {Auth} from "@/services/auth";
|
||||||
|
|
||||||
@ -21,12 +21,23 @@ onMounted(() => {
|
|||||||
|
|
||||||
const username = ref("")
|
const username = ref("")
|
||||||
const password = ref("")
|
const password = ref("")
|
||||||
|
const invalid = ref(false)
|
||||||
|
|
||||||
|
watch(props, (value, oldValue, onCleanup) => {
|
||||||
|
if(!value.active) {
|
||||||
|
username.value = ""
|
||||||
|
password.value = ""
|
||||||
|
invalid.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function attemptLogin() {
|
async function attemptLogin() {
|
||||||
let login = await Auth.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)
|
||||||
|
}else{
|
||||||
|
invalid.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -37,6 +48,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"/>
|
||||||
|
<span v-if="invalid" style="color: red; text-align: center; margin-bottom: 10px">Benutzername oder<br> Passwort falsch.</span>
|
||||||
<button @click="attemptLogin"><i>login</i>Anmelden</button>
|
<button @click="attemptLogin"><i>login</i>Anmelden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,7 @@ function two(s) {
|
|||||||
|
|
||||||
function formatDay(time) {
|
function formatDay(time) {
|
||||||
let date = new Date(time)
|
let date = new Date(time)
|
||||||
return two(date.getDate()) + "." + two(date.getMonth()) + "."
|
return two(date.getDate()) + "." + two(date.getMonth() + 1) + "."
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(time) {
|
function formatTime(time) {
|
||||||
@ -99,7 +99,7 @@ function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMinis() {
|
function getMinis() {
|
||||||
return props.ministranten.filter(m => props.editable.includes(m.username))
|
return props.ministranten.filter(m => props.editable.length === 0 || props.editable.includes(m.username))
|
||||||
}
|
}
|
||||||
function getMiniName(mini) {
|
function getMiniName(mini) {
|
||||||
return mini.firstname + " " + mini.lastname
|
return mini.firstname + " " + mini.lastname
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const emit = defineEmits(["addPlan", "save"])
|
const emit = defineEmits(["addPlan", "addGodi", "addMini", "save"])
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
save: boolean,
|
save: boolean,
|
||||||
plan: boolean,
|
plan: boolean,
|
||||||
@ -12,6 +12,8 @@ const props = defineProps<{
|
|||||||
<div class="action-bar" :class="{save: props.save}">
|
<div class="action-bar" :class="{save: props.save}">
|
||||||
<div class="other-action">
|
<div class="other-action">
|
||||||
<button class="add-plan" :class="{show: props.plan}" @click="$emit('addPlan')"> <i class="icon">add_box</i> Neuer Plan</button>
|
<button class="add-plan" :class="{show: props.plan}" @click="$emit('addPlan')"> <i class="icon">add_box</i> Neuer Plan</button>
|
||||||
|
<button class="add-godi" :class="{show: props.godi}" @click="$emit('addGodi')"> <i class="icon">add</i> Gottesdienst</button>
|
||||||
|
<button class="add-mini" :class="{show: props.godi}" @click="$emit('addMini')"> <i class="icon">add</i> Ministrant</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="save" :class="{show: props.save}" @click="$emit('save')"><i class="icon">save</i> Änderungen speichern </button>
|
<button class="save" :class="{show: props.save}" @click="$emit('save')"><i class="icon">save</i> Änderungen speichern </button>
|
||||||
</div>
|
</div>
|
||||||
@ -58,7 +60,11 @@ button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button{
|
@media print {
|
||||||
|
.action-bar{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
import {API} from "@/services/api";
|
import {API} from "@/services/api";
|
||||||
|
|
||||||
import {onMounted, reactive, ref} from "vue";
|
import {onMounted, reactive, ref, toRaw} from "vue";
|
||||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
||||||
import Input from "@/components/Input.vue";
|
import Input from "@/components/Input.vue";
|
||||||
|
import GroupView from "@/components/GroupView.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
gottesdienste: Gottesdienst[],
|
gottesdienste: Gottesdienst[],
|
||||||
@ -14,22 +15,13 @@ const props = defineProps<{
|
|||||||
edit: boolean,
|
edit: boolean,
|
||||||
smallMode: boolean
|
smallMode: boolean
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit", "resetPassword"])
|
const emit = defineEmits(["toggleMark", "added", "delete", "endEdit", "resetPassword", "deleteMinistrant", "createMinistrant", "editMinistrant"])
|
||||||
|
const openEditUser = ref<number>(-1)
|
||||||
|
const miniCopy = reactive<{ data?: SimplifiedMinistrant }>({})
|
||||||
const data = reactive({
|
const data = reactive({
|
||||||
godi: {}
|
godi: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener("keypress", ev => {
|
|
||||||
if(ev.key == "Enter" && props.edit){
|
|
||||||
emit("added", data.godi, () => {
|
|
||||||
data.godi = {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
function getIconForMark(gid, mid) {
|
function getIconForMark(gid, mid) {
|
||||||
const mark = getMark(gid, mid).value
|
const mark = getMark(gid, mid).value
|
||||||
@ -45,7 +37,6 @@ function getIconForMark(gid, mid) {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getClassForMark(gid, mid) {
|
function getClassForMark(gid, mid) {
|
||||||
const mark = getMark(gid, mid).value
|
const mark = getMark(gid, mid).value
|
||||||
return {
|
return {
|
||||||
@ -74,7 +65,7 @@ function two(s) {
|
|||||||
|
|
||||||
function formatDay(time) {
|
function formatDay(time) {
|
||||||
let date = new Date(time)
|
let date = new Date(time)
|
||||||
return two(date.getDate()) + "." + two(date.getMonth()) + "."
|
return two(date.getDate()) + "." + two(date.getMonth() + 1) + "."
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(time) {
|
function formatTime(time) {
|
||||||
@ -112,81 +103,102 @@ function getMinistrantClasses(mini: SimplifiedMinistrant) {
|
|||||||
edit: props.editable.includes(mini.username)
|
edit: props.editable.includes(mini.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleEditMinistrant(mini: SimplifiedMinistrant) {
|
||||||
|
console.log("Toggled", miniCopy.data, mini, props.ministranten, openEditUser)
|
||||||
|
if (openEditUser.value == mini.id) {
|
||||||
|
miniCopy.data = undefined
|
||||||
|
openEditUser.value = -1;
|
||||||
|
} else {
|
||||||
|
openEditUser.value = mini.id
|
||||||
|
miniCopy.data = toRaw(mini)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
|
||||||
<thead>
|
<thead>
|
||||||
|
|
||||||
<tr v-if="props.edit">
|
<tr v-if="props.edit" class="no-print">
|
||||||
<th></th>
|
<th></th>
|
||||||
<th v-for="godi in props.gottesdienste"><i @click="$emit('delete', godi.id)">delete</i></th>
|
<th v-for="godi in props.gottesdienste"><i @click="$emit('delete', godi.id)">delete</i></th>
|
||||||
<th><i @click="$emit('endEdit')">close</i></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th v-for="godi in props.gottesdienste">{{ godi.name }}</th>
|
<th v-for="godi in props.gottesdienste" class="name">{{ godi.name }}</th>
|
||||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.name"/></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="bold">
|
<tr class="bold">
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th v-for="godi in props.gottesdienste">{{ formatDay(godi.date) }}</th>
|
<th v-for="godi in props.gottesdienste">{{ formatDay(godi.date) }}</th>
|
||||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.date" type="date"/></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Uhrzeit</th>
|
<th>Uhrzeit</th>
|
||||||
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.date) }}</th>
|
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.date) }}</th>
|
||||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.time" type="time"/></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="bold">
|
<tr class="bold">
|
||||||
<th>Anwesenheit</th>
|
<th>Anwesenheit</th>
|
||||||
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.attendance) }}</th>
|
<th v-for="godi in props.gottesdienste">{{ formatTime(godi.attendance) }}</th>
|
||||||
<th class="edit" v-if="props.edit"><Input v-model:value="data.godi.attendance" type="time"/></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Wochentag</th>
|
<th>Wochentag</th>
|
||||||
<th v-for="godi in props.gottesdienste">{{ formatWeekday(godi.date) }}</th>
|
<th v-for="godi in props.gottesdienste">{{ formatWeekday(godi.date) }}</th>
|
||||||
<th class="edit" v-if="props.edit"></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</thead>
|
</thead>
|
||||||
<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"><i v-if="edit" style="margin-right: 10px" @click="$emit('resetPassword', mini.username)">lock_reset</i>{{ mini.id }} {{ mini.firstname }} {{ mini.lastname }}</td>
|
<td class="name">
|
||||||
|
<div class="center">
|
||||||
|
<i class="edit-button no-print"
|
||||||
|
v-if="edit"
|
||||||
|
style="margin-right: 10px; font-size: 18px"
|
||||||
|
@click="$emit('editMinistrant', mini.id)">edit</i>
|
||||||
|
{{ mini.firstname }}
|
||||||
|
{{ mini.lastname }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
v-for="godi in props.gottesdienste"
|
v-for="godi in props.gottesdienste"
|
||||||
class="mark"
|
class="mark"
|
||||||
:class="getClassForMark(godi.id, mini.id)"
|
:class="getClassForMark(godi.id, mini.id)"
|
||||||
@click="$emit('toggleMark', godi.id, mini.id)">
|
@click="$emit('toggleMark', godi.id, mini.id)">
|
||||||
<i class="icon"> {{ getIconForMark(godi.id, mini.id) }} </i><br>
|
<i class="icon"> {{ getIconForMark(godi.id, mini.id) }} </i><br>
|
||||||
<span class="hint">{{ getHintForMark(godi.id, mini.id) }}</span>
|
<span class="hint no-print">{{ getHintForMark(godi.id, mini.id) }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="edit" v-if="props.edit"></td>
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
th {
|
th {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
&.edit {
|
&.edit {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bold th {
|
&.bold th {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@ -199,6 +211,7 @@ td {
|
|||||||
td:first-child, th:first-child {
|
td:first-child, th:first-child {
|
||||||
padding: 6px 30px 6px 12px;
|
padding: 6px 30px 6px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
td:nth-child(2n), th:nth-child(2n) {
|
td:nth-child(2n), th:nth-child(2n) {
|
||||||
@ -222,19 +235,15 @@ td:nth-child(2n), th:nth-child(2n){
|
|||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media not print {
|
||||||
&.minus {
|
&.minus {
|
||||||
background: #fdd5d5;
|
background: #fdd5d5;
|
||||||
color: #690b0b;
|
color: #690b0b;
|
||||||
|
|
||||||
i {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.cross {
|
&.cross {
|
||||||
background: #d1fcd1;
|
background: #d1fcd1;
|
||||||
color: #045b04;
|
color: #045b04;
|
||||||
|
|
||||||
i {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,19 +262,47 @@ td:nth-child(2n), th:nth-child(2n){
|
|||||||
//mix-blend-mode: difference;
|
//mix-blend-mode: difference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&:not(.showIcon) {
|
&:not(.showIcon) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
||||||
.hint, br {
|
.hint, br {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ministrant {
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.edit-button {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:not(.show) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 20px 10px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ministrant.edit {
|
.ministrant.edit {
|
||||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@ -274,13 +311,21 @@ td:nth-child(2n), th:nth-child(2n){
|
|||||||
.name {
|
.name {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
i {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mark {
|
.mark {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media not print {
|
||||||
&.neutral i {
|
&.neutral i {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
37
public/src/components/dialog/AlertDialog.vue
Normal file
37
public/src/components/dialog/AlertDialog.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Dialog from "@/components/dialog/Dialog.vue";
|
||||||
|
import type {DialogControls} from "@/components/dialog/dialog";
|
||||||
|
|
||||||
|
interface AlertDialogProps extends DialogControls {
|
||||||
|
title: string,
|
||||||
|
text: string,
|
||||||
|
positive: string,
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<AlertDialogProps>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<Dialog class="dialog">
|
||||||
|
<h3 style="margin-bottom: 10px"><i v-if="icon">{{icon}}</i>{{title}}</h3>
|
||||||
|
<p>{{text}}</p>
|
||||||
|
<div class="buttons" style="display: flex; justify-content: end; margin-top: 20px;">
|
||||||
|
<button @click="onPositive" colored>{{positive}}</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
39
public/src/components/dialog/ConfirmDialog.vue
Normal file
39
public/src/components/dialog/ConfirmDialog.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Dialog from "@/components/dialog/Dialog.vue";
|
||||||
|
import type {DialogControls} from "@/components/dialog/dialog";
|
||||||
|
|
||||||
|
interface ConfirmDialogProps extends DialogControls {
|
||||||
|
title: string,
|
||||||
|
text: string,
|
||||||
|
positive: string,
|
||||||
|
negative: string,
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<ConfirmDialogProps>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<Dialog class="dialog">
|
||||||
|
<h3 style="margin-bottom: 10px"><i v-if="icon">{{icon}}</i>{{title}}</h3>
|
||||||
|
<p>{{text}}</p>
|
||||||
|
<div class="buttons" style="display: flex; justify-content: end; margin-top: 20px;">
|
||||||
|
<button @click="onNegative">{{negative}}</button>
|
||||||
|
<button @click="onPositive" colored>{{positive}}</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
81
public/src/components/dialog/CreateGottesdienstDialog.vue
Normal file
81
public/src/components/dialog/CreateGottesdienstDialog.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Dialog from "@/components/dialog/Dialog.vue";
|
||||||
|
import type {DialogControls} from "@/components/dialog/dialog";
|
||||||
|
import Input from "@/components/Input.vue";
|
||||||
|
import {onMounted, ref, toRaw} from "vue";
|
||||||
|
import type {Gottesdienst, GottesdienstGroup} from "@/models/models";
|
||||||
|
import {onKeyPress} from "@/composables/enter";
|
||||||
|
interface CreateGottesdienstDialogProps extends DialogControls {
|
||||||
|
onCreate: (Gottesdienst) => (Promise<any> | undefined),
|
||||||
|
godi?: Gottesdienst,
|
||||||
|
planId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress("Enter", create)
|
||||||
|
|
||||||
|
const props = defineProps<CreateGottesdienstDialogProps>()
|
||||||
|
|
||||||
|
const date = ref("")
|
||||||
|
const time = ref("")
|
||||||
|
|
||||||
|
const godi = ref(props.godi ?? {
|
||||||
|
planId: props.planId,
|
||||||
|
date: "",
|
||||||
|
attendance: "",
|
||||||
|
name: "",
|
||||||
|
id: -1
|
||||||
|
})
|
||||||
|
|
||||||
|
let submitted = false
|
||||||
|
|
||||||
|
|
||||||
|
async function create(){
|
||||||
|
if(submitted) return;
|
||||||
|
submitted = true
|
||||||
|
await props.onCreate({
|
||||||
|
planId: godi.value.planId,
|
||||||
|
date: new Date(date.value + "T" + time.value),
|
||||||
|
attendance: new Date(date.value + "T" + godi.value.attendance),
|
||||||
|
name: godi.value.name,
|
||||||
|
id: godi.value.id
|
||||||
|
})
|
||||||
|
props.onDismiss()
|
||||||
|
submitted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<Dialog class="dialog">
|
||||||
|
<h3>Gottesdienst {{ godi.id == -1 ? "erstellen" : "bearbeiten"}}</h3>
|
||||||
|
<Input class="input" v-model:value="godi.name" label="Name" focus/>
|
||||||
|
<Input class="input" v-model:value="date" type="date" label="Datum"/>
|
||||||
|
<Input class="input" v-model:value="time" type="time" label="Um"/>
|
||||||
|
<Input class="input" v-model:value="godi.attendance" type="time" label="Anwesenheit"/>
|
||||||
|
<div class="buttons" style="display: flex; justify-content: end; margin-top: 20px;">
|
||||||
|
<button @click="onDismiss">Abbrechen</button>
|
||||||
|
<button @click="create">{{ godi?.id == -1 ? "Erstellen" : "Speichern"}}</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
h3{
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
107
public/src/components/dialog/CreateMinistrantDialog.vue
Normal file
107
public/src/components/dialog/CreateMinistrantDialog.vue
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Dialog from "@/components/dialog/Dialog.vue";
|
||||||
|
import type {DialogControls} from "@/components/dialog/dialog";
|
||||||
|
import Input from "@/components/Input.vue";
|
||||||
|
import {ref, toRaw} from "vue";
|
||||||
|
import {onKeyPress} from "@/composables/enter";
|
||||||
|
import {API} from "@/services/api";
|
||||||
|
import {Dialogs} from "@/services/DialogService";
|
||||||
|
interface CreateMinistrantDialogProps extends DialogControls {
|
||||||
|
onCreate: (any) => (Promise<any> | undefined),
|
||||||
|
onDelete: (id) => (Promise<any> | undefined),
|
||||||
|
ministrant?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress("Enter", create)
|
||||||
|
|
||||||
|
const props = defineProps<CreateMinistrantDialogProps>()
|
||||||
|
|
||||||
|
const date = ref("")
|
||||||
|
const time = ref("")
|
||||||
|
|
||||||
|
let submitted = false
|
||||||
|
|
||||||
|
const ministrant = ref(props.ministrant ?? {
|
||||||
|
id: -1,
|
||||||
|
username: "",
|
||||||
|
firstname: "",
|
||||||
|
lastname: "",
|
||||||
|
birthday: "",
|
||||||
|
privileges: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
async function create(){
|
||||||
|
if(submitted) return;
|
||||||
|
submitted = true
|
||||||
|
const { id, username, firstname, lastname, privileges, birthday } = toRaw(ministrant.value)
|
||||||
|
await props.onCreate({
|
||||||
|
id, username, firstname, lastname, privileges,
|
||||||
|
birthday: new Date(birthday)
|
||||||
|
})
|
||||||
|
props.onDismiss()
|
||||||
|
submitted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMinistrant() {
|
||||||
|
if(submitted) return;
|
||||||
|
submitted = true
|
||||||
|
const mini = ministrant.value
|
||||||
|
const shouldDelete = confirm("Möchtest du wirklich " + mini.firstname + " " + mini.lastname + " löschen? Der Ministrant und alle Eintragungen können nicht wiederhergestellt werden!")
|
||||||
|
|
||||||
|
if(!shouldDelete) return;
|
||||||
|
|
||||||
|
await props.onDelete(mini.id);
|
||||||
|
props.onDismiss()
|
||||||
|
submitted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPassword(username: string) {
|
||||||
|
const result = await API.resetPassword(username)
|
||||||
|
alert("Neues Passwort für " + username + "\n\n" + result.password)
|
||||||
|
console.log(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<Dialog class="dialog">
|
||||||
|
<h3>Ministrant {{ ministrant.id == -1 ? "erstellen" : "bearbeiten"}}</h3>
|
||||||
|
<Input class="input" v-model:value="ministrant.username" label="Nutzername" focus/>
|
||||||
|
<Input class="input" v-model:value="ministrant.firstname" label="Vorname"/>
|
||||||
|
<Input class="input" v-model:value="ministrant.lastname" label="Nachname"/>
|
||||||
|
<Input class="input" v-model:value="ministrant.birthday" type="date" label="Geburtstag"/>
|
||||||
|
<Input class="input" v-model:value="ministrant.privileges" label="Privilegien"/>
|
||||||
|
|
||||||
|
<div class="actions" v-if="ministrant.id != -1">
|
||||||
|
<button @click="resetPassword(ministrant.username)"><i>lock_reset</i>Passwort zurücksetzen
|
||||||
|
</button>
|
||||||
|
<button class="red" @click="deleteMinistrant()"><i>delete</i>Entfernen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons" style="display: flex; justify-content: end; margin-top: 20px;">
|
||||||
|
<button @click="onDismiss">Abbrechen</button>
|
||||||
|
<button @click="create">{{ ministrant.id == -1 ? "Erstellen" : "Speichern"}}</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
h3{
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
66
public/src/components/dialog/CreatePlanDialog.vue
Normal file
66
public/src/components/dialog/CreatePlanDialog.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Dialog from "@/components/dialog/Dialog.vue";
|
||||||
|
import type {DialogControls} from "@/components/dialog/dialog";
|
||||||
|
import Input from "@/components/Input.vue";
|
||||||
|
import {ref, toRaw} from "vue";
|
||||||
|
import type {GottesdienstGroup} from "@/models/models";
|
||||||
|
import {onKeyPress} from "@/composables/enter";
|
||||||
|
|
||||||
|
interface CreatePlanDialogProps extends DialogControls {
|
||||||
|
onCreate: (GottesdienstGroup) => (Promise<any> | undefined),
|
||||||
|
group?: GottesdienstGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress("Enter", create)
|
||||||
|
|
||||||
|
const props = defineProps<CreatePlanDialogProps>()
|
||||||
|
let submitted = false;
|
||||||
|
|
||||||
|
const plan = ref<GottesdienstGroup>(props.group ?? {
|
||||||
|
from: "",
|
||||||
|
to: "",
|
||||||
|
id: -1
|
||||||
|
})
|
||||||
|
|
||||||
|
async function create() {
|
||||||
|
if(submitted) return;
|
||||||
|
submitted = true
|
||||||
|
await props.onCreate(toRaw(plan.value))
|
||||||
|
props.onDismiss()
|
||||||
|
submitted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<Dialog class="dialog">
|
||||||
|
<h3>Neuen Plan {{ plan.id == -1 ? "erstellen" : "bearbeiten" }}</h3>
|
||||||
|
<Input class="input" v-model:value="plan.from" type="date" label="Von" focus/>
|
||||||
|
<Input class="input" v-model:value="plan.to" type="date" label="Bis"/>
|
||||||
|
<div class="buttons" style="display: flex; justify-content: end; margin-top: 20px;">
|
||||||
|
<button @click="onDismiss">Abbrechen</button>
|
||||||
|
<button @click="create">{{ plan.id == -1 ? "Erstellen" : "Speichern" }}</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
h3{
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
26
public/src/components/dialog/Dialog.vue
Normal file
26
public/src/components/dialog/Dialog.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {DialogOptions} from "@/components/dialog/dialog";
|
||||||
|
|
||||||
|
const props = defineProps<DialogOptions>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="dialog">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
padding: 10px 30px 30px 30px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 3px 6px rgba(0,0,0,0.08), 0 3px 6px rgba(0,0,0,0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
76
public/src/components/dialog/DialogHost.vue
Normal file
76
public/src/components/dialog/DialogHost.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import type {Component} from "vue";
|
||||||
|
import {createApp, ref} from "vue";
|
||||||
|
import type {DialogOptions} from "@/components/dialog/dialog";
|
||||||
|
import {Dialogs} from "@/services/DialogService";
|
||||||
|
|
||||||
|
const host = ref<HTMLDivElement>()
|
||||||
|
const active = ref(false)
|
||||||
|
let mountedApplication = null
|
||||||
|
|
||||||
|
Dialogs.subject.subscribe(({component, controls, props}) => {
|
||||||
|
createDialog(component, controls, props)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function createDialog(component: Component, controls: DialogOptions, props: any) {
|
||||||
|
const mergedProps = {
|
||||||
|
onPositive: (...args) => { controls?.onPositive?.call(this, ...args); closeDialog()},
|
||||||
|
onNegative: (...args) => { controls?.onNegative?.call(this, ...args); closeDialog()},
|
||||||
|
onDismiss: closeDialog,
|
||||||
|
...props
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDialog()
|
||||||
|
const app = createApp(component, mergedProps)
|
||||||
|
app.mount(host.value!!)
|
||||||
|
mountedApplication = app
|
||||||
|
active.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
if(mountedApplication != null){
|
||||||
|
mountedApplication.unmount()
|
||||||
|
mountedApplication = null
|
||||||
|
active.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="host" ref="host" :class="{active}" @mousedown.self="closeDialog()">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.host {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: 200ms opacity;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.active{
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
18
public/src/components/dialog/dialog.ts
Normal file
18
public/src/components/dialog/dialog.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface DialogControls {
|
||||||
|
onPositive: () => void,
|
||||||
|
onNegative: () => void,
|
||||||
|
onDismiss: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DialogOptions {
|
||||||
|
onPositive?: () => void,
|
||||||
|
onNegative?: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmDialogOptions {
|
||||||
|
title: string,
|
||||||
|
text: string,
|
||||||
|
positive: string,
|
||||||
|
negative: string,
|
||||||
|
icon?: string
|
||||||
|
}
|
||||||
20
public/src/composables/enter.ts
Normal file
20
public/src/composables/enter.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {onMounted, onUnmounted} from "vue";
|
||||||
|
|
||||||
|
|
||||||
|
export function useWindowEvent<K extends keyof WindowEventMap>(event: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions | undefined){
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener(event, listener, options as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener(event, listener)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onKeyPress(key: string, listener: () => void) {
|
||||||
|
useWindowEvent("keydown", (event) => {
|
||||||
|
if(event.key == key) {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import './assets/main.css'
|
import './assets/main.less'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|||||||
@ -24,3 +24,9 @@ export interface PlanModel {
|
|||||||
ministranten: SimplifiedMinistrant[],
|
ministranten: SimplifiedMinistrant[],
|
||||||
marks: Mark[]
|
marks: Mark[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GottesdienstGroup {
|
||||||
|
from: string,
|
||||||
|
to: string,
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,30 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import HomeView from '../views/PlanView.vue'
|
import HomeView from '../views/PlanView.vue'
|
||||||
|
import {API} from "@/services/api";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/:id',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView
|
component: HomeView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home-redirect",
|
||||||
|
component: null,
|
||||||
|
beforeEnter: async (to, from) => {
|
||||||
|
const groups = (await API.getPlans())
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
return {
|
||||||
|
name: "home",
|
||||||
|
params: {
|
||||||
|
id: groups.length > 0 ? groups[groups.length - 1].id : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
42
public/src/services/DialogService.ts
Normal file
42
public/src/services/DialogService.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type {Component} from "vue";
|
||||||
|
import {Subject} from "rxjs";
|
||||||
|
import type {ConfirmDialogOptions, DialogControls} from "@/components/dialog/dialog";
|
||||||
|
import ConfirmDialog from "@/components/dialog/ConfirmDialog.vue";
|
||||||
|
|
||||||
|
export namespace Dialogs {
|
||||||
|
|
||||||
|
interface NextDialog {
|
||||||
|
component: Component,
|
||||||
|
controls: DialogControls,
|
||||||
|
props: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subject = new Subject<NextDialog>()
|
||||||
|
|
||||||
|
|
||||||
|
export function createDialog(component: Component, controls: DialogControls, props: any) {
|
||||||
|
subject.next({component, controls, props})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirm(options: ConfirmDialogOptions) {
|
||||||
|
return new Promise<boolean | void>((resolve, reject) => {
|
||||||
|
const controls: DialogControls = {
|
||||||
|
onPositive() {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
onNegative() {
|
||||||
|
resolve(false)
|
||||||
|
},
|
||||||
|
onDismiss() {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const next: NextDialog = {
|
||||||
|
component: ConfirmDialog,
|
||||||
|
controls,
|
||||||
|
props: options
|
||||||
|
}
|
||||||
|
subject.next(next)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import type {Gottesdienst, Mark} from "@/models/models";
|
import type {Gottesdienst, GottesdienstGroup, Mark} from "@/models/models";
|
||||||
import {Auth} from "@/services/auth";
|
import {Auth} from "@/services/auth";
|
||||||
|
|
||||||
|
|
||||||
const API_ENDPOINT = "http://0.0.0.0:8080/api"
|
const API_ENDPOINT = import.meta.env.MODE == "development"
|
||||||
|
? "http://0.0.0.0:8080/api"
|
||||||
|
: window.location.origin + "/api"
|
||||||
|
|
||||||
|
|
||||||
export 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")
|
||||||
@ -40,6 +43,26 @@ export namespace API {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPlans(): Promise<GottesdienstGroup[]> {
|
||||||
|
return api("/groups").then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPlan(from, to) {
|
||||||
|
return api("/groups", "POST", {
|
||||||
|
id: -1,
|
||||||
|
from,
|
||||||
|
to
|
||||||
|
}).then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePlan(id) {
|
||||||
|
return api("/groups?id=" + id, "DELETE").then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePlan(group) {
|
||||||
|
return api("/groups", "PATCH", group).then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
function formatGottesdienste(data: any): Array<Gottesdienst>{
|
function formatGottesdienste(data: any): Array<Gottesdienst>{
|
||||||
return data.map(json => {
|
return data.map(json => {
|
||||||
json["date"] = new Date(json["date"])
|
json["date"] = new Date(json["date"])
|
||||||
@ -64,10 +87,26 @@ export namespace API {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMinistranten() {
|
||||||
|
return api("/ministranten", "GET").then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteGottesdienst(id) {
|
export async function deleteGottesdienst(id) {
|
||||||
return api("/gottesdienste?id=" + id, "DELETE")
|
return api("/gottesdienste?id=" + id, "DELETE")
|
||||||
.then(data => data.status == 200)
|
.then(data => data.status == 200)
|
||||||
}
|
}
|
||||||
|
export async function deleteMinistrant(id) {
|
||||||
|
return api("/ministranten?id=" + id, "DELETE")
|
||||||
|
.then(data => data.status == 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMinistrant(ministrant) {
|
||||||
|
return api("/ministranten", "PUT", ministrant).then(data => data.json())
|
||||||
|
}
|
||||||
|
export async function updateMinistrant(ministrant) {
|
||||||
|
return api("/ministranten", "PATCH", ministrant)
|
||||||
|
.then(data => data.status == 200)
|
||||||
|
}
|
||||||
|
|
||||||
export async function setMarks(marks: Mark[]): Promise<boolean> {
|
export async function setMarks(marks: Mark[]): Promise<boolean> {
|
||||||
return api("/marks", "PATCH", marks)
|
return api("/marks", "PATCH", marks)
|
||||||
|
|||||||
@ -1,37 +1,110 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import {onMounted, reactive, ref, toRaw, computed} from "vue";
|
import {computed, onMounted, reactive, ref, toRaw, watch} from "vue";
|
||||||
import TablePlan from "@/components/TablePlan.vue";
|
import TablePlan from "@/components/TablePlan.vue";
|
||||||
import {API} from "@/services/api";
|
import {API} from "@/services/api";
|
||||||
import type {Gottesdienst, Mark, PlanModel, SimplifiedMinistrant} from "@/models/models";
|
import type {Gottesdienst, GottesdienstGroup, Mark, 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";
|
import {Auth} from "@/services/auth";
|
||||||
|
import GroupView from "@/components/GroupView.vue";
|
||||||
|
import {Dialogs} from "@/services/DialogService";
|
||||||
|
import ConfirmDialog from "@/components/dialog/ConfirmDialog.vue";
|
||||||
|
import CreatePlanDialog from "@/components/dialog/CreatePlanDialog.vue";
|
||||||
|
import CreateGottesdienstDialog from "@/components/dialog/CreateGottesdienstDialog.vue";
|
||||||
|
import CreateMinistrantDialog from "@/components/dialog/CreateMinistrantDialog.vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import {min} from "rxjs";
|
||||||
|
|
||||||
const MAX_WIDTH_MOBILE = 600;
|
const MAX_WIDTH_MOBILE = 600;
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const plan = reactive<{
|
const plan = reactive<{
|
||||||
gottesdienste: Gottesdienst[],
|
gottesdienste: Gottesdienst[],
|
||||||
ministranten: SimplifiedMinistrant[],
|
ministranten: SimplifiedMinistrant[],
|
||||||
marks: Mark[],
|
marks: Mark[],
|
||||||
editable: string[]
|
editable: string[],
|
||||||
|
groups: GottesdienstGroup[]
|
||||||
}>({
|
}>({
|
||||||
gottesdienste: [],
|
gottesdienste: [],
|
||||||
ministranten: [],
|
ministranten: [],
|
||||||
marks: [],
|
marks: [],
|
||||||
editable: []
|
editable: [],
|
||||||
|
groups: []
|
||||||
})
|
})
|
||||||
const mobile = ref(window.innerWidth <= MAX_WIDTH_MOBILE)
|
const mobile = ref(window.innerWidth <= MAX_WIDTH_MOBILE)
|
||||||
const editedMarks = reactive<Mark[]>([]);
|
const editedMarks = reactive<Mark[]>([]);
|
||||||
const editPlanAdmin = ref(false)
|
const editPlanAdmin = ref(false)
|
||||||
|
const planId = ref(parseInt(route.params.id as string))
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const groups = await API.getPlans()
|
||||||
|
console.log("Groups", groups)
|
||||||
|
plan.groups = groups
|
||||||
|
loadPlan()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => route.params.id, async (value, oldValue) => {
|
||||||
|
planId.value = parseInt(value as string)
|
||||||
|
await loadPlan()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadPlan() {
|
||||||
|
const { ministranten, gottesdienste, marks} = await API.getPlan(planId.value)
|
||||||
|
plan.ministranten = ministranten
|
||||||
|
plan.gottesdienste = gottesdienste
|
||||||
|
plan.marks = marks
|
||||||
|
Auth.checkForToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth.loggedInSubject.subscribe(async (loggedIn) => {
|
||||||
|
console.log("logged in " + loggedIn)
|
||||||
|
if (loggedIn) {
|
||||||
|
plan.editable = Auth.getPrivileges()
|
||||||
|
if (Auth.getUser() == "admin") {
|
||||||
|
editPlanAdmin.value = true
|
||||||
|
plan.ministranten = await API.getMinistranten();
|
||||||
|
console.log("Plan", plan.ministranten)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editPlanAdmin.value = false
|
||||||
|
plan.editable = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener("resize", (ev) => {
|
||||||
|
mobile.value = window.innerWidth <= MAX_WIDTH_MOBILE
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const sortedGottesdienste = computed(() => {
|
const sortedGottesdienste = computed(() => {
|
||||||
return plan.gottesdienste.sort((a, b) => {
|
return plan.gottesdienste.sort((a, b) => {
|
||||||
console.log(a, b)
|
|
||||||
return a.date - b.date
|
return a.date - b.date
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function createGottesdienst(){
|
||||||
|
Dialogs.createDialog(CreateGottesdienstDialog, {
|
||||||
|
onPositive() {},
|
||||||
|
onNegative() {},
|
||||||
|
onDismiss() {}
|
||||||
|
}, {
|
||||||
|
planId: parseInt(planId.value as string),
|
||||||
|
async onCreate(godi: Gottesdienst) {
|
||||||
|
const newGodi = await API.addGottesdienst(
|
||||||
|
godi.name,
|
||||||
|
godi.date,
|
||||||
|
godi.attendance,
|
||||||
|
godi.planId
|
||||||
|
)
|
||||||
|
plan.gottesdienste.push(newGodi)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function addGodi(data, validate) {
|
async function addGodi(data, validate) {
|
||||||
console.log("Test")
|
console.log("Test")
|
||||||
console.log(data)
|
console.log(data)
|
||||||
@ -44,7 +117,7 @@ async function addGodi(data, validate) {
|
|||||||
data.name,
|
data.name,
|
||||||
new Date(date),
|
new Date(date),
|
||||||
new Date(attendance),
|
new Date(attendance),
|
||||||
0
|
parseInt(planId.value as string)
|
||||||
)
|
)
|
||||||
console.log(newGodi)
|
console.log(newGodi)
|
||||||
plan.gottesdienste.push(newGodi);
|
plan.gottesdienste.push(newGodi);
|
||||||
@ -60,30 +133,6 @@ async function deleteGottedienst(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
let fetchedPlan = await API.getPlan(0)
|
|
||||||
plan.gottesdienste = fetchedPlan.gottesdienste
|
|
||||||
plan.ministranten = fetchedPlan.ministranten
|
|
||||||
plan.marks = fetchedPlan.marks
|
|
||||||
Auth.checkForToken()
|
|
||||||
})
|
|
||||||
|
|
||||||
Auth.loggedInSubject.subscribe(loggedIn => {
|
|
||||||
if (loggedIn) {
|
|
||||||
plan.editable = Auth.getPrivileges()
|
|
||||||
if(Auth.getUser() == "admin"){
|
|
||||||
editPlanAdmin.value = true
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
editPlanAdmin.value = false
|
|
||||||
plan.editable = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener("resize", (ev) => {
|
|
||||||
mobile.value = window.innerWidth <= MAX_WIDTH_MOBILE
|
|
||||||
})
|
|
||||||
|
|
||||||
function getMarks(): Mark[] {
|
function getMarks(): Mark[] {
|
||||||
return plan.marks.filter((mark: Mark) => {
|
return plan.marks.filter((mark: Mark) => {
|
||||||
let difMark = editedMarks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid)
|
let difMark = editedMarks.find((m: Mark) => m.gid == mark.gid && m.mid == mark.mid)
|
||||||
@ -133,11 +182,90 @@ function toggleMark(gid, mid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function createNewPlan() {
|
||||||
|
Dialogs.createDialog(CreatePlanDialog, {
|
||||||
|
onPositive: () => {},
|
||||||
|
onNegative: () => {},
|
||||||
|
onDismiss: () => {},
|
||||||
|
}, {
|
||||||
|
onCreate: async (group: GottesdienstGroup) => {
|
||||||
|
const newPlan = await API.createPlan(new Date(group.from).getTime(), new Date(group.to).getTime());
|
||||||
|
plan.groups = [newPlan].concat(toRaw(plan.groups))
|
||||||
|
router.replace("/" + newPlan.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePlan(id) {
|
||||||
|
if(!confirm("Möchtest du den Plan wirklich löschen?")) return
|
||||||
|
await API.deletePlan(id)
|
||||||
|
location.reload()
|
||||||
|
plan.groups.splice(plan.groups.findIndex(g => g.id == id), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editPlan(id) {
|
||||||
|
const group = Object.assign({}, toRaw(plan.groups.find(g => g.id == id)))
|
||||||
|
group.from = new Date(group.from).toISOString().substring(0,10)
|
||||||
|
group.to = new Date(group.to).toISOString().substring(0,10)
|
||||||
|
Dialogs.createDialog(CreatePlanDialog, {
|
||||||
|
onPositive() {},
|
||||||
|
onNegative() {},
|
||||||
|
onDismiss() {}
|
||||||
|
}, {
|
||||||
|
group,
|
||||||
|
async onCreate(group) {
|
||||||
|
group.from = new Date(group.from).getTime()
|
||||||
|
group.to = new Date(group.to).getTime()
|
||||||
|
await API.updatePlan(group)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createMinistrant(ministrantId?: number) {
|
||||||
|
let ministrantRef = plan.ministranten.find(ministrant => ministrant.id == ministrantId)
|
||||||
|
const ministrant = ministrantRef ? Object.assign({}, toRaw(ministrantRef)) : null;
|
||||||
|
console.log("Found mini ref?", ministrant)
|
||||||
|
Dialogs.createDialog(CreateMinistrantDialog, {
|
||||||
|
onPositive() {},
|
||||||
|
onNegative() {},
|
||||||
|
onDismiss() {}
|
||||||
|
}, {
|
||||||
|
ministrant,
|
||||||
|
async onCreate(ministrant) {
|
||||||
|
ministrant.birthday = ministrant.birthday.getTime()
|
||||||
|
if(ministrant.id == -1) {
|
||||||
|
const newMinistrant = await API.createMinistrant(ministrant)
|
||||||
|
ministrant.id = newMinistrant.id
|
||||||
|
plan.ministranten.push(ministrant)
|
||||||
|
}else {
|
||||||
|
await API.updateMinistrant(ministrant)
|
||||||
|
const index = plan.ministranten.findIndex(m => m.id == ministrant.id)
|
||||||
|
plan.ministranten.splice(index, 1)
|
||||||
|
plan.ministranten.push(ministrant)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onDelete(id) {
|
||||||
|
let deleted = await API.deleteMinistrant(id)
|
||||||
|
if (deleted) {
|
||||||
|
let index = plan.ministranten.findIndex(godi => godi.id == id)
|
||||||
|
plan.ministranten.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<GroupView :groups="plan.groups" :admin="editPlanAdmin"
|
||||||
|
@new="createNewPlan" @delete="deletePlan" @edit="editPlan" class="no-print"/>
|
||||||
<TablePlan
|
<TablePlan
|
||||||
:gottesdienste="sortedGottesdienste"
|
:gottesdienste="sortedGottesdienste"
|
||||||
:ministranten="plan.ministranten"
|
:ministranten="plan.ministranten"
|
||||||
@ -150,6 +278,8 @@ function toggleMark(gid, mid) {
|
|||||||
@toggle-mark="toggleMark"
|
@toggle-mark="toggleMark"
|
||||||
@end-edit="editPlanAdmin = false"
|
@end-edit="editPlanAdmin = false"
|
||||||
@reset-password="resetPassword"
|
@reset-password="resetPassword"
|
||||||
|
@create-ministrant="createMinistrant"
|
||||||
|
@edit-ministrant="createMinistrant"
|
||||||
class="plan table"
|
class="plan table"
|
||||||
v-if="!mobile">
|
v-if="!mobile">
|
||||||
|
|
||||||
@ -167,26 +297,37 @@ function toggleMark(gid, mid) {
|
|||||||
</MobilePlan>
|
</MobilePlan>
|
||||||
|
|
||||||
<PlanActionBar
|
<PlanActionBar
|
||||||
class="action-bar"
|
class="action-bar no-print"
|
||||||
:save="getDif().length > 0"
|
:save="getDif().length > 0"
|
||||||
:plan="false"
|
:plan="false"
|
||||||
:godi="true"
|
:godi="true"
|
||||||
@save="saveChanges()"
|
@save="saveChanges()"
|
||||||
|
@add-godi="createGottesdienst()"
|
||||||
|
@add-mini="createMinistrant()"
|
||||||
|
v-if="editPlanAdmin"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
overflow-x: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.plan {
|
.plan {
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan.table {
|
.plan.table {
|
||||||
width: 100%;
|
//width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar {
|
.action-bar {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user