Add calendar live editing
This commit is contained in:
parent
8063e15421
commit
4e5dc610a3
35 changed files with 2239 additions and 111 deletions
14
build.gradle
14
build.gradle
|
@ -11,8 +11,8 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'kotlin-multiplatform' version '1.3.20'
|
id 'kotlin-multiplatform' version '1.3.31'
|
||||||
id 'kotlinx-serialization' version '1.3.20'
|
id 'kotlinx-serialization' version '1.3.31'
|
||||||
id "org.kravemir.gradle.sass" version "1.2.2"
|
id "org.kravemir.gradle.sass" version "1.2.2"
|
||||||
id "com.github.johnrengelman.shadow" version "4.0.4"
|
id "com.github.johnrengelman.shadow" version "4.0.4"
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,11 @@ repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
maven { url "http://dl.bintray.com/kotlin/ktor" }
|
maven { url "http://dl.bintray.com/kotlin/ktor" }
|
||||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||||
|
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
def ktor_version = '1.1.2'
|
def ktor_version = '1.1.5'
|
||||||
def serialization_version = '0.10.0'
|
def serialization_version = '0.11.0'
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm() {
|
jvm() {
|
||||||
|
@ -72,7 +73,8 @@ kotlin {
|
||||||
implementation "io.ktor:ktor-server-netty:$ktor_version"
|
implementation "io.ktor:ktor-server-netty:$ktor_version"
|
||||||
implementation "io.ktor:ktor-auth:$ktor_version"
|
implementation "io.ktor:ktor-auth:$ktor_version"
|
||||||
implementation "io.ktor:ktor-locations:$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"
|
implementation "io.ktor:ktor-html-builder:$ktor_version"
|
||||||
|
|
||||||
|
@ -98,7 +100,7 @@ kotlin {
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
|
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 {
|
jsTest {
|
||||||
|
|
96
src/commonMain/kotlin/kif/common/model/Color.kt
Normal file
96
src/commonMain/kotlin/kif/common/model/Color.kt
Normal 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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
3
src/commonMain/kotlin/kif/common/model/Constants.kt
Normal file
3
src/commonMain/kotlin/kif/common/model/Constants.kt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package kif.common.model
|
||||||
|
|
||||||
|
const val CALENDAR_GRID_WIDTH = 15
|
58
src/commonMain/kotlin/kif/common/model/Message.kt
Normal file
58
src/commonMain/kotlin/kif/common/model/Message.kt
Normal 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()
|
|
@ -1,19 +1,381 @@
|
||||||
package de.kif.frontend
|
package de.kif.frontend
|
||||||
|
|
||||||
import de.kif.frontend.calendar.Calendar
|
import de.westermann.kobserve.property.mapBinding
|
||||||
import de.westermann.kwebview.components.boxView
|
import de.westermann.kwebview.*
|
||||||
import de.westermann.kwebview.components.h1
|
import de.westermann.kwebview.components.Body
|
||||||
import de.westermann.kwebview.components.init
|
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 {
|
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
|
||||||
clear()
|
|
||||||
h1("Test")
|
init {
|
||||||
boxView {
|
var linkM10: HTMLAnchorElement? = null
|
||||||
style {
|
var linkM5: HTMLAnchorElement? = null
|
||||||
width = "600px"
|
var linkReset: HTMLAnchorElement? = null
|
||||||
height = "400px"
|
var linkP5: HTMLAnchorElement? = null
|
||||||
margin = "10px"
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package de.westermann.kwebview
|
package de.westermann.kwebview
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.basic.property
|
import de.westermann.kobserve.property.property
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package de.westermann.kwebview
|
package de.westermann.kwebview
|
||||||
|
|
||||||
import de.westermann.kobserve.ListenerReference
|
import de.westermann.kobserve.event.EventListener
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
import de.westermann.kobserve.ReadOnlyProperty
|
||||||
import org.w3c.dom.DOMTokenList
|
import org.w3c.dom.DOMTokenList
|
||||||
|
@ -102,7 +102,7 @@ class ClassList(
|
||||||
throw IllegalArgumentException("Class is not bound!")
|
throw IllegalArgumentException("Class is not bound!")
|
||||||
}
|
}
|
||||||
|
|
||||||
bound[clazz]?.reference?.remove()
|
bound[clazz]?.reference?.detach()
|
||||||
bound -= clazz
|
bound -= clazz
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +114,6 @@ class ClassList(
|
||||||
|
|
||||||
private data class Bound(
|
private data class Bound(
|
||||||
val property: ReadOnlyProperty<Boolean>,
|
val property: ReadOnlyProperty<Boolean>,
|
||||||
val reference: ListenerReference<Unit>?
|
val reference: EventListener<Unit>?
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,11 +1,16 @@
|
||||||
package de.westermann.kwebview
|
package de.westermann.kwebview
|
||||||
|
|
||||||
import de.westermann.kobserve.ListenerReference
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
import de.westermann.kobserve.ReadOnlyProperty
|
||||||
|
import de.westermann.kobserve.event.EventListener
|
||||||
import org.w3c.dom.DOMStringMap
|
import org.w3c.dom.DOMStringMap
|
||||||
import org.w3c.dom.get
|
import org.w3c.dom.get
|
||||||
import org.w3c.dom.set
|
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.
|
* Represents the css classes of an html element.
|
||||||
|
@ -13,7 +18,7 @@ import org.w3c.dom.set
|
||||||
* @author lars
|
* @author lars
|
||||||
*/
|
*/
|
||||||
class DataSet(
|
class DataSet(
|
||||||
private val map: DOMStringMap
|
private val map: DOMStringMap
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val bound: MutableMap<String, Bound> = mutableMapOf()
|
private val bound: MutableMap<String, Bound> = mutableMapOf()
|
||||||
|
@ -49,11 +54,11 @@ class DataSet(
|
||||||
* Set css class present.
|
* Set css class present.
|
||||||
*/
|
*/
|
||||||
operator fun set(key: String, value: String?) =
|
operator fun set(key: String, value: String?) =
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
this -= key
|
this -= key
|
||||||
} else {
|
} else {
|
||||||
this += key to value
|
this += key to value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(key: String, property: ReadOnlyProperty<String>) {
|
fun bind(key: String, property: ReadOnlyProperty<String>) {
|
||||||
if (key in bound) {
|
if (key in bound) {
|
||||||
|
@ -71,22 +76,34 @@ class DataSet(
|
||||||
bound[key] = Bound(key, property, null)
|
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) {
|
fun unbind(key: String) {
|
||||||
if (key !in bound) {
|
if (key !in bound) {
|
||||||
throw IllegalArgumentException("Class is not bound!")
|
throw IllegalArgumentException("Class is not bound!")
|
||||||
}
|
}
|
||||||
|
|
||||||
bound[key]?.reference?.remove()
|
bound[key]?.reference?.detach()
|
||||||
bound -= key
|
bound -= key
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class Bound(
|
private inner class Bound(
|
||||||
val key: String,
|
val key: String,
|
||||||
val propertyNullable: ReadOnlyProperty<String?>?,
|
val propertyNullable: ReadOnlyProperty<String?>?,
|
||||||
val property: ReadOnlyProperty<String>?
|
val property: ReadOnlyProperty<String>?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var reference: ListenerReference<Unit>? = null
|
var reference: EventListener<Unit>? = null
|
||||||
|
|
||||||
fun set(value: String?) {
|
fun set(value: String?) {
|
||||||
if (propertyNullable != null && propertyNullable is Property) {
|
if (propertyNullable != null && propertyNullable is Property) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package de.westermann.kwebview
|
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.HTMLElement
|
||||||
import org.w3c.dom.css.CSSStyleDeclaration
|
import org.w3c.dom.css.CSSStyleDeclaration
|
||||||
import org.w3c.dom.events.FocusEvent
|
import org.w3c.dom.events.FocusEvent
|
||||||
|
@ -63,6 +64,9 @@ abstract class View(view: HTMLElement = createHtmlView()) {
|
||||||
val dimension: Dimension
|
val dimension: Dimension
|
||||||
get() = html.getBoundingClientRect().toDimension()
|
get() = html.getBoundingClientRect().toDimension()
|
||||||
|
|
||||||
|
val point: Point
|
||||||
|
get() = dimension.position
|
||||||
|
|
||||||
var title by AttributeDelegate()
|
var title by AttributeDelegate()
|
||||||
|
|
||||||
val style = view.style
|
val style = view.style
|
||||||
|
@ -101,6 +105,15 @@ abstract class View(view: HTMLElement = createHtmlView()) {
|
||||||
val onFocus = EventHandler<FocusEvent>()
|
val onFocus = EventHandler<FocusEvent>()
|
||||||
val onBlur = 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 {
|
init {
|
||||||
onClick.bind(view, "click")
|
onClick.bind(view, "click")
|
||||||
onDblClick.bind(view, "dblclick")
|
onDblClick.bind(view, "dblclick")
|
||||||
|
@ -120,5 +133,17 @@ abstract class View(view: HTMLElement = createHtmlView()) {
|
||||||
|
|
||||||
onFocus.bind(view, "focus")
|
onFocus.bind(view, "focus")
|
||||||
onBlur.bind(view, "blur")
|
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) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,4 +66,8 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
|
||||||
operator fun V.unaryPlus() {
|
operator fun V.unaryPlus() {
|
||||||
append(this)
|
append(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <V : View> wrap(htmlElement: HTMLElement) = object : ViewCollection<V>(htmlElement) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
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.KWebViewDsl
|
||||||
import de.westermann.kwebview.View
|
import de.westermann.kwebview.View
|
||||||
import de.westermann.kwebview.ViewCollection
|
import de.westermann.kwebview.ViewCollection
|
||||||
|
|
|
@ -3,7 +3,7 @@ package de.westermann.kwebview.components
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
import de.westermann.kobserve.ReadOnlyProperty
|
||||||
import de.westermann.kobserve.ValidationProperty
|
import de.westermann.kobserve.ValidationProperty
|
||||||
import de.westermann.kobserve.basic.property
|
import de.westermann.kobserve.property.property
|
||||||
import de.westermann.kwebview.*
|
import de.westermann.kwebview.*
|
||||||
import org.w3c.dom.events.Event
|
import org.w3c.dom.events.Event
|
||||||
import org.w3c.dom.events.EventListener
|
import org.w3c.dom.events.EventListener
|
||||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
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.KWebViewDsl
|
||||||
import de.westermann.kwebview.View
|
import de.westermann.kwebview.View
|
||||||
import de.westermann.kwebview.ViewCollection
|
import de.westermann.kwebview.ViewCollection
|
||||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
import de.westermann.kobserve.ReadOnlyProperty
|
||||||
import de.westermann.kobserve.basic.property
|
import de.westermann.kobserve.property.property
|
||||||
import de.westermann.kwebview.*
|
import de.westermann.kwebview.*
|
||||||
import org.w3c.dom.HTMLImageElement
|
import org.w3c.dom.HTMLImageElement
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package de.westermann.kwebview.components
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
import de.westermann.kobserve.ReadOnlyProperty
|
||||||
import de.westermann.kobserve.ValidationProperty
|
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.kobserve.not
|
||||||
import de.westermann.kwebview.*
|
import de.westermann.kwebview.*
|
||||||
import org.w3c.dom.HTMLInputElement
|
import org.w3c.dom.HTMLInputElement
|
||||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
import de.westermann.kobserve.ReadOnlyProperty
|
||||||
import de.westermann.kobserve.basic.property
|
import de.westermann.kobserve.property.property
|
||||||
import de.westermann.kwebview.*
|
import de.westermann.kwebview.*
|
||||||
import org.w3c.dom.HTMLLabelElement
|
import org.w3c.dom.HTMLLabelElement
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
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.AttributeDelegate
|
||||||
import de.westermann.kwebview.KWebViewDsl
|
import de.westermann.kwebview.KWebViewDsl
|
||||||
import de.westermann.kwebview.ViewCollection
|
import de.westermann.kwebview.ViewCollection
|
||||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
||||||
|
|
||||||
import de.westermann.kobserve.Property
|
import de.westermann.kobserve.Property
|
||||||
import de.westermann.kobserve.ReadOnlyProperty
|
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.KWebViewDsl
|
||||||
import de.westermann.kwebview.View
|
import de.westermann.kwebview.View
|
||||||
import de.westermann.kwebview.ViewCollection
|
import de.westermann.kwebview.ViewCollection
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package de.westermann.kwebview
|
package de.westermann.kwebview
|
||||||
|
|
||||||
import de.westermann.kobserve.EventHandler
|
import de.westermann.kobserve.event.EventHandler
|
||||||
import org.w3c.dom.DOMRect
|
import org.w3c.dom.DOMRect
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
import org.w3c.dom.events.Event
|
import org.w3c.dom.events.Event
|
||||||
import org.w3c.dom.events.EventListener
|
import org.w3c.dom.events.EventListener
|
||||||
import org.w3c.dom.events.MouseEvent
|
import org.w3c.dom.events.MouseEvent
|
||||||
|
import org.w3c.xhr.FormData
|
||||||
|
import org.w3c.xhr.XMLHttpRequest
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
import kotlin.browser.window
|
import kotlin.browser.window
|
||||||
|
|
||||||
|
@ -15,9 +17,8 @@ inline fun <reified V : HTMLElement> createHtmlView(tag: String? = null): V {
|
||||||
tagName = tag
|
tagName = tag
|
||||||
} else {
|
} else {
|
||||||
tagName = V::class.js.name.toLowerCase().replace("html([a-z]*)element".toRegex(), "$1")
|
tagName = V::class.js.name.toLowerCase().replace("html([a-z]*)element".toRegex(), "$1")
|
||||||
if (tagName.isBlank()) {
|
if (tagName.isBlank()) tagName = "div"
|
||||||
tagName = "div"
|
if (tagName == "anchor") tagName = "a"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return document.createElement(tagName) as V
|
return document.createElement(tagName) as V
|
||||||
}
|
}
|
||||||
|
@ -77,3 +78,66 @@ fun interval(timeout: Int, block: () -> Unit): Int {
|
||||||
fun clearInterval(id: Int) {
|
fun clearInterval(id: Int) {
|
||||||
window.clearInterval(id)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ a {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
z-index: 6;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -157,13 +158,24 @@ a {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: $background-secondary-color;
|
background-color: $background-secondary-color;
|
||||||
z-index: 1;
|
z-index: 5;
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
top: -8rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 8rem;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
bottom: 0.2rem;
|
bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
|
@ -192,6 +204,7 @@ a {
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
bottom: 1.8em
|
bottom: 1.8em
|
||||||
}
|
}
|
||||||
|
@ -215,7 +228,7 @@ a {
|
||||||
.btn-search {
|
.btn-search {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: calc(2.5rem + 2px);
|
height: 2.5rem;
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
right: -3px;
|
right: -3px;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
|
@ -228,9 +241,6 @@ a {
|
||||||
input:focus ~ .btn-search {
|
input:focus ~ .btn-search {
|
||||||
border-color: $primary-color;
|
border-color: $primary-color;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
margin-top: 0;
|
|
||||||
height: calc(2.5rem + 4px);
|
|
||||||
margin-right: 1px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,6 +292,7 @@ a {
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $background-primary-color;
|
background-color: $background-primary-color;
|
||||||
border-radius: 0.2rem;
|
border-radius: 0.2rem;
|
||||||
|
@ -291,10 +302,14 @@ a {
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: $primary-color;
|
border-color: $primary-color;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select:-moz-focusring {
|
||||||
|
color: transparent;
|
||||||
|
text-shadow: 0 0 0 $text-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -411,7 +426,7 @@ a {
|
||||||
border-color: $error-color;
|
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;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,3 +440,358 @@ form {
|
||||||
width: 24rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,9 @@ import io.ktor.response.respondRedirect
|
||||||
import io.ktor.routing.routing
|
import io.ktor.routing.routing
|
||||||
import io.ktor.sessions.*
|
import io.ktor.sessions.*
|
||||||
import io.ktor.util.hex
|
import io.ktor.util.hex
|
||||||
|
import io.ktor.websocket.WebSockets
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
data class PortalSession(val id: Int, val name: String) {
|
data class PortalSession(val id: Int, val name: String) {
|
||||||
suspend fun getUser(call: ApplicationCall): User {
|
suspend fun getUser(call: ApplicationCall): User {
|
||||||
|
@ -37,8 +39,39 @@ data class PortalSession(val id: Int, val name: String) {
|
||||||
@Location("/")
|
@Location("/")
|
||||||
class LocationDashboard()
|
class LocationDashboard()
|
||||||
|
|
||||||
@Location("/calendar")
|
@Location("/calendar/{day}")
|
||||||
class LocationCalendar()
|
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")
|
@Location("/login")
|
||||||
data class LocationLogin(val username: String = "", val password: String = "", val next: String = "/")
|
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)
|
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")
|
@Location("/room")
|
||||||
data class LocationRoom(val search: String = "") {
|
data class LocationRoom(val search: String = "") {
|
||||||
@Location("/{id}")
|
@Location("/{id}")
|
||||||
|
@ -106,6 +151,7 @@ fun Application.main() {
|
||||||
install(Compression)
|
install(Compression)
|
||||||
install(DataConversion)
|
install(DataConversion)
|
||||||
install(Locations)
|
install(Locations)
|
||||||
|
install(WebSockets)
|
||||||
|
|
||||||
install(Authentication) {
|
install(Authentication) {
|
||||||
form {
|
form {
|
||||||
|
@ -145,9 +191,12 @@ fun Application.main() {
|
||||||
account()
|
account()
|
||||||
|
|
||||||
workGroup()
|
workGroup()
|
||||||
|
track()
|
||||||
room()
|
room()
|
||||||
person()
|
person()
|
||||||
user()
|
user()
|
||||||
|
|
||||||
|
pushService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ object Connection {
|
||||||
DbPerson, DbPersonConstraint,
|
DbPerson, DbPersonConstraint,
|
||||||
DbTrack, DbWorkGroup, DbWorkGroupConstraint,
|
DbTrack, DbWorkGroup, DbWorkGroupConstraint,
|
||||||
DbLeader, DbWorkGroupOrder,
|
DbLeader, DbWorkGroupOrder,
|
||||||
DbRoom, DbTimeSlot, DbSchedule,
|
DbRoom, DbSchedule,
|
||||||
DbUser, DbUserPermission
|
DbUser, DbUserPermission
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ object DbPersonConstraint : Table() {
|
||||||
object DbTrack : Table() {
|
object DbTrack : Table() {
|
||||||
val id = integer("id").autoIncrement().primaryKey()
|
val id = integer("id").autoIncrement().primaryKey()
|
||||||
val name = varchar("name", 64)
|
val name = varchar("name", 64)
|
||||||
|
val color = varchar("color", 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbWorkGroup : Table() {
|
object DbWorkGroup : Table() {
|
||||||
|
@ -35,6 +36,7 @@ object DbWorkGroup : Table() {
|
||||||
val trackId = integer("track_id").nullable()
|
val trackId = integer("track_id").nullable()
|
||||||
val projector = bool("projector")
|
val projector = bool("projector")
|
||||||
val resolution = bool("resolution")
|
val resolution = bool("resolution")
|
||||||
|
val language = enumeration("language", Language::class)
|
||||||
|
|
||||||
val length = integer("length")
|
val length = integer("length")
|
||||||
|
|
||||||
|
@ -70,18 +72,11 @@ object DbRoom : Table() {
|
||||||
val projector = bool("projector")
|
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() {
|
object DbSchedule : Table() {
|
||||||
val workGroupId = integer("work_group_id").primaryKey(0)
|
val workGroupId = integer("work_group_id").primaryKey(0)
|
||||||
val timeSlotId = integer("time_slot_id").primaryKey(1)
|
val day = integer("day").primaryKey(1)
|
||||||
val roomId = integer("room_id").primaryKey(2)
|
val time = integer("time_slot").primaryKey(2)
|
||||||
|
val roomId = integer("room_id").primaryKey(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class DbConstraintType {
|
enum class DbConstraintType {
|
||||||
|
@ -98,3 +93,10 @@ object DbUserPermission : Table() {
|
||||||
val userId = integer("id").primaryKey(0)
|
val userId = integer("id").primaryKey(0)
|
||||||
val permission = enumeration("permission", Permission::class).primaryKey(1)
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
package de.kif.backend.model
|
|
||||||
|
|
||||||
class Day {
|
|
||||||
}
|
|
|
@ -34,15 +34,34 @@ class Room(
|
||||||
suspend fun delete() {
|
suspend fun delete() {
|
||||||
val id = id
|
val id = id
|
||||||
if (id >= 0) {
|
if (id >= 0) {
|
||||||
|
for (it in Schedule.getByRoom(id)) {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbRoom.deleteWhere { DbRoom.id eq id }
|
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 {
|
companion object {
|
||||||
suspend fun get(roomId: Int): Room? = dbQuery {
|
suspend fun get(roomId: Int): Room = dbQuery {
|
||||||
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: return@dbQuery null
|
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: throw IllegalArgumentException()
|
||||||
Room(
|
Room(
|
||||||
result[DbRoom.id],
|
result[DbRoom.id],
|
||||||
result[DbRoom.name],
|
result[DbRoom.name],
|
||||||
|
|
99
src/jvmMain/kotlin/de/kif/backend/model/Schedule.kt
Normal file
99
src/jvmMain/kotlin/de/kif/backend/model/Schedule.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
package de.kif.backend.model
|
|
||||||
|
|
||||||
class TimeSlot {
|
|
||||||
}
|
|
63
src/jvmMain/kotlin/de/kif/backend/model/Track.kt
Normal file
63
src/jvmMain/kotlin/de/kif/backend/model/Track.kt
Normal 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])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,9 @@ package de.kif.backend.model
|
||||||
|
|
||||||
import de.kif.backend.database.DbWorkGroup
|
import de.kif.backend.database.DbWorkGroup
|
||||||
import de.kif.backend.database.DbWorkGroupConstraint
|
import de.kif.backend.database.DbWorkGroupConstraint
|
||||||
|
import de.kif.backend.database.Language
|
||||||
import de.kif.backend.database.dbQuery
|
import de.kif.backend.database.dbQuery
|
||||||
|
import io.ktor.features.NotFoundException
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
|
||||||
class WorkGroup(
|
class WorkGroup(
|
||||||
|
@ -13,10 +15,12 @@ class WorkGroup(
|
||||||
var projector: Boolean = false,
|
var projector: Boolean = false,
|
||||||
var resolution: Boolean = false,
|
var resolution: Boolean = false,
|
||||||
var length: Int = 0,
|
var length: Int = 0,
|
||||||
|
var language: Language = Language.GERMAN,
|
||||||
var start: Long? = null,
|
var start: Long? = null,
|
||||||
var end: Long? = null
|
var end: Long? = null
|
||||||
) {
|
) {
|
||||||
var constraints: Set<WorkGroupConstraint> = emptySet()
|
var constraints: Set<WorkGroupConstraint> = emptySet()
|
||||||
|
var track: Track? = null
|
||||||
|
|
||||||
suspend fun save() {
|
suspend fun save() {
|
||||||
if (id < 0) {
|
if (id < 0) {
|
||||||
|
@ -28,6 +32,7 @@ class WorkGroup(
|
||||||
it[projector] = this@WorkGroup.projector
|
it[projector] = this@WorkGroup.projector
|
||||||
it[resolution] = this@WorkGroup.resolution
|
it[resolution] = this@WorkGroup.resolution
|
||||||
it[length] = this@WorkGroup.length
|
it[length] = this@WorkGroup.length
|
||||||
|
it[language] = this@WorkGroup.language
|
||||||
it[start] = this@WorkGroup.start
|
it[start] = this@WorkGroup.start
|
||||||
it[end] = this@WorkGroup.end
|
it[end] = this@WorkGroup.end
|
||||||
}[DbWorkGroup.id]!!
|
}[DbWorkGroup.id]!!
|
||||||
|
@ -45,6 +50,7 @@ class WorkGroup(
|
||||||
it[projector] = this@WorkGroup.projector
|
it[projector] = this@WorkGroup.projector
|
||||||
it[resolution] = this@WorkGroup.resolution
|
it[resolution] = this@WorkGroup.resolution
|
||||||
it[length] = this@WorkGroup.length
|
it[length] = this@WorkGroup.length
|
||||||
|
it[language] = this@WorkGroup.language
|
||||||
it[start] = this@WorkGroup.start
|
it[start] = this@WorkGroup.start
|
||||||
it[end] = this@WorkGroup.end
|
it[end] = this@WorkGroup.end
|
||||||
}
|
}
|
||||||
|
@ -60,6 +66,9 @@ class WorkGroup(
|
||||||
suspend fun delete() {
|
suspend fun delete() {
|
||||||
val id = id
|
val id = id
|
||||||
if (id >= 0) {
|
if (id >= 0) {
|
||||||
|
for (it in Schedule.getByWorkGroup(id)) {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id }
|
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id }
|
||||||
DbWorkGroup.deleteWhere { DbWorkGroup.id eq id }
|
DbWorkGroup.deleteWhere { DbWorkGroup.id eq id }
|
||||||
|
@ -70,12 +79,13 @@ class WorkGroup(
|
||||||
suspend fun loadConstraints() {
|
suspend fun loadConstraints() {
|
||||||
if (id >= 0) {
|
if (id >= 0) {
|
||||||
constraints = WorkGroupConstraint.get(id)
|
constraints = WorkGroupConstraint.get(id)
|
||||||
|
track = trackId?.let { if (it < 0) null else Track.get(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun get(workGroupId: Int): WorkGroup? = dbQuery {
|
suspend fun get(workGroupId: Int): WorkGroup = dbQuery {
|
||||||
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: return@dbQuery null
|
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: throw IllegalArgumentException()
|
||||||
WorkGroup(
|
WorkGroup(
|
||||||
result[DbWorkGroup.id],
|
result[DbWorkGroup.id],
|
||||||
result[DbWorkGroup.name],
|
result[DbWorkGroup.name],
|
||||||
|
@ -84,10 +94,11 @@ class WorkGroup(
|
||||||
result[DbWorkGroup.projector],
|
result[DbWorkGroup.projector],
|
||||||
result[DbWorkGroup.resolution],
|
result[DbWorkGroup.resolution],
|
||||||
result[DbWorkGroup.length],
|
result[DbWorkGroup.length],
|
||||||
|
result[DbWorkGroup.language],
|
||||||
result[DbWorkGroup.start],
|
result[DbWorkGroup.start],
|
||||||
result[DbWorkGroup.end]
|
result[DbWorkGroup.end]
|
||||||
)
|
)
|
||||||
}?.apply { loadConstraints() }
|
}.apply { loadConstraints() }
|
||||||
|
|
||||||
suspend fun list(): List<WorkGroup> = dbQuery {
|
suspend fun list(): List<WorkGroup> = dbQuery {
|
||||||
val query = DbWorkGroup.selectAll()
|
val query = DbWorkGroup.selectAll()
|
||||||
|
@ -100,6 +111,7 @@ class WorkGroup(
|
||||||
result[DbWorkGroup.projector],
|
result[DbWorkGroup.projector],
|
||||||
result[DbWorkGroup.resolution],
|
result[DbWorkGroup.resolution],
|
||||||
result[DbWorkGroup.length],
|
result[DbWorkGroup.length],
|
||||||
|
result[DbWorkGroup.language],
|
||||||
result[DbWorkGroup.start],
|
result[DbWorkGroup.start],
|
||||||
result[DbWorkGroup.end]
|
result[DbWorkGroup.end]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,28 +1,490 @@
|
||||||
package de.kif.backend.route
|
package de.kif.backend.route
|
||||||
|
|
||||||
import de.kif.backend.LocationCalendar
|
import de.kif.backend.LocationCalendar
|
||||||
|
import de.kif.backend.LocationLogin
|
||||||
import de.kif.backend.PortalSession
|
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.MainTemplate
|
||||||
import de.kif.backend.view.MenuTemplate
|
import de.kif.backend.view.MenuTemplate
|
||||||
|
import de.kif.backend.view.TableTemplate
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
|
import io.ktor.html.insert
|
||||||
import io.ktor.html.respondHtmlTemplate
|
import io.ktor.html.respondHtmlTemplate
|
||||||
import io.ktor.locations.get
|
import io.ktor.locations.get
|
||||||
|
import io.ktor.request.path
|
||||||
|
import io.ktor.response.respondRedirect
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.get
|
||||||
import io.ktor.sessions.get
|
import io.ktor.sessions.get
|
||||||
import io.ktor.sessions.sessions
|
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() {
|
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 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()) {
|
call.respondHtmlTemplate(MainTemplate()) {
|
||||||
menuTemplate {
|
menuTemplate {
|
||||||
this.user = user
|
this.user = user
|
||||||
active = MenuTemplate.Tab.CALENDAR
|
active = MenuTemplate.Tab.CALENDAR
|
||||||
}
|
}
|
||||||
content {
|
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
|
||||||
}
|
}
|
||||||
|
|
47
src/jvmMain/kotlin/de/kif/backend/route/PushService.kt
Normal file
47
src/jvmMain/kotlin/de/kif/backend/route/PushService.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,7 +95,7 @@ fun Route.room() {
|
||||||
if (user == null || !user.checkPermission(Permission.ROOM)) {
|
if (user == null || !user.checkPermission(Permission.ROOM)) {
|
||||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||||
} else {
|
} else {
|
||||||
val editRoom = Room.get(roomId.id) ?: return@get
|
val editRoom = Room.get(roomId.id)
|
||||||
call.respondHtmlTemplate(MainTemplate()) {
|
call.respondHtmlTemplate(MainTemplate()) {
|
||||||
menuTemplate {
|
menuTemplate {
|
||||||
this.user = user
|
this.user = user
|
||||||
|
@ -183,7 +183,7 @@ fun Route.room() {
|
||||||
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
||||||
list.firstOrNull()
|
list.firstOrNull()
|
||||||
}
|
}
|
||||||
val editRoom = Room.get(roomId.id) ?: return@post
|
val editRoom = Room.get(roomId.id)
|
||||||
|
|
||||||
params["name"]?.let { editRoom.name = it }
|
params["name"]?.let { editRoom.name = it }
|
||||||
params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 }
|
params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 }
|
||||||
|
@ -298,7 +298,7 @@ fun Route.room() {
|
||||||
if (user == null || !user.checkPermission(Permission.ROOM)) {
|
if (user == null || !user.checkPermission(Permission.ROOM)) {
|
||||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||||
} else {
|
} else {
|
||||||
val deleteRoom = Room.get(roomId.id) ?: return@get
|
val deleteRoom = Room.get(roomId.id)
|
||||||
|
|
||||||
deleteRoom.delete()
|
deleteRoom.delete()
|
||||||
|
|
||||||
|
|
304
src/jvmMain/kotlin/de/kif/backend/route/Track.kt
Normal file
304
src/jvmMain/kotlin/de/kif/backend/route/Track.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,9 @@ package de.kif.backend.route
|
||||||
import de.kif.backend.LocationLogin
|
import de.kif.backend.LocationLogin
|
||||||
import de.kif.backend.LocationWorkGroup
|
import de.kif.backend.LocationWorkGroup
|
||||||
import de.kif.backend.PortalSession
|
import de.kif.backend.PortalSession
|
||||||
|
import de.kif.backend.database.Language
|
||||||
import de.kif.backend.model.Permission
|
import de.kif.backend.model.Permission
|
||||||
|
import de.kif.backend.model.Track
|
||||||
import de.kif.backend.model.WorkGroup
|
import de.kif.backend.model.WorkGroup
|
||||||
import de.kif.backend.util.Search
|
import de.kif.backend.util.Search
|
||||||
import de.kif.backend.view.MainTemplate
|
import de.kif.backend.view.MainTemplate
|
||||||
|
@ -41,6 +43,11 @@ fun Route.workGroup() {
|
||||||
searchValue = param.search
|
searchValue = param.search
|
||||||
|
|
||||||
action {
|
action {
|
||||||
|
a("/track") {
|
||||||
|
button(classes = "form-btn") {
|
||||||
|
+"Edit tracks"
|
||||||
|
}
|
||||||
|
}
|
||||||
a("/workgroup/new") {
|
a("/workgroup/new") {
|
||||||
button(classes = "form-btn btn-primary") {
|
button(classes = "form-btn btn-primary") {
|
||||||
+"Add work group"
|
+"Add work group"
|
||||||
|
@ -67,6 +74,9 @@ fun Route.workGroup() {
|
||||||
th {
|
th {
|
||||||
+"Length"
|
+"Length"
|
||||||
}
|
}
|
||||||
|
th {
|
||||||
|
+"Language"
|
||||||
|
}
|
||||||
th(classes = "action") {
|
th(classes = "action") {
|
||||||
+"Action"
|
+"Action"
|
||||||
}
|
}
|
||||||
|
@ -83,7 +93,7 @@ fun Route.workGroup() {
|
||||||
+u.interested.toString()
|
+u.interested.toString()
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
+u.trackId.toString()
|
+(u.track?.name ?: "")
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
+u.projector.toString()
|
+u.projector.toString()
|
||||||
|
@ -94,9 +104,12 @@ fun Route.workGroup() {
|
||||||
td {
|
td {
|
||||||
+u.length.toString()
|
+u.length.toString()
|
||||||
}
|
}
|
||||||
|
td {
|
||||||
|
+u.language.toString()
|
||||||
|
}
|
||||||
td(classes = "action") {
|
td(classes = "action") {
|
||||||
a("/workgroup/${u.id}") {
|
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)) {
|
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
||||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||||
} else {
|
} else {
|
||||||
val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get
|
val editWorkGroup = WorkGroup.get(workGroupId.id)
|
||||||
|
val tracks = Track.list()
|
||||||
call.respondHtmlTemplate(MainTemplate()) {
|
call.respondHtmlTemplate(MainTemplate()) {
|
||||||
menuTemplate {
|
menuTemplate {
|
||||||
this.user = user
|
this.user = user
|
||||||
|
@ -158,16 +172,29 @@ fun Route.workGroup() {
|
||||||
htmlFor = "track"
|
htmlFor = "track"
|
||||||
+"Track"
|
+"Track"
|
||||||
}
|
}
|
||||||
input(
|
div("input-group") {
|
||||||
name = "track",
|
select(
|
||||||
classes = "form-control",
|
classes = "form-control"
|
||||||
type = InputType.number
|
) {
|
||||||
) {
|
name = "track"
|
||||||
id = "track"
|
|
||||||
value = editWorkGroup.trackId.toString()
|
|
||||||
|
|
||||||
min = "0"
|
option {
|
||||||
max = "1337"
|
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") {
|
div("form-group") {
|
||||||
|
@ -185,6 +212,26 @@ fun Route.workGroup() {
|
||||||
|
|
||||||
min = "0"
|
min = "0"
|
||||||
max = "1440"
|
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) ->
|
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
||||||
list.firstOrNull()
|
list.firstOrNull()
|
||||||
}
|
}
|
||||||
val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@post
|
val editWorkGroup = WorkGroup.get(workGroupId.id)
|
||||||
|
|
||||||
params["name"]?.let { editWorkGroup.name = it }
|
params["name"]?.let { editWorkGroup.name = it }
|
||||||
params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it }
|
params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it }
|
||||||
|
@ -256,6 +303,7 @@ fun Route.workGroup() {
|
||||||
params["projector"]?.let { editWorkGroup.projector = it == "on" }
|
params["projector"]?.let { editWorkGroup.projector = it == "on" }
|
||||||
params["resolution"]?.let { editWorkGroup.resolution = it == "on" }
|
params["resolution"]?.let { editWorkGroup.resolution = it == "on" }
|
||||||
params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it }
|
params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it }
|
||||||
|
params["language"]?.let { editWorkGroup.language = Language.values().find { l -> l.name == it } ?: Language.GERMAN }
|
||||||
|
|
||||||
editWorkGroup.save()
|
editWorkGroup.save()
|
||||||
|
|
||||||
|
@ -268,6 +316,7 @@ fun Route.workGroup() {
|
||||||
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
||||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||||
} else {
|
} else {
|
||||||
|
val tracks = Track.list()
|
||||||
call.respondHtmlTemplate(MainTemplate()) {
|
call.respondHtmlTemplate(MainTemplate()) {
|
||||||
menuTemplate {
|
menuTemplate {
|
||||||
this.user = user
|
this.user = user
|
||||||
|
@ -312,16 +361,22 @@ fun Route.workGroup() {
|
||||||
htmlFor = "track"
|
htmlFor = "track"
|
||||||
+"Track"
|
+"Track"
|
||||||
}
|
}
|
||||||
input(
|
select(
|
||||||
name = "track",
|
classes = "form-control"
|
||||||
classes = "form-control",
|
|
||||||
type = InputType.number
|
|
||||||
) {
|
) {
|
||||||
id = "track"
|
name = "track"
|
||||||
value = ""
|
|
||||||
|
|
||||||
min = "0"
|
option {
|
||||||
max = "1337"
|
selected = true
|
||||||
|
value = "-1"
|
||||||
|
+"None"
|
||||||
|
}
|
||||||
|
for (track in tracks) {
|
||||||
|
option {
|
||||||
|
value = track.id.toString()
|
||||||
|
+track.name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
|
@ -339,6 +394,26 @@ fun Route.workGroup() {
|
||||||
|
|
||||||
min = "0"
|
min = "0"
|
||||||
max = "1440"
|
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 projector = params["projector"] == "on"
|
||||||
val resolution = params["resolution"] == "on"
|
val resolution = params["resolution"] == "on"
|
||||||
val length = (params["length"] ?: return@post).toIntOrNull() ?: 0
|
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(
|
WorkGroup(
|
||||||
name = name,
|
name = name,
|
||||||
|
@ -411,7 +487,8 @@ fun Route.workGroup() {
|
||||||
trackId = trackId,
|
trackId = trackId,
|
||||||
projector = projector,
|
projector = projector,
|
||||||
resolution = resolution,
|
resolution = resolution,
|
||||||
length = length
|
length = length,
|
||||||
|
language = language
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
call.respondRedirect("/workgroup")
|
call.respondRedirect("/workgroup")
|
||||||
|
@ -423,7 +500,7 @@ fun Route.workGroup() {
|
||||||
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
||||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||||
} else {
|
} else {
|
||||||
val deleteWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get
|
val deleteWorkGroup = WorkGroup.get(workGroupId.id)
|
||||||
|
|
||||||
deleteWorkGroup.delete()
|
deleteWorkGroup.delete()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package de.kif.backend.view
|
package de.kif.backend.view
|
||||||
|
|
||||||
|
import de.kif.backend.Resources
|
||||||
import io.ktor.html.Placeholder
|
import io.ktor.html.Placeholder
|
||||||
import io.ktor.html.Template
|
import io.ktor.html.Template
|
||||||
import io.ktor.html.TemplatePlaceholder
|
import io.ktor.html.TemplatePlaceholder
|
||||||
|
@ -24,12 +25,12 @@ class MainTemplate : Template<HTML> {
|
||||||
|
|
||||||
script(src = "/static/require.min.js") {}
|
script(src = "/static/require.min.js") {}
|
||||||
|
|
||||||
/*script {
|
script {
|
||||||
unsafe {
|
unsafe {
|
||||||
+"require.config({baseUrl: '/static'});\n"
|
+"require.config({baseUrl: '/static'});\n"
|
||||||
+("require([${Resources.jsModules}]);\n")
|
+("require([${Resources.jsModules}]);\n")
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
insert(MenuTemplate(), menuTemplate)
|
insert(MenuTemplate(), menuTemplate)
|
||||||
|
|
Loading…
Reference in a new issue