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 {
|
||||
id 'kotlin-multiplatform' version '1.3.20'
|
||||
id 'kotlinx-serialization' version '1.3.20'
|
||||
id 'kotlin-multiplatform' version '1.3.31'
|
||||
id 'kotlinx-serialization' version '1.3.31'
|
||||
id "org.kravemir.gradle.sass" version "1.2.2"
|
||||
id "com.github.johnrengelman.shadow" version "4.0.4"
|
||||
}
|
||||
|
@ -24,10 +24,11 @@ repositories {
|
|||
jcenter()
|
||||
maven { url "http://dl.bintray.com/kotlin/ktor" }
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
|
||||
mavenCentral()
|
||||
}
|
||||
def ktor_version = '1.1.2'
|
||||
def serialization_version = '0.10.0'
|
||||
def ktor_version = '1.1.5'
|
||||
def serialization_version = '0.11.0'
|
||||
|
||||
kotlin {
|
||||
jvm() {
|
||||
|
@ -72,7 +73,8 @@ kotlin {
|
|||
implementation "io.ktor:ktor-server-netty:$ktor_version"
|
||||
implementation "io.ktor:ktor-auth:$ktor_version"
|
||||
implementation "io.ktor:ktor-locations:$ktor_version"
|
||||
//implementation "io.ktor:ktor-websockets:$ktor_version"
|
||||
implementation "io.ktor:ktor-websockets:$ktor_version"
|
||||
implementation 'org.jetbrains:kotlin-css-jvm:1.0.0-pre.70-kotlin-1.3.21'
|
||||
|
||||
implementation "io.ktor:ktor-html-builder:$ktor_version"
|
||||
|
||||
|
@ -98,7 +100,7 @@ kotlin {
|
|||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
|
||||
|
||||
implementation "de.westermann:KObserve-js:0.8.0"
|
||||
implementation "de.westermann:KObserve-js:0.9.1"
|
||||
}
|
||||
}
|
||||
jsTest {
|
||||
|
|
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
|
||||
|
||||
import de.kif.frontend.calendar.Calendar
|
||||
import de.westermann.kwebview.components.boxView
|
||||
import de.westermann.kwebview.components.h1
|
||||
import de.westermann.kobserve.property.mapBinding
|
||||
import de.westermann.kwebview.*
|
||||
import de.westermann.kwebview.components.Body
|
||||
import de.westermann.kwebview.components.init
|
||||
import kif.common.model.*
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.events.EventListener
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import kotlin.browser.document
|
||||
import kotlin.browser.window
|
||||
import kotlin.collections.Iterator
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.emptyList
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.find
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.collections.minus
|
||||
import kotlin.collections.plus
|
||||
import kotlin.dom.appendText
|
||||
|
||||
fun main() = init {
|
||||
clear()
|
||||
h1("Test")
|
||||
boxView {
|
||||
style {
|
||||
width = "600px"
|
||||
height = "400px"
|
||||
margin = "10px"
|
||||
class CalendarTools(entry: CalendarEntry, view: HTMLElement) : View(view) {
|
||||
|
||||
init {
|
||||
var linkM10: HTMLAnchorElement? = null
|
||||
var linkM5: HTMLAnchorElement? = null
|
||||
var linkReset: HTMLAnchorElement? = null
|
||||
var linkP5: HTMLAnchorElement? = null
|
||||
var linkP10: HTMLAnchorElement? = null
|
||||
var linkDel: HTMLAnchorElement? = null
|
||||
|
||||
for (element in html.children) {
|
||||
when {
|
||||
element.classList.contains("calendar-tools-m10") -> linkM10 = element as? HTMLAnchorElement
|
||||
element.classList.contains("calendar-tools-m5") -> linkM5 = element as? HTMLAnchorElement
|
||||
element.classList.contains("calendar-tools-reset") -> linkReset = element as? HTMLAnchorElement
|
||||
element.classList.contains("calendar-tools-p5") -> linkP5 = element as? HTMLAnchorElement
|
||||
element.classList.contains("calendar-tools-p10") -> linkP10 = element as? HTMLAnchorElement
|
||||
element.classList.contains("calendar-tools-del") -> linkDel = element as? HTMLAnchorElement
|
||||
}
|
||||
}
|
||||
+Calendar()
|
||||
|
||||
linkM10 = linkM10 ?: run {
|
||||
val link = createHtmlView<HTMLAnchorElement>()
|
||||
link.classList.add("calendar-tools-m10")
|
||||
link.textContent = "-10"
|
||||
html.appendChild(link)
|
||||
link
|
||||
}
|
||||
linkM10.removeAttribute("href")
|
||||
linkM10.addEventListener("click", EventListener {
|
||||
classList += "pending"
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 10}/${entry.workGroup}") {
|
||||
println("success")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
linkM5 = linkM5 ?: run {
|
||||
val link = createHtmlView<HTMLAnchorElement>()
|
||||
link.classList.add("calendar-tools-m5")
|
||||
link.textContent = "-5"
|
||||
html.appendChild(link)
|
||||
link
|
||||
}
|
||||
linkM5.removeAttribute("href")
|
||||
linkM5.addEventListener("click", EventListener {
|
||||
classList += "pending"
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.timeId - 5}/${entry.workGroup}") {
|
||||
println("success")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
linkReset = linkReset ?: run {
|
||||
val link = createHtmlView<HTMLAnchorElement>()
|
||||
link.classList.add("calendar-tools-reset")
|
||||
link.textContent = "reset"
|
||||
html.appendChild(link)
|
||||
link
|
||||
}
|
||||
linkReset.removeAttribute("href")
|
||||
linkReset.addEventListener("click", EventListener {
|
||||
classList += "pending"
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.cellTime}/${entry.workGroup}") {
|
||||
println("success")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
linkP5 = linkP5 ?: run {
|
||||
val link = createHtmlView<HTMLAnchorElement>()
|
||||
link.classList.add("calendar-tools-p5")
|
||||
link.textContent = "+5"
|
||||
html.appendChild(link)
|
||||
link
|
||||
}
|
||||
linkP5.removeAttribute("href")
|
||||
linkP5.addEventListener("click", EventListener {
|
||||
classList += "pending"
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 5}/${entry.workGroup}") {
|
||||
println("success")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
linkP10 = linkP10 ?: run {
|
||||
val link = createHtmlView<HTMLAnchorElement>()
|
||||
link.classList.add("calendar-tools-p10")
|
||||
link.textContent = "+10"
|
||||
html.appendChild(link)
|
||||
link
|
||||
}
|
||||
linkP10.removeAttribute("href")
|
||||
linkP10.addEventListener("click", EventListener {
|
||||
classList += "pending"
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.timeId + 10}/${entry.workGroup}") {
|
||||
println("success")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
linkDel = linkDel ?: run {
|
||||
val link = createHtmlView<HTMLAnchorElement>()
|
||||
link.classList.add("calendar-tools-del")
|
||||
link.textContent = "del"
|
||||
html.appendChild(link)
|
||||
link
|
||||
}
|
||||
linkDel.removeAttribute("href")
|
||||
linkDel.addEventListener("click", EventListener {
|
||||
classList += "pending"
|
||||
get("/calendar/${entry.day}/${entry.room}/${entry.time}/-1") {
|
||||
println("success")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarEntry(view: HTMLElement) : View(view) {
|
||||
|
||||
private lateinit var mouseDelta: Point
|
||||
private var newCell: CalendarCell? = null
|
||||
|
||||
|
||||
var day by dataset.property("day")
|
||||
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
|
||||
var time by dataset.property("time")
|
||||
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
|
||||
var room by dataset.property("room")
|
||||
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
|
||||
var cellTime by dataset.property("cellTime")
|
||||
var language by dataset.property("language")
|
||||
|
||||
var workGroup by dataset.property("workgroup")
|
||||
val workGroupId by dataset.property("workgroup").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
private fun onMove(event: MouseEvent) {
|
||||
val position = event.toPoint() - mouseDelta
|
||||
|
||||
newCell?.classList?.remove("drag")
|
||||
|
||||
val cell = calendarCells.find {
|
||||
position in it.dimension
|
||||
}
|
||||
|
||||
if (cell != null) {
|
||||
cell.classList.add("drag")
|
||||
cell += this
|
||||
newCell = cell
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
private fun onFinishMove(event: MouseEvent) {
|
||||
classList -= "drag"
|
||||
|
||||
newCell?.let { cell ->
|
||||
cell.classList -= "drag"
|
||||
|
||||
val newTime = cell.time
|
||||
val newRoom = cell.room
|
||||
val day =
|
||||
(document.getElementsByClassName("calendar")[0] as? HTMLElement)?.dataset?.get("day")?.toIntOrNull()
|
||||
?: 0
|
||||
|
||||
classList += "pending"
|
||||
get("/calendar/$day/$room/$time/-1") {
|
||||
get("/calendar/$day/$newRoom/$newTime/$workGroup") {
|
||||
println("success")
|
||||
}
|
||||
}
|
||||
}
|
||||
newCell = null
|
||||
|
||||
for (it in listeners) {
|
||||
it.detach()
|
||||
}
|
||||
listeners = emptyList()
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
|
||||
|
||||
init {
|
||||
onMouseDown { event ->
|
||||
if (event.target != html || "pending" in classList) {
|
||||
event.stopPropagation()
|
||||
return@onMouseDown
|
||||
}
|
||||
|
||||
classList += "drag"
|
||||
|
||||
mouseDelta = event.toPoint() - point
|
||||
|
||||
listeners = listOf(
|
||||
Body.onMouseMove.reference(this::onMove),
|
||||
Body.onMouseUp.reference(this::onFinishMove),
|
||||
Body.onMouseLeave.reference(this::onFinishMove)
|
||||
)
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
var calendarTools: CalendarTools? = null
|
||||
for (item in html.children) {
|
||||
if (item.classList.contains("calendar-tools")) {
|
||||
calendarTools = CalendarTools(this, item)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (calendarTools == null) {
|
||||
calendarTools = CalendarTools(this, createHtmlView())
|
||||
html.appendChild(calendarTools.html)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
day: Int,
|
||||
time: Int,
|
||||
cellTime: Int,
|
||||
room: Int,
|
||||
workGroupId: Int,
|
||||
workGroupName: String,
|
||||
workGroupLength: Int,
|
||||
workGroupLanguage: String,
|
||||
workGroupColor: Color?
|
||||
): CalendarEntry {
|
||||
val entry = CalendarEntry(createHtmlView())
|
||||
|
||||
entry.day = day.toString()
|
||||
entry.time = time.toString()
|
||||
entry.cellTime = cellTime.toString()
|
||||
entry.room = room.toString()
|
||||
entry.workGroup = workGroupId.toString()
|
||||
entry.language = workGroupLanguage
|
||||
if (workGroupColor != null) {
|
||||
|
||||
entry.style {
|
||||
val size = workGroupLength / CALENDAR_GRID_WIDTH.toDouble()
|
||||
val pos = (time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
|
||||
|
||||
val pct = "${pos * 100}%"
|
||||
val sz = "${size * 100}%"
|
||||
|
||||
left = pct
|
||||
top = "calc($pct + 0.1rem)"
|
||||
|
||||
width = sz
|
||||
height = "calc($sz - 0.2rem)"
|
||||
|
||||
backgroundColor = workGroupColor.toString()
|
||||
color = workGroupColor.textColor.toString()
|
||||
}
|
||||
}
|
||||
entry.html.appendText(workGroupName)
|
||||
|
||||
return entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
|
||||
var day by dataset.property("day")
|
||||
val dayId by dataset.property("day").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
|
||||
var time by dataset.property("time")
|
||||
val timeId by dataset.property("time").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
|
||||
var room by dataset.property("room")
|
||||
val roomId by dataset.property("room").mapBinding { it?.toIntOrNull() ?: 0 }
|
||||
}
|
||||
|
||||
var calendarEntries: List<CalendarEntry> = emptyList()
|
||||
var calendarCells: List<CalendarCell> = emptyList()
|
||||
|
||||
fun main() = init {
|
||||
|
||||
val ws = WebSocket("ws://${window.location.host}/".also { println(it) })
|
||||
|
||||
ws.onmessage = {
|
||||
val messageWrapper = Message.parse(it.data?.toString() ?: "")
|
||||
val message = messageWrapper.message
|
||||
|
||||
println(message)
|
||||
|
||||
when (message) {
|
||||
is MessageCreateCalendarEntry -> {
|
||||
val entry = CalendarEntry.create(
|
||||
message.day,
|
||||
message.time,
|
||||
message.cellTime,
|
||||
message.room,
|
||||
message.workGroupId,
|
||||
message.workGroupName,
|
||||
message.workGroupLength,
|
||||
message.workGroupLanguage,
|
||||
message.workGroupColor
|
||||
)
|
||||
for (cell in calendarCells) {
|
||||
if (cell.dayId == message.day && cell.timeId == message.cellTime && cell.roomId == message.room) {
|
||||
cell.html.appendChild(entry.html)
|
||||
calendarEntries += entry
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
is MessageDeleteCalendarEntry -> {
|
||||
val remove = calendarEntries.filter { entry ->
|
||||
entry.dayId == message.day &&
|
||||
entry.timeId == message.time &&
|
||||
entry.roomId == message.roomId &&
|
||||
entry.workGroupId == message.workGroupId
|
||||
}
|
||||
calendarEntries -= remove
|
||||
remove.forEach {
|
||||
it.html.remove()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.onopen = {
|
||||
console.log("yes!")
|
||||
}
|
||||
|
||||
calendarEntries = document.getElementsByClassName("calendar-entry")
|
||||
.iterator().asSequence().map(::CalendarEntry).toList()
|
||||
|
||||
calendarCells = document.getElementsByClassName("calendar-cell")
|
||||
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
|
||||
}
|
||||
|
||||
operator fun HTMLCollection.iterator() = object : Iterator<HTMLElement> {
|
||||
private var index = 0
|
||||
override fun hasNext(): Boolean {
|
||||
return index < this@iterator.length
|
||||
}
|
||||
|
||||
override fun next(): HTMLElement {
|
||||
return this@iterator.get(index++) as HTMLElement
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package de.westermann.kwebview
|
||||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package de.westermann.kwebview
|
||||
|
||||
import de.westermann.kobserve.ListenerReference
|
||||
import de.westermann.kobserve.event.EventListener
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import org.w3c.dom.DOMTokenList
|
||||
|
@ -102,7 +102,7 @@ class ClassList(
|
|||
throw IllegalArgumentException("Class is not bound!")
|
||||
}
|
||||
|
||||
bound[clazz]?.reference?.remove()
|
||||
bound[clazz]?.reference?.detach()
|
||||
bound -= clazz
|
||||
}
|
||||
|
||||
|
@ -114,6 +114,6 @@ class ClassList(
|
|||
|
||||
private data class Bound(
|
||||
val property: ReadOnlyProperty<Boolean>,
|
||||
val reference: ListenerReference<Unit>?
|
||||
val reference: EventListener<Unit>?
|
||||
)
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
package de.westermann.kwebview
|
||||
|
||||
import de.westermann.kobserve.ListenerReference
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.event.EventListener
|
||||
import org.w3c.dom.DOMStringMap
|
||||
import org.w3c.dom.get
|
||||
import org.w3c.dom.set
|
||||
import kotlin.collections.MutableMap
|
||||
import kotlin.collections.contains
|
||||
import kotlin.collections.minusAssign
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
* Represents the css classes of an html element.
|
||||
|
@ -13,7 +18,7 @@ import org.w3c.dom.set
|
|||
* @author lars
|
||||
*/
|
||||
class DataSet(
|
||||
private val map: DOMStringMap
|
||||
private val map: DOMStringMap
|
||||
) {
|
||||
|
||||
private val bound: MutableMap<String, Bound> = mutableMapOf()
|
||||
|
@ -49,11 +54,11 @@ class DataSet(
|
|||
* Set css class present.
|
||||
*/
|
||||
operator fun set(key: String, value: String?) =
|
||||
if (value == null) {
|
||||
this -= key
|
||||
} else {
|
||||
this += key to value
|
||||
}
|
||||
if (value == null) {
|
||||
this -= key
|
||||
} else {
|
||||
this += key to value
|
||||
}
|
||||
|
||||
fun bind(key: String, property: ReadOnlyProperty<String>) {
|
||||
if (key in bound) {
|
||||
|
@ -71,22 +76,34 @@ class DataSet(
|
|||
bound[key] = Bound(key, property, null)
|
||||
}
|
||||
|
||||
fun property(key: String): Property<String?> {
|
||||
if (key in bound) {
|
||||
return bound[key]?.propertyNullable as? Property<String?> ?: throw IllegalArgumentException("Class is already bound!")
|
||||
}
|
||||
|
||||
val property = de.westermann.kobserve.property.property(get(key))
|
||||
|
||||
bound[key] = Bound(key, property, null)
|
||||
|
||||
return property
|
||||
}
|
||||
|
||||
fun unbind(key: String) {
|
||||
if (key !in bound) {
|
||||
throw IllegalArgumentException("Class is not bound!")
|
||||
}
|
||||
|
||||
bound[key]?.reference?.remove()
|
||||
bound[key]?.reference?.detach()
|
||||
bound -= key
|
||||
}
|
||||
|
||||
private inner class Bound(
|
||||
val key: String,
|
||||
val propertyNullable: ReadOnlyProperty<String?>?,
|
||||
val property: ReadOnlyProperty<String>?
|
||||
val key: String,
|
||||
val propertyNullable: ReadOnlyProperty<String?>?,
|
||||
val property: ReadOnlyProperty<String>?
|
||||
) {
|
||||
|
||||
var reference: ListenerReference<Unit>? = null
|
||||
var reference: EventListener<Unit>? = null
|
||||
|
||||
fun set(value: String?) {
|
||||
if (propertyNullable != null && propertyNullable is Property) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package de.westermann.kwebview
|
||||
|
||||
import de.westermann.kobserve.EventHandler
|
||||
import de.westermann.kobserve.event.EventHandler
|
||||
import org.w3c.dom.DragEvent
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.css.CSSStyleDeclaration
|
||||
import org.w3c.dom.events.FocusEvent
|
||||
|
@ -63,6 +64,9 @@ abstract class View(view: HTMLElement = createHtmlView()) {
|
|||
val dimension: Dimension
|
||||
get() = html.getBoundingClientRect().toDimension()
|
||||
|
||||
val point: Point
|
||||
get() = dimension.position
|
||||
|
||||
var title by AttributeDelegate()
|
||||
|
||||
val style = view.style
|
||||
|
@ -101,6 +105,15 @@ abstract class View(view: HTMLElement = createHtmlView()) {
|
|||
val onFocus = EventHandler<FocusEvent>()
|
||||
val onBlur = EventHandler<FocusEvent>()
|
||||
|
||||
|
||||
val onDragStart = EventHandler<DragEvent>()
|
||||
val onDrag = EventHandler<DragEvent>()
|
||||
val onDragEnter = EventHandler<DragEvent>()
|
||||
val onDragLeave = EventHandler<DragEvent>()
|
||||
val onDragOver = EventHandler<DragEvent>()
|
||||
val onDrop = EventHandler<DragEvent>()
|
||||
val onDragEnd = EventHandler<DragEvent>()
|
||||
|
||||
init {
|
||||
onClick.bind(view, "click")
|
||||
onDblClick.bind(view, "dblclick")
|
||||
|
@ -120,5 +133,17 @@ abstract class View(view: HTMLElement = createHtmlView()) {
|
|||
|
||||
onFocus.bind(view, "focus")
|
||||
onBlur.bind(view, "blur")
|
||||
|
||||
onDragStart.bind(view, "dragstart")
|
||||
onDrag.bind(view, "drag")
|
||||
onDragEnter.bind(view, "dragenter")
|
||||
onDragLeave.bind(view, "dragleave")
|
||||
onDragOver.bind(view, "dragover")
|
||||
onDrop.bind(view, "drop")
|
||||
onDragEnd.bind(view, "dragend")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun wrap(htmlElement: HTMLElement) = object : View(htmlElement) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,4 +66,8 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
|
|||
operator fun V.unaryPlus() {
|
||||
append(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <V : View> wrap(htmlElement: HTMLElement) = object : ViewCollection<V>(htmlElement) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
|||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.KWebViewDsl
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
|
|
|
@ -3,7 +3,7 @@ package de.westermann.kwebview.components
|
|||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.ValidationProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.*
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventListener
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
|||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.KWebViewDsl
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
|||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.*
|
||||
import org.w3c.dom.HTMLImageElement
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package de.westermann.kwebview.components
|
|||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.ValidationProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kobserve.not
|
||||
import de.westermann.kwebview.*
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
|||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.*
|
||||
import org.w3c.dom.HTMLLabelElement
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
|||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.AttributeDelegate
|
||||
import de.westermann.kwebview.KWebViewDsl
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.westermann.kwebview.components
|
|||
|
||||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.basic.property
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.KWebViewDsl
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package de.westermann.kwebview
|
||||
|
||||
import de.westermann.kobserve.EventHandler
|
||||
import de.westermann.kobserve.event.EventHandler
|
||||
import org.w3c.dom.DOMRect
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventListener
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import org.w3c.xhr.FormData
|
||||
import org.w3c.xhr.XMLHttpRequest
|
||||
import kotlin.browser.document
|
||||
import kotlin.browser.window
|
||||
|
||||
|
@ -15,9 +17,8 @@ inline fun <reified V : HTMLElement> createHtmlView(tag: String? = null): V {
|
|||
tagName = tag
|
||||
} else {
|
||||
tagName = V::class.js.name.toLowerCase().replace("html([a-z]*)element".toRegex(), "$1")
|
||||
if (tagName.isBlank()) {
|
||||
tagName = "div"
|
||||
}
|
||||
if (tagName.isBlank()) tagName = "div"
|
||||
if (tagName == "anchor") tagName = "a"
|
||||
}
|
||||
return document.createElement(tagName) as V
|
||||
}
|
||||
|
@ -77,3 +78,66 @@ fun interval(timeout: Int, block: () -> Unit): Int {
|
|||
fun clearInterval(id: Int) {
|
||||
window.clearInterval(id)
|
||||
}
|
||||
|
||||
fun get(
|
||||
url: String,
|
||||
data: Map<String, String> = emptyMap(),
|
||||
onError: (Int) -> Unit = {},
|
||||
onSuccess: (String) -> Unit = {}
|
||||
) {
|
||||
val xhttp = XMLHttpRequest()
|
||||
|
||||
xhttp.onreadystatechange = {
|
||||
if (xhttp.readyState == 4.toShort()) {
|
||||
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
|
||||
onSuccess(xhttp.responseText)
|
||||
} else {
|
||||
onError(xhttp.status.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
xhttp.open("GET", url, true)
|
||||
|
||||
if (data.isNotEmpty()) {
|
||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
val formData = FormData()
|
||||
for ((key, value) in data) {
|
||||
formData.append(key, value)
|
||||
}
|
||||
xhttp.send(formData)
|
||||
} else {
|
||||
xhttp.send()
|
||||
}
|
||||
}
|
||||
|
||||
fun post(
|
||||
url: String,
|
||||
data: Map<String, String> = emptyMap(),
|
||||
onError: (Int) -> Unit = {},
|
||||
onSuccess: (String) -> Unit = {}
|
||||
) {
|
||||
val xhttp = XMLHttpRequest()
|
||||
|
||||
xhttp.onreadystatechange = {
|
||||
if (xhttp.readyState == 4.toShort()) {
|
||||
console.log(xhttp.status)
|
||||
if (xhttp.status == 200.toShort() || xhttp.status == 304.toShort()) {
|
||||
onSuccess(xhttp.responseText)
|
||||
} else {
|
||||
onError(xhttp.status.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
xhttp.open("POST", url, true)
|
||||
|
||||
if (data.isNotEmpty()) {
|
||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
val formData = FormData()
|
||||
for ((key, value) in data) {
|
||||
formData.append(key, value)
|
||||
}
|
||||
xhttp.send(formData)
|
||||
} else {
|
||||
xhttp.send()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ a {
|
|||
position: relative;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
z-index: 6;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
|
@ -157,13 +158,24 @@ a {
|
|||
display: none;
|
||||
position: absolute;
|
||||
background-color: $background-secondary-color;
|
||||
z-index: 1;
|
||||
z-index: 5;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
top: -8rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8rem;
|
||||
display: block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
line-height: 3rem;
|
||||
|
||||
&:after {
|
||||
bottom: 0.2rem;
|
||||
}
|
||||
|
@ -192,6 +204,7 @@ a {
|
|||
a {
|
||||
display: inline-block;
|
||||
line-height: unset;
|
||||
|
||||
&:after {
|
||||
bottom: 1.8em
|
||||
}
|
||||
|
@ -215,7 +228,7 @@ a {
|
|||
.btn-search {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: calc(2.5rem + 2px);
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
right: -3px;
|
||||
border-bottom-left-radius: 0;
|
||||
|
@ -228,9 +241,6 @@ a {
|
|||
input:focus ~ .btn-search {
|
||||
border-color: $primary-color;
|
||||
border-width: 2px;
|
||||
margin-top: 0;
|
||||
height: calc(2.5rem + 4px);
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,6 +292,7 @@ a {
|
|||
outline: none;
|
||||
padding: 0 1rem;
|
||||
line-height: 2.5rem;
|
||||
height: 2.5rem;
|
||||
width: 100%;
|
||||
background-color: $background-primary-color;
|
||||
border-radius: 0.2rem;
|
||||
|
@ -291,10 +302,14 @@ a {
|
|||
&:focus {
|
||||
border-color: $primary-color;
|
||||
border-width: 2px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
select:-moz-focusring {
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 $text-primary-color;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding-bottom: 1rem;
|
||||
display: block;
|
||||
|
@ -411,7 +426,7 @@ a {
|
|||
border-color: $error-color;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner, input::-moz-focus-inner {
|
||||
button::-moz-focus-inner, input::-moz-focus-inner, select::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
@ -425,3 +440,358 @@ form {
|
|||
width: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
|
||||
.form-btn {
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
& > * {
|
||||
margin-right: 0;
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
|
||||
& > div {
|
||||
width: calc(100% - 6rem);
|
||||
overflow-x: scroll;
|
||||
overflow-y: visible;
|
||||
margin-left: 6rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-tools {
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 0.2rem;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
|
||||
border: solid 1px $border-color;
|
||||
box-shadow: 0 0.1rem 0.2rem rgba($primary-text-color);
|
||||
|
||||
a {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
position: absolute;
|
||||
display: block;
|
||||
border-radius: 0.2rem;
|
||||
z-index: 1;
|
||||
line-height: 2rem;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
background-color: $primary-color;
|
||||
color: $primary-text-color;
|
||||
|
||||
padding: 0 0.5rem;
|
||||
|
||||
&::after {
|
||||
content: attr(data-language);
|
||||
bottom: 0;
|
||||
right: 1rem;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&:hover .calendar-tools {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.drag {
|
||||
box-shadow: 0 0.1rem 0.2rem rgba($text-primary-color, 0.8);
|
||||
z-index: 2;
|
||||
|
||||
.calendar-tools {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.pending::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $background-primary-color;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-table-time-to-room {
|
||||
.calendar-header, .calendar-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
line-height: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
.calendar-cell:not(:first-child) {
|
||||
font-size: 0.8rem;
|
||||
width: 6rem;
|
||||
position: relative;
|
||||
padding-left: 0.2rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-row {
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
line-height: 3rem;
|
||||
height: 3rem;
|
||||
|
||||
.calendar-cell {
|
||||
position: relative;
|
||||
width: 1.5rem;
|
||||
|
||||
&:nth-child(2n + 2)::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
top: 0.5rem !important;
|
||||
bottom: 0.5rem;
|
||||
width: 0;
|
||||
left: 0;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-cell:first-child {
|
||||
position: absolute;
|
||||
width: 6rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
border-right: solid 1px rgba($text-primary-color, 0.1);
|
||||
}
|
||||
|
||||
.calendar-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-table-room-to-time {
|
||||
.calendar-header, .calendar-row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
line-height: 2rem;
|
||||
height: 2rem;
|
||||
|
||||
.calendar-cell:not(:first-child) {
|
||||
width: 12rem;
|
||||
position: relative;
|
||||
padding-left: 0.2rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-row {
|
||||
line-height: 2rem;
|
||||
height: 1.3rem;
|
||||
|
||||
&:nth-child(4n + 2)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(100% - 6rem);
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
}
|
||||
|
||||
.calendar-cell {
|
||||
position: relative;
|
||||
width: 12rem;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
top: 0.1rem;
|
||||
bottom: 0;
|
||||
width: auto !important;
|
||||
left: 0.1rem !important;
|
||||
right: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-cell:first-child:not(:empty)::before {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
border-left: none;
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-cell:first-child {
|
||||
position: absolute;
|
||||
width: 6rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
.color-picker-entry {
|
||||
position: relative;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
float: left;
|
||||
|
||||
input {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 1.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input:checked ~ label::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
top: 1rem;
|
||||
left: 0.8rem;
|
||||
width: 1.1rem;
|
||||
height: 0.5rem;
|
||||
transform: rotate(-45deg);
|
||||
border-left: solid 0.3rem $background-primary-color;
|
||||
border-bottom: solid 0.3rem $background-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-custom {
|
||||
position: relative;
|
||||
clear: both;
|
||||
height: 4rem;
|
||||
|
||||
& > input {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 1.5rem;
|
||||
z-index: 1;
|
||||
border: solid 0.1rem $text-primary-color;
|
||||
}
|
||||
|
||||
input:checked ~ label::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
top: 1rem;
|
||||
left: 0.8rem;
|
||||
width: 1.1rem;
|
||||
height: 0.5rem;
|
||||
transform: rotate(-45deg);
|
||||
border-left: solid 0.3rem $text-primary-color;
|
||||
border-bottom: solid 0.3rem $text-primary-color;
|
||||
}
|
||||
|
||||
label input {
|
||||
position: absolute;
|
||||
left: 4rem;
|
||||
top: 0.2rem;
|
||||
height: 2rem;
|
||||
width: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,9 @@ import io.ktor.response.respondRedirect
|
|||
import io.ktor.routing.routing
|
||||
import io.ktor.sessions.*
|
||||
import io.ktor.util.hex
|
||||
import io.ktor.websocket.WebSockets
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
data class PortalSession(val id: Int, val name: String) {
|
||||
suspend fun getUser(call: ApplicationCall): User {
|
||||
|
@ -37,8 +39,39 @@ data class PortalSession(val id: Int, val name: String) {
|
|||
@Location("/")
|
||||
class LocationDashboard()
|
||||
|
||||
@Location("/calendar")
|
||||
class LocationCalendar()
|
||||
@Location("/calendar/{day}")
|
||||
data class LocationCalendar(val day: Int) {
|
||||
@Location("/{room}/{time}")
|
||||
data class LocationCalendarEdit(
|
||||
val calendar: LocationCalendar,
|
||||
val room: Int,
|
||||
val time: Int,
|
||||
val search: String = ""
|
||||
) {
|
||||
val day: Int get() = calendar.day
|
||||
}
|
||||
|
||||
@Location("/{room}/{time}/{workGroup}")
|
||||
data class LocationCalendarSet(
|
||||
val calendar: LocationCalendar,
|
||||
val room: Int,
|
||||
val time: Int,
|
||||
val workGroup: Int,
|
||||
val next: String? = null
|
||||
) {
|
||||
val day: Int get() = calendar.day
|
||||
}
|
||||
|
||||
@Location("/time-to-room")
|
||||
data class LocationCalendarTimeToRoom(val calendar: LocationCalendar){
|
||||
val day: Int get() = calendar.day
|
||||
}
|
||||
|
||||
@Location("/room-to-time")
|
||||
data class LocationCalendarRoomToTime(val calendar: LocationCalendar){
|
||||
val day: Int get() = calendar.day
|
||||
}
|
||||
}
|
||||
|
||||
@Location("/login")
|
||||
data class LocationLogin(val username: String = "", val password: String = "", val next: String = "/")
|
||||
|
@ -73,6 +106,18 @@ data class LocationWorkGroup(val search: String = "") {
|
|||
data class Delete(val id: Int)
|
||||
}
|
||||
|
||||
@Location("/track")
|
||||
data class LocationTrack(val search: String = "") {
|
||||
@Location("/{id}")
|
||||
data class Edit(val id: Int)
|
||||
|
||||
@Location("/new")
|
||||
class New()
|
||||
|
||||
@Location("/{id}/delete")
|
||||
data class Delete(val id: Int)
|
||||
}
|
||||
|
||||
@Location("/room")
|
||||
data class LocationRoom(val search: String = "") {
|
||||
@Location("/{id}")
|
||||
|
@ -106,6 +151,7 @@ fun Application.main() {
|
|||
install(Compression)
|
||||
install(DataConversion)
|
||||
install(Locations)
|
||||
install(WebSockets)
|
||||
|
||||
install(Authentication) {
|
||||
form {
|
||||
|
@ -145,9 +191,12 @@ fun Application.main() {
|
|||
account()
|
||||
|
||||
workGroup()
|
||||
track()
|
||||
room()
|
||||
person()
|
||||
user()
|
||||
|
||||
pushService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ object Connection {
|
|||
DbPerson, DbPersonConstraint,
|
||||
DbTrack, DbWorkGroup, DbWorkGroupConstraint,
|
||||
DbLeader, DbWorkGroupOrder,
|
||||
DbRoom, DbTimeSlot, DbSchedule,
|
||||
DbRoom, DbSchedule,
|
||||
DbUser, DbUserPermission
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ object DbPersonConstraint : Table() {
|
|||
object DbTrack : Table() {
|
||||
val id = integer("id").autoIncrement().primaryKey()
|
||||
val name = varchar("name", 64)
|
||||
val color = varchar("color", 32)
|
||||
}
|
||||
|
||||
object DbWorkGroup : Table() {
|
||||
|
@ -35,6 +36,7 @@ object DbWorkGroup : Table() {
|
|||
val trackId = integer("track_id").nullable()
|
||||
val projector = bool("projector")
|
||||
val resolution = bool("resolution")
|
||||
val language = enumeration("language", Language::class)
|
||||
|
||||
val length = integer("length")
|
||||
|
||||
|
@ -70,18 +72,11 @@ object DbRoom : Table() {
|
|||
val projector = bool("projector")
|
||||
}
|
||||
|
||||
object DbTimeSlot : Table() {
|
||||
val id = integer("id").autoIncrement().primaryKey()
|
||||
|
||||
val time = integer("time")
|
||||
val duration = integer("duration")
|
||||
val day = integer("day")
|
||||
}
|
||||
|
||||
object DbSchedule : Table() {
|
||||
val workGroupId = integer("work_group_id").primaryKey(0)
|
||||
val timeSlotId = integer("time_slot_id").primaryKey(1)
|
||||
val roomId = integer("room_id").primaryKey(2)
|
||||
val day = integer("day").primaryKey(1)
|
||||
val time = integer("time_slot").primaryKey(2)
|
||||
val roomId = integer("room_id").primaryKey(3)
|
||||
}
|
||||
|
||||
enum class DbConstraintType {
|
||||
|
@ -98,3 +93,10 @@ object DbUserPermission : Table() {
|
|||
val userId = integer("id").primaryKey(0)
|
||||
val permission = enumeration("permission", Permission::class).primaryKey(1)
|
||||
}
|
||||
|
||||
enum class Language(val value: String) {
|
||||
GERMAN("Deutsch"), ENGLISH("English");
|
||||
|
||||
override fun toString() = value
|
||||
val code = value.take(2).toLowerCase()
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
package de.kif.backend.model
|
||||
|
||||
class Day {
|
||||
}
|
|
@ -34,15 +34,34 @@ class Room(
|
|||
suspend fun delete() {
|
||||
val id = id
|
||||
if (id >= 0) {
|
||||
for (it in Schedule.getByRoom(id)) {
|
||||
it.delete()
|
||||
}
|
||||
dbQuery {
|
||||
DbRoom.deleteWhere { DbRoom.id eq id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Room
|
||||
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
suspend fun get(roomId: Int): Room? = dbQuery {
|
||||
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: return@dbQuery null
|
||||
suspend fun get(roomId: Int): Room = dbQuery {
|
||||
val result = DbRoom.select { DbRoom.id eq roomId }.firstOrNull() ?: throw IllegalArgumentException()
|
||||
Room(
|
||||
result[DbRoom.id],
|
||||
result[DbRoom.name],
|
||||
|
|
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.DbWorkGroupConstraint
|
||||
import de.kif.backend.database.Language
|
||||
import de.kif.backend.database.dbQuery
|
||||
import io.ktor.features.NotFoundException
|
||||
import org.jetbrains.exposed.sql.*
|
||||
|
||||
class WorkGroup(
|
||||
|
@ -13,10 +15,12 @@ class WorkGroup(
|
|||
var projector: Boolean = false,
|
||||
var resolution: Boolean = false,
|
||||
var length: Int = 0,
|
||||
var language: Language = Language.GERMAN,
|
||||
var start: Long? = null,
|
||||
var end: Long? = null
|
||||
) {
|
||||
var constraints: Set<WorkGroupConstraint> = emptySet()
|
||||
var track: Track? = null
|
||||
|
||||
suspend fun save() {
|
||||
if (id < 0) {
|
||||
|
@ -28,6 +32,7 @@ class WorkGroup(
|
|||
it[projector] = this@WorkGroup.projector
|
||||
it[resolution] = this@WorkGroup.resolution
|
||||
it[length] = this@WorkGroup.length
|
||||
it[language] = this@WorkGroup.language
|
||||
it[start] = this@WorkGroup.start
|
||||
it[end] = this@WorkGroup.end
|
||||
}[DbWorkGroup.id]!!
|
||||
|
@ -45,6 +50,7 @@ class WorkGroup(
|
|||
it[projector] = this@WorkGroup.projector
|
||||
it[resolution] = this@WorkGroup.resolution
|
||||
it[length] = this@WorkGroup.length
|
||||
it[language] = this@WorkGroup.language
|
||||
it[start] = this@WorkGroup.start
|
||||
it[end] = this@WorkGroup.end
|
||||
}
|
||||
|
@ -60,6 +66,9 @@ class WorkGroup(
|
|||
suspend fun delete() {
|
||||
val id = id
|
||||
if (id >= 0) {
|
||||
for (it in Schedule.getByWorkGroup(id)) {
|
||||
it.delete()
|
||||
}
|
||||
dbQuery {
|
||||
DbWorkGroupConstraint.deleteWhere { DbWorkGroupConstraint.workGroupId eq id }
|
||||
DbWorkGroup.deleteWhere { DbWorkGroup.id eq id }
|
||||
|
@ -70,12 +79,13 @@ class WorkGroup(
|
|||
suspend fun loadConstraints() {
|
||||
if (id >= 0) {
|
||||
constraints = WorkGroupConstraint.get(id)
|
||||
track = trackId?.let { if (it < 0) null else Track.get(it) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun get(workGroupId: Int): WorkGroup? = dbQuery {
|
||||
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: return@dbQuery null
|
||||
suspend fun get(workGroupId: Int): WorkGroup = dbQuery {
|
||||
val result = DbWorkGroup.select { DbWorkGroup.id eq workGroupId }.firstOrNull() ?: throw IllegalArgumentException()
|
||||
WorkGroup(
|
||||
result[DbWorkGroup.id],
|
||||
result[DbWorkGroup.name],
|
||||
|
@ -84,10 +94,11 @@ class WorkGroup(
|
|||
result[DbWorkGroup.projector],
|
||||
result[DbWorkGroup.resolution],
|
||||
result[DbWorkGroup.length],
|
||||
result[DbWorkGroup.language],
|
||||
result[DbWorkGroup.start],
|
||||
result[DbWorkGroup.end]
|
||||
)
|
||||
}?.apply { loadConstraints() }
|
||||
}.apply { loadConstraints() }
|
||||
|
||||
suspend fun list(): List<WorkGroup> = dbQuery {
|
||||
val query = DbWorkGroup.selectAll()
|
||||
|
@ -100,6 +111,7 @@ class WorkGroup(
|
|||
result[DbWorkGroup.projector],
|
||||
result[DbWorkGroup.resolution],
|
||||
result[DbWorkGroup.length],
|
||||
result[DbWorkGroup.language],
|
||||
result[DbWorkGroup.start],
|
||||
result[DbWorkGroup.end]
|
||||
)
|
||||
|
|
|
@ -1,28 +1,490 @@
|
|||
package de.kif.backend.route
|
||||
|
||||
import de.kif.backend.LocationCalendar
|
||||
import de.kif.backend.LocationLogin
|
||||
import de.kif.backend.PortalSession
|
||||
import de.kif.backend.model.Permission
|
||||
import de.kif.backend.model.Room
|
||||
import de.kif.backend.model.Schedule
|
||||
import de.kif.backend.model.WorkGroup
|
||||
import de.kif.backend.util.Search
|
||||
import de.kif.backend.view.MainTemplate
|
||||
import de.kif.backend.view.MenuTemplate
|
||||
import de.kif.backend.view.TableTemplate
|
||||
import io.ktor.application.call
|
||||
import io.ktor.html.insert
|
||||
import io.ktor.html.respondHtmlTemplate
|
||||
import io.ktor.locations.get
|
||||
import io.ktor.request.path
|
||||
import io.ktor.response.respondRedirect
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.get
|
||||
import io.ktor.sessions.get
|
||||
import io.ktor.sessions.sessions
|
||||
import kotlinx.html.h1
|
||||
import kif.common.model.CALENDAR_GRID_WIDTH
|
||||
import kif.common.model.MessageCreateCalendarEntry
|
||||
import kif.common.model.MessageDeleteCalendarEntry
|
||||
import kotlinx.css.CSSBuilder
|
||||
import kotlinx.css.Color
|
||||
import kotlinx.css.pct
|
||||
import kotlinx.css.rem
|
||||
import kotlinx.html.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
const val MINUTES_OF_DAY = 24 * 60
|
||||
|
||||
private fun DIV.calendarCell(schedule: Schedule?, day: Int, time: Int) {
|
||||
if (schedule != null) {
|
||||
span("calendar-entry") {
|
||||
attributes["style"] = CSSBuilder().apply {
|
||||
val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
|
||||
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
|
||||
|
||||
this.left = (pos * 100).pct
|
||||
this.top = (pos * 100).pct + 0.1.rem
|
||||
|
||||
this.width = (size * 100).pct
|
||||
this.height = (size * 100).pct - 0.2.rem
|
||||
|
||||
val c = schedule.workGroup.track?.color
|
||||
if (c != null) {
|
||||
backgroundColor = Color(c.toString())
|
||||
color = Color(c.textColor.toString())
|
||||
}
|
||||
}.toString()
|
||||
attributes["data-language"] = schedule.workGroup.language.code
|
||||
attributes["data-day"] = schedule.day.toString()
|
||||
attributes["data-room"] = schedule.room.id.toString()
|
||||
attributes["data-time"] = schedule.time.toString()
|
||||
attributes["data-cell-time"] = time.toString()
|
||||
attributes["data-workgroup"] = schedule.workGroup.id.toString()
|
||||
|
||||
+schedule.workGroup.name
|
||||
|
||||
div("calendar-tools") {
|
||||
a(
|
||||
classes = "calendar-tools-m10",
|
||||
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 10}/${schedule.workGroupId}"
|
||||
) { +"-10" }
|
||||
a(
|
||||
classes = "calendar-tools-m5",
|
||||
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time - 5}/${schedule.workGroupId}"
|
||||
) { +"-05" }
|
||||
a(
|
||||
classes = "calendar-tools-reset",
|
||||
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/$time/${schedule.workGroupId}"
|
||||
) { +"reset" }
|
||||
a(
|
||||
classes = "calendar-tools-p5",
|
||||
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 5}/${schedule.workGroupId}"
|
||||
) { +"+05" }
|
||||
a(
|
||||
classes = "calendar-tools-p10",
|
||||
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1?next=/calendar/$day/${schedule.room.id}/${schedule.time + 10}/${schedule.workGroupId}"
|
||||
) { +"+10" }
|
||||
a(
|
||||
classes = "calendar-tools-del",
|
||||
href = "/calendar/$day/${schedule.room.id}/${schedule.time}/-1"
|
||||
) { +"del" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DIV.renderTimeToRoom(
|
||||
day: Int,
|
||||
from: Int,
|
||||
to: Int,
|
||||
rooms: List<Room>,
|
||||
schedules: Map<Room, Map<Int, Schedule>>,
|
||||
allowEdit: Boolean
|
||||
) {
|
||||
val gridLabelWidth = 60
|
||||
val minutesOfDay = to - from
|
||||
|
||||
div("calendar-table-time-to-room") {
|
||||
div("calendar-header") {
|
||||
div("calendar-cell") {
|
||||
span {
|
||||
+"Room"
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until minutesOfDay / gridLabelWidth) {
|
||||
div("calendar-cell") {
|
||||
span {
|
||||
val time = ((i * gridLabelWidth + from) % MINUTES_OF_DAY).let {
|
||||
if (it < 0) it + MINUTES_OF_DAY else it
|
||||
}
|
||||
val minutes = (time % 60).toString().padStart(2, '0')
|
||||
val hours = (time / 60).toString().padStart(2, '0')
|
||||
+"$hours:$minutes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room in rooms) {
|
||||
div("calendar-row") {
|
||||
div("calendar-cell") {
|
||||
span {
|
||||
+room.name
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
|
||||
val start = i * CALENDAR_GRID_WIDTH + from
|
||||
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
|
||||
|
||||
div("calendar-cell") {
|
||||
val time = i * CALENDAR_GRID_WIDTH
|
||||
val minutes = (time % 60).toString().padStart(2, '0')
|
||||
val hours = (time / 60).toString().padStart(2, '0')
|
||||
title = "$hours:$minutes"
|
||||
attributes["data-time"] = time.toString()
|
||||
attributes["data-room"] = room.id.toString()
|
||||
attributes["data-day"] = day.toString()
|
||||
|
||||
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
|
||||
|
||||
calendarCell(schedule, day, time)
|
||||
|
||||
val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null
|
||||
a(href, classes = "calendar-link")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DIV.renderRoomToTime(
|
||||
day: Int,
|
||||
from: Int,
|
||||
to: Int,
|
||||
rooms: List<Room>,
|
||||
schedules: Map<Room, Map<Int, Schedule>>,
|
||||
allowEdit: Boolean
|
||||
) {
|
||||
val gridLabelWidth = 60
|
||||
val minutesOfDay = to - from
|
||||
|
||||
div("calendar-table-room-to-time") {
|
||||
div("calendar-header") {
|
||||
div("calendar-cell") {
|
||||
span {
|
||||
+"Room"
|
||||
}
|
||||
}
|
||||
|
||||
for (room in rooms) {
|
||||
div("calendar-cell") {
|
||||
span {
|
||||
+room.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until minutesOfDay / CALENDAR_GRID_WIDTH) {
|
||||
val time = ((i * CALENDAR_GRID_WIDTH + from) % MINUTES_OF_DAY).let {
|
||||
if (it < 0) it + MINUTES_OF_DAY else it
|
||||
}
|
||||
val minutes = (time % 60).toString().padStart(2, '0')
|
||||
val hours = (time / 60).toString().padStart(2, '0')
|
||||
val timeString = "$hours:$minutes"
|
||||
|
||||
val start = i * CALENDAR_GRID_WIDTH + from
|
||||
val end = (i + 1) * CALENDAR_GRID_WIDTH + from - 1
|
||||
|
||||
div("calendar-row") {
|
||||
div("calendar-cell") {
|
||||
if (time % gridLabelWidth == 0) {
|
||||
span {
|
||||
+timeString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (room in rooms) {
|
||||
div("calendar-cell") {
|
||||
attributes["data-time"] = time.toString()
|
||||
attributes["data-room"] = room.id.toString()
|
||||
attributes["data-day"] = day.toString()
|
||||
title = timeString
|
||||
|
||||
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
|
||||
|
||||
calendarCell(schedule, day, time)
|
||||
|
||||
val href = if (allowEdit) "/calendar/$day/${room.id}/$start" else null
|
||||
a(href, classes = "calendar-link")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.calendar() {
|
||||
get<LocationCalendar> {
|
||||
|
||||
get("/calendar") {
|
||||
call.respondRedirect("/calendar/0", true)
|
||||
}
|
||||
|
||||
get<LocationCalendar.LocationCalendarRoomToTime> { param ->
|
||||
call.response.cookies.append(
|
||||
"orientation",
|
||||
CalendarOrientation.ROOM_TO_TIME.name,
|
||||
maxAge = Int.MAX_VALUE,
|
||||
path = "/"
|
||||
)
|
||||
call.respondRedirect("/calendar/${param.day}")
|
||||
}
|
||||
|
||||
get<LocationCalendar.LocationCalendarTimeToRoom> { param ->
|
||||
call.response.cookies.append(
|
||||
"orientation",
|
||||
CalendarOrientation.TIME_TO_ROOM.name,
|
||||
maxAge = Int.MAX_VALUE,
|
||||
path = "/"
|
||||
)
|
||||
call.respondRedirect("/calendar/${param.day}")
|
||||
}
|
||||
|
||||
get<LocationCalendar> { param ->
|
||||
val user = call.sessions.get<PortalSession>()?.getUser(call)
|
||||
val allowEdit = user?.checkPermission(Permission.SCHEDULE) ?: false
|
||||
val rooms = Room.list()
|
||||
|
||||
val orientation = call.request.cookies["orientation"]?.let { name ->
|
||||
CalendarOrientation.values().find { it.name == name }
|
||||
} ?: CalendarOrientation.ROOM_TO_TIME
|
||||
|
||||
val day = param.day
|
||||
val h = Schedule.getByDay(day)
|
||||
val schedules = h.groupBy { it.room }.mapValues { (_, it) ->
|
||||
it.associateBy {
|
||||
it.time
|
||||
}
|
||||
}
|
||||
|
||||
var max = 0
|
||||
var min = 24 * 60
|
||||
for (s in h) {
|
||||
max = max(max, s.time + s.workGroup.length)
|
||||
min = min(min, s.time)
|
||||
}
|
||||
|
||||
if (min > max) {
|
||||
val h1 = max
|
||||
max = min
|
||||
min = h1
|
||||
}
|
||||
|
||||
min = (min / 60 - 1) * 60
|
||||
max = (max / 60 + 2) * 60
|
||||
|
||||
min = min(min, 0)
|
||||
|
||||
call.respondHtmlTemplate(MainTemplate()) {
|
||||
menuTemplate {
|
||||
this.user = user
|
||||
active = MenuTemplate.Tab.CALENDAR
|
||||
}
|
||||
content {
|
||||
h1 { +"Calendar" }
|
||||
if (rooms.isEmpty()) {
|
||||
return@content
|
||||
}
|
||||
|
||||
div("header") {
|
||||
div("header-left") {
|
||||
a("/calendar/${day - 1}") { +"<" }
|
||||
span {
|
||||
+"Day $day"
|
||||
}
|
||||
a("/calendar/${day + 1}") { +">" }
|
||||
}
|
||||
div("header-right") {
|
||||
a("/calendar/$day/room-to-time") {
|
||||
+"Room to time"
|
||||
}
|
||||
a("/calendar/$day/time-to-room") {
|
||||
+"Time to room"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div("calendar") {
|
||||
attributes["data-day"] = day.toString()
|
||||
|
||||
div {
|
||||
when (orientation) {
|
||||
CalendarOrientation.ROOM_TO_TIME -> renderRoomToTime(
|
||||
day,
|
||||
min,
|
||||
max,
|
||||
rooms,
|
||||
schedules,
|
||||
allowEdit
|
||||
)
|
||||
CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom(
|
||||
day,
|
||||
min,
|
||||
max,
|
||||
rooms,
|
||||
schedules,
|
||||
allowEdit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
get<LocationCalendar.LocationCalendarEdit> { param ->
|
||||
val user = call.sessions.get<PortalSession>()?.getUser(call)
|
||||
if (user == null || !user.checkPermission(Permission.SCHEDULE)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
val list = WorkGroup.list()
|
||||
val room = Room.get(param.room)
|
||||
val day = param.day
|
||||
val time = param.time
|
||||
|
||||
call.respondHtmlTemplate(MainTemplate()) {
|
||||
menuTemplate {
|
||||
this.user = user
|
||||
active = MenuTemplate.Tab.WORK_GROUP
|
||||
}
|
||||
content {
|
||||
h1 { +"Select work groups" }
|
||||
insert(TableTemplate()) {
|
||||
searchValue = param.search
|
||||
|
||||
action {
|
||||
a("/calendar/$day/${room.id}/$time/-1") {
|
||||
button(classes = "form-btn btn-primary") {
|
||||
+"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
th {
|
||||
+"Name"
|
||||
}
|
||||
th {
|
||||
+"Interested"
|
||||
}
|
||||
th {
|
||||
+"Track"
|
||||
}
|
||||
th {
|
||||
+"Projector"
|
||||
}
|
||||
th {
|
||||
+"Resolution"
|
||||
}
|
||||
th {
|
||||
+"Length"
|
||||
}
|
||||
}
|
||||
|
||||
for (u in list) {
|
||||
if (Search.match(param.search, u.name)) {
|
||||
val href = "/calendar/$day/${room.id}/$time/${u.id}"
|
||||
entry {
|
||||
attributes["data-search"] = Search.pack(u.name)
|
||||
td {
|
||||
a(href) {
|
||||
+u.name
|
||||
}
|
||||
}
|
||||
td {
|
||||
+u.interested.toString()
|
||||
}
|
||||
td {
|
||||
+(u.track?.name ?: "")
|
||||
}
|
||||
td {
|
||||
+u.projector.toString()
|
||||
}
|
||||
td {
|
||||
+u.resolution.toString()
|
||||
}
|
||||
td {
|
||||
+u.length.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get<LocationCalendar.LocationCalendarSet> { param ->
|
||||
val user = call.sessions.get<PortalSession>()?.getUser(call)
|
||||
if (user == null || !user.checkPermission(Permission.SCHEDULE)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
for (it in Schedule.getByRoom(param.room, param.day, param.time)) {
|
||||
PushService.notify(
|
||||
MessageDeleteCalendarEntry(
|
||||
it.day,
|
||||
it.time,
|
||||
it.room.id,
|
||||
it.workGroup.id
|
||||
)
|
||||
)
|
||||
it.delete()
|
||||
}
|
||||
if (param.workGroup >= 0) {
|
||||
val schedule = Schedule(param.workGroup, param.day, param.time, param.room)
|
||||
schedule.save()
|
||||
schedule.loadConstraints()
|
||||
|
||||
val cellTime = (schedule.time / 15) * 15
|
||||
|
||||
PushService.notify(
|
||||
MessageCreateCalendarEntry(
|
||||
schedule.day,
|
||||
schedule.time,
|
||||
cellTime,
|
||||
schedule.room.id,
|
||||
schedule.workGroup.id,
|
||||
schedule.workGroup.name,
|
||||
schedule.workGroup.length,
|
||||
schedule.workGroup.language.code,
|
||||
schedule.workGroup.track?.color
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
call.respondRedirect(param.next ?: "/calendar/${param.day}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CalendarOrientation {
|
||||
/**
|
||||
* Columns contains time
|
||||
* Rows contains rooms
|
||||
*
|
||||
* Like the old kif tool
|
||||
*/
|
||||
TIME_TO_ROOM,
|
||||
|
||||
/**
|
||||
* Columns contains rooms
|
||||
* Rows contains time
|
||||
*
|
||||
* Like the congress schedule
|
||||
*/
|
||||
ROOM_TO_TIME
|
||||
}
|
||||
|
|
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)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
val editRoom = Room.get(roomId.id) ?: return@get
|
||||
val editRoom = Room.get(roomId.id)
|
||||
call.respondHtmlTemplate(MainTemplate()) {
|
||||
menuTemplate {
|
||||
this.user = user
|
||||
|
@ -183,7 +183,7 @@ fun Route.room() {
|
|||
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
||||
list.firstOrNull()
|
||||
}
|
||||
val editRoom = Room.get(roomId.id) ?: return@post
|
||||
val editRoom = Room.get(roomId.id)
|
||||
|
||||
params["name"]?.let { editRoom.name = it }
|
||||
params["places"]?.let { editRoom.places = it.toIntOrNull() ?: 0 }
|
||||
|
@ -298,7 +298,7 @@ fun Route.room() {
|
|||
if (user == null || !user.checkPermission(Permission.ROOM)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
val deleteRoom = Room.get(roomId.id) ?: return@get
|
||||
val deleteRoom = Room.get(roomId.id)
|
||||
|
||||
deleteRoom.delete()
|
||||
|
||||
|
|
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.LocationWorkGroup
|
||||
import de.kif.backend.PortalSession
|
||||
import de.kif.backend.database.Language
|
||||
import de.kif.backend.model.Permission
|
||||
import de.kif.backend.model.Track
|
||||
import de.kif.backend.model.WorkGroup
|
||||
import de.kif.backend.util.Search
|
||||
import de.kif.backend.view.MainTemplate
|
||||
|
@ -41,6 +43,11 @@ fun Route.workGroup() {
|
|||
searchValue = param.search
|
||||
|
||||
action {
|
||||
a("/track") {
|
||||
button(classes = "form-btn") {
|
||||
+"Edit tracks"
|
||||
}
|
||||
}
|
||||
a("/workgroup/new") {
|
||||
button(classes = "form-btn btn-primary") {
|
||||
+"Add work group"
|
||||
|
@ -67,6 +74,9 @@ fun Route.workGroup() {
|
|||
th {
|
||||
+"Length"
|
||||
}
|
||||
th {
|
||||
+"Language"
|
||||
}
|
||||
th(classes = "action") {
|
||||
+"Action"
|
||||
}
|
||||
|
@ -83,7 +93,7 @@ fun Route.workGroup() {
|
|||
+u.interested.toString()
|
||||
}
|
||||
td {
|
||||
+u.trackId.toString()
|
||||
+(u.track?.name ?: "")
|
||||
}
|
||||
td {
|
||||
+u.projector.toString()
|
||||
|
@ -94,9 +104,12 @@ fun Route.workGroup() {
|
|||
td {
|
||||
+u.length.toString()
|
||||
}
|
||||
td {
|
||||
+u.language.toString()
|
||||
}
|
||||
td(classes = "action") {
|
||||
a("/workgroup/${u.id}") {
|
||||
i("material-icons") { +"edit" }
|
||||
i("material-icons") { +"edit" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +126,8 @@ fun Route.workGroup() {
|
|||
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get
|
||||
val editWorkGroup = WorkGroup.get(workGroupId.id)
|
||||
val tracks = Track.list()
|
||||
call.respondHtmlTemplate(MainTemplate()) {
|
||||
menuTemplate {
|
||||
this.user = user
|
||||
|
@ -158,16 +172,29 @@ fun Route.workGroup() {
|
|||
htmlFor = "track"
|
||||
+"Track"
|
||||
}
|
||||
input(
|
||||
name = "track",
|
||||
classes = "form-control",
|
||||
type = InputType.number
|
||||
) {
|
||||
id = "track"
|
||||
value = editWorkGroup.trackId.toString()
|
||||
div("input-group") {
|
||||
select(
|
||||
classes = "form-control"
|
||||
) {
|
||||
name = "track"
|
||||
|
||||
min = "0"
|
||||
max = "1337"
|
||||
option {
|
||||
selected = (editWorkGroup.trackId ?: -1) < 0
|
||||
value = "-1"
|
||||
+"None"
|
||||
}
|
||||
for (track in tracks) {
|
||||
option {
|
||||
selected = editWorkGroup.trackId == track.id
|
||||
value = track.id.toString()
|
||||
+track.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a("/track", classes = "form-btn") {
|
||||
i("material-icons") { +"edit" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
|
@ -185,6 +212,26 @@ fun Route.workGroup() {
|
|||
|
||||
min = "0"
|
||||
max = "1440"
|
||||
step = "5"
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "language"
|
||||
+"Language"
|
||||
}
|
||||
select(
|
||||
classes = "form-control"
|
||||
) {
|
||||
name = "language"
|
||||
|
||||
for (language in Language.values()) {
|
||||
option {
|
||||
selected = editWorkGroup.language == language
|
||||
value = language.name
|
||||
+language.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,7 +295,7 @@ fun Route.workGroup() {
|
|||
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
||||
list.firstOrNull()
|
||||
}
|
||||
val editWorkGroup = WorkGroup.get(workGroupId.id) ?: return@post
|
||||
val editWorkGroup = WorkGroup.get(workGroupId.id)
|
||||
|
||||
params["name"]?.let { editWorkGroup.name = it }
|
||||
params["interested"]?.toIntOrNull()?.let { editWorkGroup.interested = it }
|
||||
|
@ -256,6 +303,7 @@ fun Route.workGroup() {
|
|||
params["projector"]?.let { editWorkGroup.projector = it == "on" }
|
||||
params["resolution"]?.let { editWorkGroup.resolution = it == "on" }
|
||||
params["length"]?.toIntOrNull()?.let { editWorkGroup.length = it }
|
||||
params["language"]?.let { editWorkGroup.language = Language.values().find { l -> l.name == it } ?: Language.GERMAN }
|
||||
|
||||
editWorkGroup.save()
|
||||
|
||||
|
@ -268,6 +316,7 @@ fun Route.workGroup() {
|
|||
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
val tracks = Track.list()
|
||||
call.respondHtmlTemplate(MainTemplate()) {
|
||||
menuTemplate {
|
||||
this.user = user
|
||||
|
@ -312,16 +361,22 @@ fun Route.workGroup() {
|
|||
htmlFor = "track"
|
||||
+"Track"
|
||||
}
|
||||
input(
|
||||
name = "track",
|
||||
classes = "form-control",
|
||||
type = InputType.number
|
||||
select(
|
||||
classes = "form-control"
|
||||
) {
|
||||
id = "track"
|
||||
value = ""
|
||||
name = "track"
|
||||
|
||||
min = "0"
|
||||
max = "1337"
|
||||
option {
|
||||
selected = true
|
||||
value = "-1"
|
||||
+"None"
|
||||
}
|
||||
for (track in tracks) {
|
||||
option {
|
||||
value = track.id.toString()
|
||||
+track.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
|
@ -339,6 +394,26 @@ fun Route.workGroup() {
|
|||
|
||||
min = "0"
|
||||
max = "1440"
|
||||
step = "5"
|
||||
}
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
htmlFor = "language"
|
||||
+"Language"
|
||||
}
|
||||
select(
|
||||
classes = "form-control"
|
||||
) {
|
||||
name = "language"
|
||||
|
||||
for (language in Language.values()) {
|
||||
option {
|
||||
selected = language == Language.GERMAN
|
||||
value = language.name
|
||||
+language.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,6 +479,7 @@ fun Route.workGroup() {
|
|||
val projector = params["projector"] == "on"
|
||||
val resolution = params["resolution"] == "on"
|
||||
val length = (params["length"] ?: return@post).toIntOrNull() ?: 0
|
||||
val language = (params["language"] ?: return@post).let { Language.values().find { l -> l.name == it } ?: Language.GERMAN }
|
||||
|
||||
WorkGroup(
|
||||
name = name,
|
||||
|
@ -411,7 +487,8 @@ fun Route.workGroup() {
|
|||
trackId = trackId,
|
||||
projector = projector,
|
||||
resolution = resolution,
|
||||
length = length
|
||||
length = length,
|
||||
language = language
|
||||
).save()
|
||||
|
||||
call.respondRedirect("/workgroup")
|
||||
|
@ -423,7 +500,7 @@ fun Route.workGroup() {
|
|||
if (user == null || !user.checkPermission(Permission.WORK_GROUP)) {
|
||||
call.respondRedirect("/login?${LocationLogin::next.name}=${call.request.path()}}")
|
||||
} else {
|
||||
val deleteWorkGroup = WorkGroup.get(workGroupId.id) ?: return@get
|
||||
val deleteWorkGroup = WorkGroup.get(workGroupId.id)
|
||||
|
||||
deleteWorkGroup.delete()
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package de.kif.backend.view
|
||||
|
||||
import de.kif.backend.Resources
|
||||
import io.ktor.html.Placeholder
|
||||
import io.ktor.html.Template
|
||||
import io.ktor.html.TemplatePlaceholder
|
||||
|
@ -24,12 +25,12 @@ class MainTemplate : Template<HTML> {
|
|||
|
||||
script(src = "/static/require.min.js") {}
|
||||
|
||||
/*script {
|
||||
script {
|
||||
unsafe {
|
||||
+"require.config({baseUrl: '/static'});\n"
|
||||
+("require([${Resources.jsModules}]);\n")
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
body {
|
||||
insert(MenuTemplate(), menuTemplate)
|
||||
|
|
Loading…
Reference in a new issue