Add calendar live editing

This commit is contained in:
Lars Westermann 2019-05-10 11:59:41 +02:00
parent 8063e15421
commit 4e5dc610a3
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
35 changed files with 2239 additions and 111 deletions

View file

@ -11,8 +11,8 @@ buildscript {
}
plugins {
id 'kotlin-multiplatform' version '1.3.20'
id 'kotlinx-serialization' version '1.3.20'
id 'kotlin-multiplatform' version '1.3.31'
id 'kotlinx-serialization' version '1.3.31'
id "org.kravemir.gradle.sass" version "1.2.2"
id "com.github.johnrengelman.shadow" version "4.0.4"
}
@ -24,10 +24,11 @@ repositories {
jcenter()
maven { url "http://dl.bintray.com/kotlin/ktor" }
maven { url "https://kotlin.bintray.com/kotlinx" }
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
mavenCentral()
}
def ktor_version = '1.1.2'
def serialization_version = '0.10.0'
def ktor_version = '1.1.5'
def serialization_version = '0.11.0'
kotlin {
jvm() {
@ -72,7 +73,8 @@ kotlin {
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-auth:$ktor_version"
implementation "io.ktor:ktor-locations:$ktor_version"
//implementation "io.ktor:ktor-websockets:$ktor_version"
implementation "io.ktor:ktor-websockets:$ktor_version"
implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21'
implementation "io.ktor:ktor-html-builder:$ktor_version"
@ -98,7 +100,7 @@ kotlin {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
implementation "de.westermann:KObserve-js:0.8.0"
implementation "de.westermann:KObserve-js:0.9.1"
}
}
jsTest {

View file

@ -0,0 +1,96 @@
package kif.common.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
data class Color(
val red: Int,
val green: Int,
val blue: Int,
val alpha: Double = 1.0
) {
override fun toString(): String {
return if (alpha >= 1.0) {
val r = red.toString(16).padStart(2, '0')
val g = green.toString(16).padStart(2, '0')
val b = blue.toString(16).padStart(2, '0')
"#$r$g$b"
} else {
"rgba($red, $green, $blue, $alpha)"
}
}
@Transient
val redDouble
get() = red / 255.0
@Transient
val greenDouble
get() = green / 255.0
@Transient
val blueDouble
get() = blue / 255.0
@Transient
val luminance: Double
get() = 0.2126 * redDouble + 0.7152 * greenDouble + 0.0722 * blueDouble
@Transient
val textColor: Color
get() = if (luminance < 0.7) WHITE else BLACK
companion object {
fun parse(color: String): Color {
val r: Int
val g: Int
val b: Int
val a: Double
if (color.startsWith("#")) {
if (color.length == 3) {
r = color.substring(1, 2).toInt(16)
g = color.substring(2, 3).toInt(16)
b = color.substring(3, 4).toInt(16)
} else {
r = color.substring(1, 3).toInt(16)
g = color.substring(3, 5).toInt(16)
b = color.substring(5, 7).toInt(16)
}
a = 1.0
} else {
val split = color.substringAfter("(").substringBefore(")").split(",")
r = split[0].toInt()
g = split[1].toInt()
b = split[2].toInt()
a = split.getOrNull(3)?.toDouble() ?: 1.0
}
return Color(r, g, b, a)
}
val WHITE = Color(255, 255, 255)
val BLACK = Color(51, 51, 51)
val default = listOf(
"red" to parse("#F44336"),
"pink" to parse("#E91E63"),
"purple" to parse("#9C27B0"),
"deep-purple" to parse("#673AB7"),
"indigo" to parse("#3F51B5"),
"blue" to parse("#1E88E5"),
"light blue" to parse("#03A9F4"),
"cyan" to parse("#00BCD4"),
"teal" to parse("#009688"),
"green" to parse("#43A047"),
"light-green" to parse("#7CB342"),
"lime" to parse("#C0CA33"),
"yellow" to parse("#FDD835"),
"amber" to parse("#FFB300"),
"orange" to parse("#FB8C00"),
"deep-orange" to parse("#F4511E"),
"brown" to parse("#795548"),
"gray" to parse("#9E9E9E"),
"blue gray" to parse("#607D8B")
)
}
}

View file

@ -0,0 +1,3 @@
package kif.common.model
const val CALENDAR_GRID_WIDTH = 15

View file

@ -0,0 +1,58 @@
package kif.common.model
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
@Serializable
data class Message(
@Polymorphic
val message: MessageType
) {
fun stringify(): String = Companion.stringify(this)
companion object {
private val module = SerializersModule {
polymorphic(MessageType::class) {
MessageCreateCalendarEntry::class with MessageCreateCalendarEntry.serializer()
MessageDeleteCalendarEntry::class with MessageDeleteCalendarEntry.serializer()
}
}
private val json = Json(context = module)
fun stringify(message: Message): String {
return json.stringify(serializer(), message)
}
fun parse(message: String): Message {
return json.parse(serializer(), message)
}
}
}
abstract class MessageType()
@Serializable
data class MessageCreateCalendarEntry(
val day: Int,
val time: Int,
val cellTime: Int,
val room: Int,
val workGroupId: Int,
val workGroupName: String,
val workGroupLength: Int,
val workGroupLanguage: String,
val workGroupColor: Color? = null
) : MessageType()
@Serializable
data class MessageDeleteCalendarEntry(
val day: Int,
val time: Int,
val roomId: Int,
val workGroupId: Int
) : MessageType()

View file

@ -1,19 +1,381 @@
package de.kif.frontend
import de.kif.frontend.calendar.Calendar
import de.westermann.kwebview.components.boxView
import de.westermann.kwebview.components.h1
import de.westermann.kobserve.property.mapBinding
import de.westermann.kwebview.*
import de.westermann.kwebview.components.Body
import de.westermann.kwebview.components.init
import kif.common.model.*
import org.w3c.dom.*
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import kotlin.browser.document
import kotlin.browser.window
import kotlin.collections.Iterator
import kotlin.collections.List
import kotlin.collections.emptyList
import kotlin.collections.filter
import kotlin.collections.find
import kotlin.collections.forEach
import kotlin.collections.listOf
import kotlin.collections.minus
import kotlin.collections.plus
import kotlin.dom.appendText
fun main() = init {
clear()
h1("Test")
boxView {
style {
width = "600px"
height = "400px"
margin = "10px"
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
init {
var linkM10: HTMLAnchorElement? = null
var linkM5: HTMLAnchorElement? = null
var linkReset: HTMLAnchorElement? = null
var linkP5: HTMLAnchorElement? = null
var linkP10: HTMLAnchorElement? = null
var linkDel: HTMLAnchorElement? = null
for (element in html.children) {
when {
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
}
}
+Calendar()
linkM10 = linkM10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m10")
link.textContent = "-10"
html.appendChild(link)
link
}
linkM10.removeAttribute("href")
linkM10.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 10}/${entry.workGroup}") {
println("success")
}
}
})
linkM5 = linkM5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-m5")
link.textContent = "-5"
html.appendChild(link)
link
}
linkM5.removeAttribute("href")
linkM5.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 5}/${entry.workGroup}") {
println("success")
}
}
})
linkReset = linkReset ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-reset")
link.textContent = "reset"
html.appendChild(link)
link
}
linkReset.removeAttribute("href")
linkReset.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.cellTime}/${entry.workGroup}") {
println("success")
}
}
})
linkP5 = linkP5 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p5")
link.textContent = "+5"
html.appendChild(link)
link
}
linkP5.removeAttribute("href")
linkP5.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 5}/${entry.workGroup}") {
println("success")
}
}
})
linkP10 = linkP10 ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-p10")
link.textContent = "+10"
html.appendChild(link)
link
}
linkP10.removeAttribute("href")
linkP10.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 10}/${entry.workGroup}") {
println("success")
}
}
})
linkDel = linkDel ?: run {
val link = createHtmlView<HTMLAnchorElement>()
link.classList.add("calendar-tools-del")
link.textContent = "del"
html.appendChild(link)
link
}
linkDel.removeAttribute("href")
linkDel.addEventListener("click", EventListener {
classList += "pending"
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
println("success")
}
})
}
}
class CalendarEntry(view: HTMLElement) : View(view) {
private lateinit var mouseDelta: Point
private var newCell: CalendarCell? = null
var day by dataset.property("day")
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
var time by dataset.property("time")
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
var room by dataset.property("room")
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
var cellTime by dataset.property("cellTime")
var language by dataset.property("language")
var workGroup by dataset.property("workgroup")
val workGroupId by dataset.property("workgroup").mapBinding { it?.toIntOrNull() ?: 0 }
private fun onMove(event: MouseEvent) {
val position = event.toPoint() - mouseDelta
newCell?.classList?.remove("drag")
val cell = calendarCells.find {
position in it.dimension
}
if (cell != null) {
cell.classList.add("drag")
cell += this
newCell = cell
}
event.preventDefault()
event.stopPropagation()
}
private fun onFinishMove(event: MouseEvent) {
classList -= "drag"
newCell?.let { cell ->
cell.classList -= "drag"
val newTime = cell.time
val newRoom = cell.room
val day =
(document.getElementsByClassName("calendar")[0] as? HTMLElement)?.dataset?.get("day")?.toIntOrNull()
?: 0
classList += "pending"
get("/calendar/$day/$room/$time/-1") {
get("/calendar/$day/$newRoom/$newTime/$workGroup") {
println("success")
}
}
}
newCell = null
for (it in listeners) {
it.detach()
}
listeners = emptyList()
event.preventDefault()
event.stopPropagation()
}
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
init {
onMouseDown { event ->
if (event.target != html || "pending" in classList) {
event.stopPropagation()
return@onMouseDown
}
classList += "drag"
mouseDelta = event.toPoint() - point
listeners = listOf(
Body.onMouseMove.reference(this::onMove),
Body.onMouseUp.reference(this::onFinishMove),
Body.onMouseLeave.reference(this::onFinishMove)
)
event.preventDefault()
event.stopPropagation()
}
var calendarTools: CalendarTools? = null
for (item in html.children) {
if (item.classList.contains("calendar-tools")) {
calendarTools = CalendarTools(this, item)
break
}
}
if (calendarTools == null) {
calendarTools = CalendarTools(this, createHtmlView())
html.appendChild(calendarTools.html)
}
}
companion object {
fun create(
day: Int,
time: Int,
cellTime: Int,
room: Int,
workGroupId: Int,
workGroupName: String,
workGroupLength: Int,
workGroupLanguage: String,
workGroupColor: Color?
): CalendarEntry {
val entry = CalendarEntry(createHtmlView())
entry.day = day.toString()
entry.time = time.toString()
entry.cellTime = cellTime.toString()
entry.room = room.toString()
entry.workGroup = workGroupId.toString()
entry.language = workGroupLanguage
if (workGroupColor != null) {
entry.style {
val size = workGroupLength / CALENDAR_GRID_WIDTH.toDouble()
val pos = (time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
val pct = "${pos * 100}%"
val sz = "${size * 100}%"
left = pct
top = "calc($pct + 0.1rem)"
width = sz
height = "calc($sz - 0.2rem)"
backgroundColor = workGroupColor.toString()
color = workGroupColor.textColor.toString()
}
}
entry.html.appendText(workGroupName)
return entry
}
}
}
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
var day by dataset.property("day")
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
var time by dataset.property("time")
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
var room by dataset.property("room")
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
}
var calendarEntries: List<CalendarEntry> = emptyList()
var calendarCells: List<CalendarCell> = emptyList()
fun main() = init {
val ws = WebSocket("ws://${window.location.host}/".also { println(it) })
ws.onmessage = {
val messageWrapper = Message.parse(it.data?.toString() ?: "")
val message = messageWrapper.message
println(message)
when (message) {
is MessageCreateCalendarEntry -> {
val entry = CalendarEntry.create(
message.day,
message.time,
message.cellTime,
message.room,
message.workGroupId,
message.workGroupName,
message.workGroupLength,
message.workGroupLanguage,
message.workGroupColor
)
for (cell in calendarCells) {
if (cell.dayId == message.day && cell.timeId == message.cellTime && cell.roomId == message.room) {
cell.html.appendChild(entry.html)
calendarEntries += entry
break
}
}
}
is MessageDeleteCalendarEntry -> {
val remove = calendarEntries.filter { entry ->
entry.dayId == message.day &&
entry.timeId == message.time &&
entry.roomId == message.roomId &&
entry.workGroupId == message.workGroupId
}
calendarEntries -= remove
remove.forEach {
it.html.remove()
}
}
else -> {
}
}
}
ws.onopen = {
console.log("yes!")
}
calendarEntries = document.getElementsByClassName("calendar-entry")
.iterator().asSequence().map(::CalendarEntry).toList()
calendarCells = document.getElementsByClassName("calendar-cell")
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
}
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
private var index = 0
override fun hasNext(): Boolean {
return index < this@iterator.length
}
override fun next(): HTMLElement {
return this@iterator.get(index++) as HTMLElement
}
}

