297 lines
9.1 KiB
Kotlin
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
|
|
}
|