portal/src/jvmMain/kotlin/de/kif/backend/route/Calendar.kt
2019-06-02 18:44:01 +02:00

297 lines
9.1 KiB
Kotlin

package de.kif.backend.route
import com.soywiz.klock.*
import com.soywiz.klock.locale.german
import de.kif.backend.Configuration
import de.kif.backend.isAuthenticated
import de.kif.backend.repository.RoomRepository
import de.kif.backend.repository.ScheduleRepository
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.respondMain
import de.kif.common.CALENDAR_GRID_WIDTH
import de.kif.common.model.Permission
import de.kif.common.model.Room
import de.kif.common.model.Schedule
import io.ktor.application.call
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import kotlinx.css.CSSBuilder
import kotlinx.css.Color
import kotlinx.css.pct
import kotlinx.css.rem
import kotlinx.html.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
const val MINUTES_OF_DAY = 24 * 60
private fun DIV.calendarCell(schedule: Schedule?) {
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.calcTextColor().toString())
}
}.toString()
attributes["data-language"] = schedule.workGroup.language.code
attributes["data-id"] = schedule.id.toString()
+schedule.workGroup.name
}
}
}
private fun DIV.renderCalendar(
orientation: CalendarOrientation,
day: Int,
from: Int,
to: Int,
rooms: List<Room>,
schedules: Map<Room, Map<Int, Schedule>>
) {
val gridLabelWidth = 60
val minutesOfDay = to - from
div("calendar-table-box ${orientation.name.toLowerCase().replace("_", "-")}") {
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"] = start.toString()
attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString()
title = room.name + " - " + timeString
val schedule = (start..end).mapNotNull { schedules[room]?.get(it) }.firstOrNull()
calendarCell(schedule)
}
}
}
}
}
}
fun Route.calendar() {
get("/calendar") {
call.respondRedirect("/calendar/0", true)
}
get("/calendar/{day}/rtt") {
call.response.cookies.append(
"orientation",
CalendarOrientation.ROOM_TO_TIME.name,
maxAge = Int.MAX_VALUE,
path = "/"
)
val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day")
}
get("/calendar/{day}/ttr") {
call.response.cookies.append(
"orientation",
CalendarOrientation.TIME_TO_ROOM.name,
maxAge = Int.MAX_VALUE,
path = "/"
)
val day = call.parameters["day"]?.toIntOrNull() ?: 0
call.respondRedirect("/calendar/$day")
}
get("/calendar/{day}") {
val user = isAuthenticated(Permission.SCHEDULE)
val editable = user != null
val day = call.parameters["day"]?.toIntOrNull() ?: return@get
val range = ScheduleRepository.getDayRange()
if (!editable && day !in range) {
return@get
}
val rooms = RoomRepository.all()
val orientation = call.request.cookies["orientation"]?.let { name ->
CalendarOrientation.values().find { it.name == name }
} ?: CalendarOrientation.ROOM_TO_TIME
val h = ScheduleRepository.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
}
if (editable) {
min = min(min, 0)
max = max(max, 24 * 60)
}
min = (min / 60 - 1) * 60
max = (max / 60 + 2) * 60
val refDate = DateTime(Configuration.Schedule.referenceDate.time)
val date = refDate + day.days
val dateString = DateFormat("EEEE, d. MMMM")
.withLocale(KlockLocale.german)
.format(date)
respondMain {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.CALENDAR
}
content {
if (rooms.isEmpty()) {
return@content
}
div("header") {
div("header-left") {
if (editable || day - 1 > range.start) {
a("/calendar/${day - 1}") { i("material-icons") { +"chevron_left" } }
}
span {
+dateString
}
if (editable || day + 1 < range.endInclusive) {
a("/calendar/${day + 1}") { i("material-icons") { +"chevron_right" } }
}
}
div("header-right") {
a("/calendar/$day/rtt", classes = "form-btn") {
+"Room to time"
}
a("/calendar/$day/ttr", classes = "form-btn") {
+"Time to room"
}
if (editable) {
button(classes = "form-btn") {
id = "calendar-check-constraints"
+"Check constraints"
}
button(classes = "form-btn") {
id = "calendar-edit-button"
+"Edit"
}
}
}
}
div("calendar") {
attributes["data-day"] = day.toString()
attributes["data-editable"] = editable.toString()
div("calendar-table") {
renderCalendar(
orientation,
day,
min,
max,
rooms,
schedules
)
}
if (editable) {
div("calendar-edit") {
div("calendar-edit-main") {
div("calendar-edit-search") {
input(InputType.search, name = "search", classes = "form-control") {
placeholder = "Search"
value = ""
}
}
div("calendar-edit-list") {
}
}
}
}
}
}
}
}
}
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
}