View file

@ -1,7 +1,7 @@
package de.westermann.kwebview
import de.westermann.kobserve.Property
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import kotlin.reflect.KProperty
/**

View file

@ -1,6 +1,6 @@
package de.westermann.kwebview
import de.westermann.kobserve.ListenerReference
import de.westermann.kobserve.event.EventListener
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import org.w3c.dom.DOMTokenList
@ -102,7 +102,7 @@ class ClassList(
throw IllegalArgumentException("Class is not bound!")
}
bound[clazz]?.reference?.remove()
bound[clazz]?.reference?.detach()
bound -= clazz
}
@ -114,6 +114,6 @@ class ClassList(
private data class Bound(
val property: ReadOnlyProperty<Boolean>,
val reference: ListenerReference<Unit>?
val reference: EventListener<Unit>?
)
}

View file

@ -1,11 +1,16 @@
package de.westermann.kwebview
import de.westermann.kobserve.ListenerReference
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.event.EventListener
import org.w3c.dom.DOMStringMap
import org.w3c.dom.get
import org.w3c.dom.set
import kotlin.collections.MutableMap
import kotlin.collections.contains
import kotlin.collections.minusAssign
import kotlin.collections.mutableMapOf
import kotlin.collections.set
/**
* Represents the css classes of an html element.
@ -13,7 +18,7 @@ import org.w3c.dom.set
* @author lars
*/
class DataSet(
private val map: DOMStringMap
private val map: DOMStringMap
) {
private val bound: MutableMap<String, Bound> = mutableMapOf()
@ -49,11 +54,11 @@ class DataSet(
* Set css class present.
*/
operator fun set(key: String, value: String?) =
if (value == null) {
this -= key
} else {
this += key to value
}
if (value == null) {
this -= key
} else {
this += key to value
}
fun bind(key: String, property: ReadOnlyProperty<String>) {
if (key in bound) {
@ -71,22 +76,34 @@ class DataSet(
bound[key] = Bound(key, property, null)
}
fun property(key: String): Property<String?> {
if (key in bound) {
return bound[key]?.propertyNullable as? Property<String?> ?: throw IllegalArgumentException("Class is already bound!")
}
val property = de.westermann.kobserve.property.property(get(key))
bound[key] = Bound(key, property, null)
return property
}
fun unbind(key: String) {
if (key !in bound) {
throw IllegalArgumentException("Class is not bound!")
}
bound[key]?.reference?.remove()
bound[key]?.reference?.detach()
bound -= key
}
private inner class Bound(
val key: String,
val propertyNullable: ReadOnlyProperty<String?>?,
val property: ReadOnlyProperty<String>?
val key: String,
val propertyNullable: ReadOnlyProperty<String?>?,
val property: ReadOnlyProperty<String>?
) {
var reference: ListenerReference<Unit>? = null
var reference: EventListener<Unit>? = null
fun set(value: String?) {
if (propertyNullable != null && propertyNullable is Property) {

View file

@ -1,6 +1,7 @@
package de.westermann.kwebview
import de.westermann.kobserve.EventHandler
import de.westermann.kobserve.event.EventHandler
import org.w3c.dom.DragEvent
import org.w3c.dom.HTMLElement
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.events.FocusEvent
@ -63,6 +64,9 @@ abstract class View(view: HTMLElement = createHtmlView()) {
val dimension: Dimension
get() = html.getBoundingClientRect().toDimension()
val point: Point
get() = dimension.position
var title by AttributeDelegate()
val style = view.style
@ -101,6 +105,15 @@ abstract class View(view: HTMLElement = createHtmlView()) {
val onFocus = EventHandler<FocusEvent>()
val onBlur = EventHandler<FocusEvent>()
val onDragStart = EventHandler<DragEvent>()
val onDrag = EventHandler<DragEvent>()
val onDragEnter = EventHandler<DragEvent>()
val onDragLeave = EventHandler<DragEvent>()
val onDragOver = EventHandler<DragEvent>()
val onDrop = EventHandler<DragEvent>()
val onDragEnd = EventHandler<DragEvent>()
init {
onClick.bind(view, "click")
onDblClick.bind(view, "dblclick")
@ -120,5 +133,17 @@ abstract class View(view: HTMLElement = createHtmlView()) {
onFocus.bind(view, "focus")
onBlur.bind(view, "blur")
onDragStart.bind(view, "dragstart")
onDrag.bind(view, "drag")
onDragEnter.bind(view, "dragenter")
onDragLeave.bind(view, "dragleave")
onDragOver.bind(view, "dragover")
onDrop.bind(view, "drop")
onDragEnd.bind(view, "dragend")
}
companion object {
fun wrap(htmlElement: HTMLElement) = object : View(htmlElement) {}
}
}

View file

@ -66,4 +66,8 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
operator fun V.unaryPlus() {
append(this)
}
companion object {
fun <V : View> wrap(htmlElement: HTMLElement) = object : ViewCollection<V>(htmlElement) {}
}
}

View file

@ -2,7 +2,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection

View file

@ -3,7 +3,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.ValidationProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener

View file

@ -2,7 +2,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection

View file

@ -2,7 +2,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.HTMLImageElement

View file

@ -3,7 +3,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.ValidationProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kobserve.not
import de.westermann.kwebview.*
import org.w3c.dom.HTMLInputElement

View file

@ -2,7 +2,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.*
import org.w3c.dom.HTMLLabelElement

View file

@ -2,7 +2,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.AttributeDelegate
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.ViewCollection

View file

@ -2,7 +2,7 @@ package de.westermann.kwebview.components
import de.westermann.kobserve.Property
import de.westermann.kobserve.ReadOnlyProperty
import de.westermann.kobserve.basic.property
import de.westermann.kobserve.property.property
import de.westermann.kwebview.KWebViewDsl
import de.westermann.kwebview.View
import de.westermann.kwebview.ViewCollection

View file

@ -1,11 +1,13 @@
package de.westermann.kwebview
import de.westermann.kobserve.EventHandler
import de.westermann.kobserve.event.EventHandler
import org.w3c.dom.DOMRect
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
import org.w3c.dom.events.MouseEvent
import org.w3c.xhr.FormData
import org.w3c.xhr.XMLHttpRequest
import kotlin.browser.document
import kotlin.browser.window
@ -15,9 +17,8 @@ inline fun <reified V : HTMLElement> createHtmlView(tag: String? = null): V {
tagName = tag
} else {
tagName = V::class.js.name.toLowerCase().replace("html([a-z]*)element".toRegex(), "$1")
if (tagName.isBlank()) {
tagName = "div"
}
if (tagName.isBlank()) tagName = "div"
if (tagName == "anchor") tagName = "a"
}
return document.createElement(tagName) as V
}
@ -77,3 +78,66 @@ fun interval(timeout: Int, block: () -> Unit): Int {
fun clearInterval(id: Int) {
window.clearInterval(id)
}
fun get(
url: String,
data: Map<String, String> = emptyMap(),
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("GET", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
val formData = FormData()
for ((key, value) in data) {
formData.append(key, value)
}
xhttp.send(formData)
} else {
xhttp.send()
}
}
fun post(
url: String,
data: Map<String, String> = emptyMap(),
onError: (Int) -> Unit = {},
onSuccess: (String) -> Unit = {}
) {
val xhttp = XMLHttpRequest()
xhttp.onreadystatechange = {
if (xhttp.readyState == 4.toShort()) {
console.log(xhttp.status)
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
onSuccess(xhttp.responseText)
} else {
onError(xhttp.status.toInt())
}
}
}
xhttp.open("POST", url, true)
if (data.isNotEmpty()) {
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
val formData = FormData()
for ((key, value) in data) {
formData.append(key, value)
}
xhttp.send(formData)
} else {
xhttp.send()
}
}

View file

@ -102,6 +102,7 @@ a {
position: relative;
text-transform: uppercase;
white-space: nowrap;
z-index: 6;
&::after {
content: '';
@ -157,13 +158,24 @@ a {
display: none;
position: absolute;
background-color: $background-secondary-color;
z-index: 1;
z-index: 5;
left: 1rem;
right: 1rem;
&::before {
content: '';
top: -8rem;
left: 0;
right: 0;
height: 8rem;
display: block;
position: absolute;
}
a {
display: block;
line-height: 3rem;
&:after {
bottom: 0.2rem;
}
@ -192,6 +204,7 @@ a {
a {
display: inline-block;
line-height: unset;
&:after {
bottom: 1.8em
}
@ -215,7 +228,7 @@ a {
.btn-search {
position: absolute;
top: 0;
height: calc(2.5rem + 2px);
height: 2.5rem;
line-height: 2.5rem;
right: -3px;
border-bottom-left-radius: 0;
@ -228,9 +241,6 @@ a {
input:focus ~ .btn-search {
border-color: $primary-color;
border-width: 2px;
margin-top: 0;
height: calc(2.5rem + 4px);
margin-right: 1px;
}
}
@ -282,6 +292,7 @@ a {
outline: none;
padding: 0 1rem;
line-height: 2.5rem;
height: 2.5rem;
width: 100%;
background-color: $background-primary-color;
border-radius: 0.2rem;
@ -291,10 +302,14 @@ a {
&:focus {
border-color: $primary-color;
border-width: 2px;
margin: 0;
}
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 $text-primary-color;
}
.form-group {
padding-bottom: 1rem;
display: block;
@ -411,7 +426,7 @@ a {
border-color: $error-color;
}
button::-moz-focus-inner, input::-moz-focus-inner {
button::-moz-focus-inner, input::-moz-focus-inner, select::-moz-focus-inner {
border: 0;
}
@ -425,3 +440,358 @@ form {
width: 24rem;
}
}
.input-group {
display: flex;
.form-btn {
height: 2.5rem;
line-height: 2.5rem;
margin-top: 1px;
}
& > * {
margin-right: 0;
&:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin-left: -1px;
}
&:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.calendar {
width: 100%;
position: relative;
margin-top: 1rem;
& > div {
width: calc(100% - 6rem);
overflow-x: scroll;
overflow-y: visible;
margin-left: 6rem;
padding-bottom: 1rem;
}
}
.calendar-tools {
position: absolute;
top: -3rem;
right: 0;
background-color: #fff;
padding: 0.2rem 0.5rem;
border-radius: 0.2rem;
display: none;
z-index: 10;
border: solid 1px $border-color;
box-shadow: 0 0.1rem 0.2rem rgba($primary-text-color);
a {
padding: 0.2rem;
}
&::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: -1rem;
height: 1rem;
}
}
.calendar-entry {
position: absolute;
display: block;
border-radius: 0.2rem;
z-index: 1;
line-height: 2rem;
font-size: 0.8rem;
white-space: nowrap;
text-overflow: ellipsis;
background-color: $primary-color;
color: $primary-text-color;
padding: 0 0.5rem;
&::after {
content: attr(data-language);
bottom: 0;
right: 1rem;
opacity: 0.6;
position: absolute;
text-transform: uppercase;
}
&:hover .calendar-tools {
display: block;
}
&.drag {
box-shadow: 0 0.1rem 0.2rem rgba($text-primary-color, 0.8);
z-index: 2;
.calendar-tools {
display: none;
}
}
&.pending::before {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: $background-primary-color;
opacity: 0.6;
}
}
.calendar-table-time-to-room {
.calendar-header, .calendar-row {
display: flex;
flex-wrap: nowrap;
width: max-content;
}
.calendar-header {
line-height: 2rem;
height: 2rem;
.calendar-cell:not(:first-child) {
font-size: 0.8rem;
width: 6rem;
position: relative;
padding-left: 0.2rem;
&::before {
content: '';
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
position: absolute;
}
}
}
.calendar-row {
border-top: solid 1px rgba($text-primary-color, 0.1);
line-height: 3rem;
height: 3rem;
.calendar-cell {
position: relative;
width: 1.5rem;
&:nth-child(2n + 2)::before {
content: '';
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
position: absolute;
}
&:hover {
background-color: rgba($text-primary-color, 0.06);
}
.calendar-entry {
top: 0.5rem !important;
bottom: 0.5rem;
width: 0;
left: 0;
height: auto !important;
}
}
}
.calendar-cell:first-child {
position: absolute;
width: 6rem;
left: 0;
text-align: center;
border-right: solid 1px rgba($text-primary-color, 0.1);
}
.calendar-link {
display: block;
width: 100%;
height: 100%;
}
}
.calendar-table-room-to-time {
.calendar-header, .calendar-row {
display: flex;
flex-wrap: nowrap;
width: max-content;
}
.calendar-header {
line-height: 2rem;
height: 2rem;
.calendar-cell:not(:first-child) {
width: 12rem;
position: relative;
padding-left: 0.2rem;
&::before {
content: '';
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
position: absolute;
}
}
}
.calendar-row {
line-height: 2rem;
height: 1.3rem;
&:nth-child(4n + 2)::before {
content: '';
position: absolute;
width: calc(100% - 6rem);
border-top: solid 1px rgba($text-primary-color, 0.1);
}
.calendar-cell {
position: relative;
width: 12rem;
&::before {
content: '';
height: 100%;
left: 0;
top: 0;
border-left: solid 1px rgba($text-primary-color, 0.1);
position: absolute;
}
&:hover {
background-color: rgba($text-primary-color, 0.06);
}
.calendar-entry {
top: 0.1rem;
bottom: 0;
width: auto !important;
left: 0.1rem !important;
right: 0.1rem;
}
}
.calendar-cell:first-child:not(:empty)::before {
width: 100%;
top: 0;
border-left: none;
border-top: solid 1px rgba($text-primary-color, 0.1);
}
}
.calendar-cell:first-child {
position: absolute;
width: 6rem;
left: 0;
text-align: center;
}
.calendar-link {
display: block;
width: 100%;
height: 100%;
}
}
.color-picker {
.color-picker-entry {
position: relative;
width: 4rem;
height: 4rem;
float: left;
input {
visibility: hidden;
}
label {
position: absolute;
top: 0.5rem;
left: 0.5rem;
width: 3rem;
height: 3rem;
border-radius: 1.5rem;
z-index: 1;
}
input:checked ~ label::before {
content: '';
position: absolute;
background-color: transparent;
top: 1rem;
left: 0.8rem;
width: 1.1rem;
height: 0.5rem;
transform: rotate(-45deg);
border-left: solid 0.3rem $background-primary-color;
border-bottom: solid 0.3rem $background-primary-color;
}
}
.color-picker-custom {
position: relative;
clear: both;
height: 4rem;
& > input {
visibility: hidden;
}
label {
position: absolute;
top: 0.5rem;
left: 0.5rem;
width: 3rem;
height: 3rem;
border-radius: 1.5rem;
z-index: 1;
border: solid 0.1rem $text-primary-color;
}
input:checked ~ label::before {
content: '';
position: absolute;
background-color: transparent;
top: 1rem;
left: 0.8rem;
width: 1.1rem;
height: 0.5rem;
transform: rotate(-45deg);
border-left: solid 0.3rem $text-primary-color;
border-bottom: solid 0.3rem $text-primary-color;
}
label input {
position: absolute;
left: 4rem;
top: 0.2rem;
height: 2rem;
width: 4rem;
}
}
}

View file

@ -20,7 +20,9 @@ import io.ktor.response.respondRedirect
import io.ktor.routing.routing
import io.ktor.sessions.*
import io.ktor.util.hex
import io.ktor.websocket.WebSockets
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
data class PortalSession(val id: Int, val name: String) {
suspend fun getUser(call: ApplicationCall): User {
@ -37,8 +39,39 @@ data class PortalSession(val id: Int, val name: String) {
@Location("/")
class LocationDashboard()
@Location("/calendar")
class LocationCalendar()
@Location("/calendar/{day}")
data class LocationCalendar(val day: Int) {
@Location("/{room}/{time}")
data class LocationCalendarEdit(
val calendar: LocationCalendar,
val room: Int,
val time: Int,
val search: String = ""
) {
val day: Int get() = calendar.day
}
@Location("/{room}/{time}/{workGroup}")
data class LocationCalendarSet(
val calendar: LocationCalendar,
val room: Int,
val time: Int,
val workGroup: Int,
val next: String? = null
) {
val day: Int get() = calendar.day
}
@Location("/time-to-room")
data class LocationCalendarTimeToRoom(val calendar: LocationCalendar){
val day: Int get() = calendar.day
}
@Location("/room-to-time")
data class LocationCalendarRoomToTime(val calendar: LocationCalendar){
val day: Int get() = calendar.day
}
}
@Location("/login")
data class LocationLogin(val username: String = "", val password: String = "", val next: String = "/")
@ -73,6 +106,18 @@ data class LocationWorkGroup(val search: String = "") {
data class Delete(val id: Int)
}
@Location("/track")
data class LocationTrack(val search: String = "") {
@Location("/{id}")
data class Edit(val id: Int)
@Location("/new")
class New()
@Location("/{id}/delete")
data class Delete(val id: Int)
}
@Location("/room")
data class LocationRoom(val search: String = "") {
@Location("/{id}")
@ -106,6 +151,7 @@ fun Application.main() {
install(Compression)
install(DataConversion)
install(Locations)
install(WebSockets)
install(Authentication) {
form {
@ -145,9 +191,12 @@ fun Application.main() {
account()
workGroup()
track()
room()
person()
user()
pushService()
}
}
}

View file

@ -19,7 +19,7 @@ object Connection {
DbPerson, DbPersonConstraint,
DbTrack, DbWorkGroup, DbWorkGroupConstraint,
DbLeader, DbWorkGroupOrder,
DbRoom, DbTimeSlot, DbSchedule,
DbRoom, DbSchedule,
DbUser, DbUserPermission
)
}

View file

@ -25,6 +25,7 @@ object DbPersonConstraint : Table() {
object DbTrack : Table() {
val id = integer("id").autoIncrement().primaryKey()
val name = varchar("name", 64)
val color = varchar("color", 32)
}
object DbWorkGroup : Table() {
@ -35,6 +36,7 @@ object DbWorkGroup : Table() {
val trackId = integer("track_id").nullable()
val projector = bool("projector")
val resolution = bool("resolution")
val language = enumeration("language", Language::class)
val length = integer("length")
@ -70,18 +72,11 @@ object DbRoom : Table() {
val projector = bool("projector")
}
object DbTimeSlot : Table() {
val id = integer("id").autoIncrement().primaryKey()
val time = integer("time")
val duration = integer("duration")
val day = integer("day")
}
object DbSchedule : Table() {
val workGroupId = integer("work_group_id").primaryKey(0)
val timeSlotId = integer("time_slot_id").primaryKey(1)
val roomId = integer("room_id").primaryKey(2)
val day = integer("day").primaryKey(1)
val time = integer("time_slot").primaryKey(2)
val roomId = integer("room_id").primaryKey(3)
}
enum class DbConstraintType {
@ -98,3 +93,10 @@ object DbUserPermission : Table() {
val userId = integer("id").primaryKey(0)
val permission = enumeration("permission", Permission::class).primaryKey(1)
}
enum class Language(val value: String) {
GERMAN("Deutsch"), ENGLISH("English");
override fun toString() = value
val code = value.take(2).toLowerCase()
}

View file

@ -1,4 +0,0 @@
package de.kif.backend.model
class Day {
}

View file

@ -34,15 +34,34 @@ class Room(
suspend fun delete() {
val id = id
if (id >= 0) {
for (it in Schedule.getByRoom(id)) {
it.delete()
}
dbQuery {
DbRoom.deleteWhere { DbRoom.id eq id }
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Room
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id
}
companion object {
suspend fun get(roomId: Int): Room? = dbQuery {
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: return@dbQuery null
suspend fun get(roomId: Int): Room = dbQuery {
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: throw IllegalArgumentException()
Room(
result[DbRoom.id],
result[DbRoom.name],

View file

@ -0,0 +1,99 @@
package de.kif.backend.model
import de.kif.backend.database.DbSchedule
import de.kif.backend.database.dbQuery
import org.jetbrains.exposed.sql.*
data class Schedule(
val workGroupId: Int,
val day: Int,
val time: Int,
val roomId: Int
) {
lateinit var workGroup: WorkGroup
lateinit var room: Room
suspend fun save() {
delete()
dbQuery {
DbSchedule.insert {
it[workGroupId] = this@Schedule.workGroupId
it[day] = this@Schedule.day
it[time] = this@Schedule.time
it[roomId] = this@Schedule.roomId
}
}
}
suspend fun delete() {
dbQuery {
DbSchedule.deleteWhere {
(DbSchedule.workGroupId eq workGroupId) and
(DbSchedule.day eq day) and
(DbSchedule.time eq time) and
(DbSchedule.roomId eq roomId)
}
}
}
suspend fun loadConstraints() {
try {
workGroup = WorkGroup.get(workGroupId)
room = Room.get(roomId)
} catch (e: IllegalArgumentException) {
delete()
throw e
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Schedule
if (workGroupId != other.workGroupId) return false
return true
}
override fun hashCode(): Int {
return workGroupId
}
companion object {
private suspend fun parseQuery(block: () -> Query): List<Schedule> =
dbQuery {
val query = block()
query.map { result ->
Schedule(
result[DbSchedule.workGroupId],
result[DbSchedule.day],
result[DbSchedule.time],
result[DbSchedule.roomId]
)
}
}.onEach { it.loadConstraints() }
suspend fun getByRoom(roomId: Int): List<Schedule> = parseQuery {
DbSchedule.select { DbSchedule.roomId eq roomId }
}
suspend fun getByRoom(roomId: Int, day: Int, time: Int): List<Schedule> = parseQuery {
DbSchedule.select { (DbSchedule.roomId eq roomId) and (DbSchedule.day eq day) and (DbSchedule.time eq time) }
}
suspend fun getByWorkGroup(workGroupId: Int): List<Schedule> = parseQuery {
DbSchedule.select { DbSchedule.workGroupId eq workGroupId }
}
suspend fun getByDay(day: Int): List<Schedule> = parseQuery {
DbSchedule.select { DbSchedule.day eq day }
}
suspend fun list(): List<Schedule> = parseQuery {
DbSchedule.selectAll()
}
}
}

View file

@ -1,4 +0,0 @@
package de.kif.backend.model
class TimeSlot {
}

View file

@ -0,0 +1,63 @@
package de.kif.backend.model
import de.kif.backend.database.DbTrack
import de.kif.backend.database.dbQuery
import kif.common.model.Color
import org.jetbrains.exposed.sql.*
import java.lang.IllegalArgumentException
class Track(
var id: Int = -1,
var name: String = "",
var color: Color
) {
suspend fun save() {
if (id < 0) {
dbQuery {
val newId = DbTrack.insert {
it[name] = this@Track.name
it[color] = this@Track.color.toString()
}[DbTrack.id]!!
this@Track.id = newId
}
} else {
dbQuery {
DbTrack.update({ DbTrack.id eq id }) {
it[name] = this@Track.name
it[color] = this@Track.color.toString()
}
}
}
}
suspend fun delete() {
val id = id
if (id >= 0) {
dbQuery {
DbTrack.deleteWhere { DbTrack.id eq id }
}
}
}
companion object {
suspend fun get(TrackId: Int): Track = dbQuery {
val result = DbTrack.select { DbTrack.id eq TrackId }.firstOrNull() ?: throw IllegalArgumentException()
Track(
result[DbTrack.id],
result[DbTrack.name],
Color.parse(result[DbTrack.color])
)
}
suspend fun list(): List<Track> = dbQuery {
val query = DbTrack.selectAll()
query.map { result ->
Track(
result[DbTrack.id],
result[DbTrack.name],
Color.parse(result[DbTrack.color])
)
}
}
}
}

View file

@ -2,7 +2,9 @@ package de.kif.backend.model
import de.kif.backend.database.DbWorkGroup
import de.kif.backend.database.DbWorkGroupConstraint
import de.kif.backend.database.Language
import de.kif.backend.database.dbQuery
import io.ktor.features.NotFoundException
import org.jetbrains.exposed.sql.*
class WorkGroup(
@ -13,10 +15,12 @@ class WorkGroup(
var projector: Boolean = false,
var resolution: Boolean = false,
var length: Int = 0,
var language: Language = Language.GERMAN,
var start: Long? = null,
var end: Long? = null
) {
var constraints: Set<WorkGroupConstraint> = emptySet()
var track: Track? = null
suspend fun save() {
if (id < 0) {
@ -28,6 +32,7 @@ class WorkGroup(
it[projector] = this@WorkGroup.projector
it[resolution] = this@WorkGroup.resolution
it[length] = this@WorkGroup.length
it[language] = this@WorkGroup.language
it[start] = this@WorkGroup.start
it[end] = this@WorkGroup.end
}[DbWorkGroup.id]!!
@ -45,6 +50,7 @@ class WorkGroup(
it[projector] = this@WorkGroup.projector
it[resolution] = this@WorkGroup.resolution
it[length] = this@WorkGroup.length
it[language] = this@WorkGroup.language
it[start] = this@WorkGroup.start
it[end] = this@WorkGroup.end
}
@ -60,6 +66,9 @@ class WorkGroup(
suspend fun delete() {
val id = id
if (id >= 0) {
for (it in Schedule.getByWorkGroup(id)) {
it.delete()
}
dbQuery {
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id }
DbWorkGroup.deleteWhere { DbWorkGroup.id eq id }
@ -70,12 +79,13 @@ class WorkGroup(
suspend fun loadConstraints() {
if (id >= 0) {
constraints = WorkGroupConstraint.get(id)
track = trackId?.let { if (it < 0) null else Track.get(it) }
}
}
companion object {
suspend fun get(workGroupId: Int): WorkGroup? = dbQuery {
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: return@dbQuery null
suspend fun get(workGroupId: Int): WorkGroup = dbQuery {
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: throw IllegalArgumentException()
WorkGroup(
result[DbWorkGroup.id],
result[DbWorkGroup.name],
@ -84,10 +94,11 @@ class WorkGroup(
result[DbWorkGroup.projector],
result[DbWorkGroup.resolution],
result[DbWorkGroup.length],
result[DbWorkGroup.language],
result[DbWorkGroup.start],
result[DbWorkGroup.end]
)
}?.apply { loadConstraints() }
}.apply { loadConstraints() }
suspend fun list(): List<WorkGroup> = dbQuery {
val query = DbWorkGroup.selectAll()
@ -100,6 +111,7 @@ class WorkGroup(
result[DbWorkGroup.projector],
result[DbWorkGroup.resolution],
result[DbWorkGroup.length],
result[DbWorkGroup.language],
result[DbWorkGroup.start],
result[DbWorkGroup.end]
)

View file

@ -1,28 +1,490 @@
package de.kif.backend.route
import de.kif.backend.LocationCalendar
import de.kif.backend.LocationLogin
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.Room
import de.kif.backend.model.Schedule
import de.kif.backend.model.WorkGroup
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.request.path
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import kotlinx.html.h1
import kif.common.model.CALENDAR_GRID_WIDTH
import kif.common.model.MessageCreateCalendarEntry
import kif.common.model.MessageDeleteCalendarEntry
import kotlinx.css.CSSBuilder
import kotlinx.css.Color
import kotlinx.css.pct
import kotlinx.css.rem
import kotlinx.html.*
import kotlin.math.max
import kotlin.math.min
const val MINUTES_OF_DAY = 24 * 60
private fun DIV.calendarCell(schedule: Schedule?, day: Int, time: Int) {
if (schedule != null) {
span("calendar-entry") {
attributes["style"] = CSSBuilder().apply {
val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
this.left = (pos * 100).pct
this.top = (pos * 100).pct + 0.1.rem
this.width = (size * 100).pct
this.height = (size * 100).pct - 0.2.rem
val c = schedule.workGroup.track?.color
if (c != null) {
backgroundColor = Color(c.toString())
color = Color(c.textColor.toString())
}
}.toString()
attributes["data-language"] = schedule.workGroup.language.code
attributes["data-day"] = schedule.day.toString()
attributes["data-room"] = schedule.room.id.toString()
attributes["data-time"] = schedule.time.toString()
attributes["data-cell-time"] = time.toString()
attributes["data-workgroup"] = schedule.workGroup.id.toString()
+schedule.workGroup.name
div("calendar-tools") {
a(
classes = "calendar-tools-m10",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 10}/${schedule.workGroupId}"
) { +"-10" }
a(
classes = "calendar-tools-m5",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 5}/${schedule.workGroupId}"
) { +"-05" }
a(
classes = "calendar-tools-reset",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/$time/${schedule.workGroupId}"
) { +"reset" }
a(
classes = "calendar-tools-p5",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 5}/${schedule.workGroupId}"
) { +"+05" }
a(
classes = "calendar-tools-p10",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 10}/${schedule.workGroupId}"
) { +"+10" }
a(
classes = "calendar-tools-del",
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1"
) { +"del" }
}
}
}
}
private fun DIV.renderTimeToRoom(
day: Int,
from: Int,
to: Int,
rooms: List<Room>,
schedules: Map<Room, Map<Int, Schedule>>,
allowEdit: Boolean
) {
val gridLabelWidth = 60
val minutesOfDay = to - from
div("calendar-table-time-to-room") {
div("calendar-header") {
div("calendar-cell") {
span {
+"Room"
}
}
for (i in 0 until minutesOfDay / gridLabelWidth) {
div("calendar-cell") {
span {
val time = ((i * gridLabelWidth + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
+"$hours:$minutes"
}
}
}
}
for (room in rooms) {
div("calendar-row") {
div("calendar-cell") {
span {
+room.name
}
}
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val start = i * CALENDAR_GRID_WIDTH + from
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
div("calendar-cell") {
val time = i * CALENDAR_GRID_WIDTH
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
title = "$hours:$minutes"
attributes["data-time"] = time.toString()
attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString()
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule, day, time)
val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null
a(href, classes = "calendar-link")
}
}
}
}
}
}
private fun DIV.renderRoomToTime(
day: Int,
from: Int,
to: Int,
rooms: List<Room>,
schedules: Map<Room, Map<Int, Schedule>>,
allowEdit: Boolean
) {
val gridLabelWidth = 60
val minutesOfDay = to - from
div("calendar-table-room-to-time") {
div("calendar-header") {
div("calendar-cell") {
span {
+"Room"
}
}
for (room in rooms) {
div("calendar-cell") {
span {
+room.name
}
}
}
}
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
if (it < 0) it + MINUTES_OF_DAY else it
}
val minutes = (time % 60).toString().padStart(2, '0')
val hours = (time / 60).toString().padStart(2, '0')
val timeString = "$hours:$minutes"
val start = i * CALENDAR_GRID_WIDTH + from
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
div("calendar-row") {
div("calendar-cell") {
if (time % gridLabelWidth == 0) {
span {
+timeString
}
}
}
for (room in rooms) {
div("calendar-cell") {
attributes["data-time"] = time.toString()
attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString()
title = timeString
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule, day, time)
val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null
a(href, classes = "calendar-link")
}
}
}
}
}
}
fun Route.calendar() {
get<LocationCalendar> {
get("/calendar") {
call.respondRedirect("/calendar/0", true)
}
get<LocationCalendar.LocationCalendarRoomToTime> { param ->
call.response.cookies.append(
"orientation",
CalendarOrientation.ROOM_TO_TIME.name,
maxAge = Int.MAX_VALUE,
path = "/"
)
call.respondRedirect("/calendar/${param.day}")
}
get<LocationCalendar.LocationCalendarTimeToRoom> { param ->
call.response.cookies.append(
"orientation",
CalendarOrientation.TIME_TO_ROOM.name,
maxAge = Int.MAX_VALUE,
path = "/"
)
call.respondRedirect("/calendar/${param.day}")
}
get<LocationCalendar> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
val allowEdit = user?.checkPermission(Permission.SCHEDULE) ?: false
val rooms = Room.list()
val orientation = call.request.cookies["orientation"]?.let { name ->
CalendarOrientation.values().find { it.name == name }
} ?: CalendarOrientation.ROOM_TO_TIME
val day = param.day
val h = Schedule.getByDay(day)
val schedules = h.groupBy { it.room }.mapValues { (_, it) ->
it.associateBy {
it.time
}
}
var max = 0
var min = 24 * 60
for (s in h) {
max = max(max, s.time + s.workGroup.length)
min = min(min, s.time)
}
if (min > max) {
val h1 = max
max = min
min = h1
}
min = (min / 60 - 1) * 60
max = (max / 60 + 2) * 60
min = min(min, 0)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.CALENDAR
}
content {
h1 { +"Calendar" }
if (rooms.isEmpty()) {
return@content
}
div("header") {
div("header-left") {
a("/calendar/${day - 1}") { +"<" }
span {
+"Day $day"
}
a("/calendar/${day + 1}") { +">" }
}
div("header-right") {
a("/calendar/$day/room-to-time") {
+"Room to time"
}
a("/calendar/$day/time-to-room") {
+"Time to room"
}
}
}
div("calendar") {
attributes["data-day"] = day.toString()
div {
when (orientation) {
CalendarOrientation.ROOM_TO_TIME -> renderRoomToTime(
day,
min,
max,
rooms,
schedules,
allowEdit
)
CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom(
day,
min,
max,
rooms,
schedules,
allowEdit
)
}
}
}
}
}
}
get<LocationCalendar.LocationCalendarEdit> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.SCHEDULE)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = WorkGroup.list()
val room = Room.get(param.room)
val day = param.day
val time = param.time
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content {
h1 { +"Select work groups" }
insert(TableTemplate()) {
searchValue = param.search
action {
a("/calendar/$day/${room.id}/$time/-1") {
button(classes = "form-btn btn-primary") {
+"Delete"
}
}
}
header {
th {
+"Name"
}
th {
+"Interested"
}
th {
+"Track"
}
th {
+"Projector"
}
th {
+"Resolution"
}
th {
+"Length"
}
}
for (u in list) {
if (Search.match(param.search, u.name)) {
val href = "/calendar/$day/${room.id}/$time/${u.id}"
entry {
attributes["data-search"] = Search.pack(u.name)
td {
a(href) {
+u.name
}
}
td {
+u.interested.toString()
}
td {
+(u.track?.name ?: "")
}
td {
+u.projector.toString()
}
td {
+u.resolution.toString()
}
td {
+u.length.toString()
}
}
}
}
}
}
}
}
}
get<LocationCalendar.LocationCalendarSet> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.SCHEDULE)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
for (it in Schedule.getByRoom(param.room, param.day, param.time)) {
PushService.notify(
MessageDeleteCalendarEntry(
it.day,
it.time,
it.room.id,
it.workGroup.id
)
)
it.delete()
}
if (param.workGroup >= 0) {
val schedule = Schedule(param.workGroup, param.day, param.time, param.room)
schedule.save()
schedule.loadConstraints()
val cellTime = (schedule.time / 15) * 15
PushService.notify(
MessageCreateCalendarEntry(
schedule.day,
schedule.time,
cellTime,
schedule.room.id,
schedule.workGroup.id,
schedule.workGroup.name,
schedule.workGroup.length,
schedule.workGroup.language.code,
schedule.workGroup.track?.color
)
)
}
call.respondRedirect(param.next ?: "/calendar/${param.day}")
}
}
}
enum class CalendarOrientation {
/**
* Columns contains time
* Rows contains rooms
*
* Like the old kif tool
*/
TIME_TO_ROOM,
/**
* Columns contains rooms
* Rows contains time
*
* Like the congress schedule
*/
ROOM_TO_TIME
}

View file

@ -0,0 +1,47 @@
package de.kif.backend.route
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import io.ktor.routing.Route
import io.ktor.websocket.WebSocketServerSession
import io.ktor.websocket.webSocket
import kif.common.model.Message
import kif.common.model.MessageType
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import java.lang.Exception
object PushService {
var clients: List<WebSocketServerSession> = emptyList()
suspend fun notify(messageType: MessageType) {
try {
val data = Message(messageType).stringify()
for (client in clients) {
client.outgoing.send(Frame.Text(data))
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun Route.pushService() {
webSocket {
PushService.clients += this
try {
while (true) {
val text = (incoming.receive() as Frame.Text).readText()
println("onMessage($text)")
outgoing.send(Frame.Text(text))
}
} catch (_: ClosedReceiveChannelException) {
PushService.clients -= this
} catch (e: Throwable) {
println("onError ${closeReason.await()}")
e.printStackTrace()
PushService.clients -= this
}
}
}

View file

@ -95,7 +95,7 @@ fun Route.room() {
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editRoom = Room.get(roomId.id) ?: return@get
val editRoom = Room.get(roomId.id)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -183,7 +183,7 @@ fun Route.room() {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editRoom = Room.get(roomId.id) ?: return@post
val editRoom = Room.get(roomId.id)
params["name"]?.let { editRoom.name = it }
params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 }
@ -298,7 +298,7 @@ fun Route.room() {
if (user == null || !user.checkPermission(Permission.ROOM)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteRoom = Room.get(roomId.id) ?: return@get
val deleteRoom = Room.get(roomId.id)
deleteRoom.delete()

View file

@ -0,0 +1,304 @@
package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationTrack
import de.kif.backend.PortalSession
import de.kif.backend.model.Permission
import de.kif.backend.model.Track
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.path
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import io.ktor.util.toMap
import kif.common.model.Color
import kotlinx.css.CSSBuilder
import kotlinx.html.*
import kotlin.random.Random
fun DIV.colorPicker(color: Color?) {
val colorString = color?.toString() ?: Color(
Random.nextInt(256),
Random.nextInt(256),
Random.nextInt(256)
).toString()
var found = false
fun DIV.generate(colorId: String, c: Color) {
val name = colorId.replace("-", " ").capitalize()
div("color-picker-entry") {
radioInput(name = "color") {
id = "color-$colorId"
value = colorId
if (color == c) {
checked = true
found = true
}
}
label {
title = name
htmlFor = "color-$colorId"
attributes["style"] = CSSBuilder().apply {
backgroundColor = kotlinx.css.Color(c.toString())
}.toString()
}
}
}
div("color-picker") {
for ((name, c) in Color.default) {
generate(name, c)
}
div("color-picker-custom") {
radioInput(name = "color") {
id = "color-custom"
value = "custom"
if (!found) {
checked = true
}
}
label {
title = "Custom"
htmlFor = "color-custom"
input(
name = "color-input",
type = InputType.color
) {
value = colorString
}
}
}
}
}
fun Route.track() {
get<LocationTrack> { param ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val list = Track.list()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content {
h1 { +"Tracks" }
insert(TableTemplate()) {
searchValue = param.search
action {
a("/track/new") {
button(classes = "form-btn btn-primary") {
+"Add track"
}
}
}
header {
th {
+"Name"
}
th {
+"Color"
}
th(classes = "action") {
+"Action"
}
}
for (u in list) {
if (Search.match(param.search, u.name)) {
entry {
attributes["data-search"] = Search.pack(u.name)
td {
+u.name
}
td {
+u.color.toString()
}
td(classes = "action") {
a("/track/${u.id}") {
i("material-icons") { +"edit" }
}
}
}
}
}
}
}
}
}
}
get<LocationTrack.Edit> { trackId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editTrack = Track.get(trackId.id)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content {
h1 { +"Edit track" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "name"
+"Name"
}
input(
name = "name",
classes = "form-control"
) {
id = "name"
placeholder = "Name"
value = editTrack.name
}
}
div("form-group") {
colorPicker(editTrack.color)
}
div("form-group") {
a("/track") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Save"
}
}
}
a("/track/${editTrack.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Delete"
}
}
}
}
}
}
post<LocationTrack.Edit> { trackId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editTrack = Track.get(trackId.id)
params["name"]?.let { editTrack.name = it }
editTrack.color = (params["color"] ?: return@post).let { c ->
Color.default.find { it.first == c }
}?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse)
editTrack.save()
call.respondRedirect("/track")
}
}
get<LocationTrack.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.WORK_GROUP
}
content {
h1 { +"Create track" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "name"
+"Name"
}
input(
name = "name",
classes = "form-control"
) {
id = "name"
placeholder = "Name"
value = ""
}
}
div("form-group") {
colorPicker(null)
}
div("form-group") {
a("/track") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Create"
}
}
}
}
}
}
}
post<LocationTrack.New> {
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val name = params["name"] ?: return@post
val color = (params["color"] ?: return@post).let { c ->
Color.default.find { it.first == c }
}?.second ?: (params["color-input"] ?: return@post).let(Color.Companion::parse)
Track(
name = name,
color = color
).save()
call.respondRedirect("/track")
}
}
get<LocationTrack.Delete> { trackId ->
val user = call.sessions.get<PortalSession>()?.getUser(call)
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteTrack = Track.get(trackId.id)
deleteTrack.delete()
call.respondRedirect("/track")
}
}
}

View file

@ -3,7 +3,9 @@ package de.kif.backend.route
import de.kif.backend.LocationLogin
import de.kif.backend.LocationWorkGroup
import de.kif.backend.PortalSession
import de.kif.backend.database.Language
import de.kif.backend.model.Permission
import de.kif.backend.model.Track
import de.kif.backend.model.WorkGroup
import de.kif.backend.util.Search
import de.kif.backend.view.MainTemplate
@ -41,6 +43,11 @@ fun Route.workGroup() {
searchValue = param.search
action {
a("/track") {
button(classes = "form-btn") {
+"Edit tracks"
}
}
a("/workgroup/new") {
button(classes = "form-btn btn-primary") {
+"Add work group"
@ -67,6 +74,9 @@ fun Route.workGroup() {
th {
+"Length"
}
th {
+"Language"
}
th(classes = "action") {
+"Action"
}
@ -83,7 +93,7 @@ fun Route.workGroup() {
+u.interested.toString()
}
td {
+u.trackId.toString()
+(u.track?.name ?: "")
}
td {
+u.projector.toString()
@ -94,9 +104,12 @@ fun Route.workGroup() {
td {
+u.length.toString()
}
td {
+u.language.toString()
}
td(classes = "action") {
a("/workgroup/${u.id}") {
i("material-icons") { +"edit" }
i("material-icons") { +"edit" }
}
}
}
@ -113,7 +126,8 @@ fun Route.workGroup() {
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get
val editWorkGroup = WorkGroup.get(workGroupId.id)
val tracks = Track.list()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -158,16 +172,29 @@ fun Route.workGroup() {
htmlFor = "track"
+"Track"
}
input(
name = "track",
classes = "form-control",
type = InputType.number
) {
id = "track"
value = editWorkGroup.trackId.toString()
div("input-group") {
select(
classes = "form-control"
) {
name = "track"
min = "0"
max = "1337"
option {
selected = (editWorkGroup.trackId ?: -1) < 0
value = "-1"
+"None"
}
for (track in tracks) {
option {
selected = editWorkGroup.trackId == track.id
value = track.id.toString()
+track.name
}
}
}
a("/track", classes = "form-btn") {
i("material-icons") { +"edit" }
}
}
}
div("form-group") {
@ -185,6 +212,26 @@ fun Route.workGroup() {
min = "0"
max = "1440"
step = "5"
}
}
div("form-group") {
label {
htmlFor = "language"
+"Language"
}
select(
classes = "form-control"
) {
name = "language"
for (language in Language.values()) {
option {
selected = editWorkGroup.language == language
value = language.name
+language.toString()
}
}
}
}
@ -248,7 +295,7 @@ fun Route.workGroup() {
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@post
val editWorkGroup = WorkGroup.get(workGroupId.id)
params["name"]?.let { editWorkGroup.name = it }
params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it }
@ -256,6 +303,7 @@ fun Route.workGroup() {
params["projector"]?.let { editWorkGroup.projector = it == "on" }
params["resolution"]?.let { editWorkGroup.resolution = it == "on" }
params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it }
params["language"]?.let { editWorkGroup.language = Language.values().find { l -> l.name == it } ?: Language.GERMAN }
editWorkGroup.save()
@ -268,6 +316,7 @@ fun Route.workGroup() {
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val tracks = Track.list()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -312,16 +361,22 @@ fun Route.workGroup() {
htmlFor = "track"
+"Track"
}
input(
name = "track",
classes = "form-control",
type = InputType.number
select(
classes = "form-control"
) {
id = "track"
value = ""
name = "track"
min = "0"
max = "1337"
option {
selected = true
value = "-1"
+"None"
}
for (track in tracks) {
option {
value = track.id.toString()
+track.name
}
}
}
}
div("form-group") {
@ -339,6 +394,26 @@ fun Route.workGroup() {
min = "0"
max = "1440"
step = "5"
}
}
div("form-group") {
label {
htmlFor = "language"
+"Language"
}
select(
classes = "form-control"
) {
name = "language"
for (language in Language.values()) {
option {
selected = language == Language.GERMAN
value = language.name
+language.toString()
}
}
}
}
@ -404,6 +479,7 @@ fun Route.workGroup() {
val projector = params["projector"] == "on"
val resolution = params["resolution"] == "on"
val length = (params["length"] ?: return@post).toIntOrNull() ?: 0
val language = (params["language"] ?: return@post).let { Language.values().find { l -> l.name == it } ?: Language.GERMAN }
WorkGroup(
name = name,
@ -411,7 +487,8 @@ fun Route.workGroup() {
trackId = trackId,
projector = projector,
resolution = resolution,
length = length
length = length,
language = language
).save()
call.respondRedirect("/workgroup")
@ -423,7 +500,7 @@ fun Route.workGroup() {
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
} else {
val deleteWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get
val deleteWorkGroup = WorkGroup.get(workGroupId.id)
deleteWorkGroup.delete()

View file

@ -1,5 +1,6 @@
package de.kif.backend.view
import de.kif.backend.Resources
import io.ktor.html.Placeholder
import io.ktor.html.Template
import io.ktor.html.TemplatePlaceholder
@ -24,12 +25,12 @@ class MainTemplate : Template<HTML> {
script(src = "/static/require.min.js") {}
/*script {
script {
unsafe {
+"require.config({baseUrl: '/static'});\n"
+("require([${Resources.jsModules}]);\n")
}
}*/
}
}
body {
insert(MenuTemplate(), menuTemplate)