diff --git a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt index de7761d..a188dc7 100644 --- a/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt +++ b/src/commonMain/kotlin/de/kif/common/ConstraintChecking.kt @@ -40,39 +40,80 @@ fun checkConstraints( errors += ConstraintError("Work group requires accessible, but room does not have one!") } - for (constraint in schedule.workGroup.constraints) { - when (constraint.type) { + for ((type, constraints) in schedule.workGroup.constraints.groupBy { it.type }) { + when (type) { ConstraintType.OnlyOnDay -> { - if (constraint.number.toInt() != schedule.day) { - errors += ConstraintError("Work group requires day ${constraint.number}, but is on ${schedule.day}!") + val onlyOnDay = constraints.map { it.day == schedule.day } + if (!onlyOnDay.any()) { + val dayList = constraints.mapNotNull { it.day }.distinct().sorted() + errors += ConstraintError("Work group requires days $dayList, but is on ${schedule.day}!") } } - ConstraintType.OnlyAfterTime -> { - if (constraint.number.toInt() > schedule.time) { - errors += ConstraintError("Work group requires time after ${constraint.number}, but is on ${schedule.time}!") + + ConstraintType.NotOnDay -> { + val notOnDay = constraints.map { it.day == schedule.day } + if (notOnDay.any()) { + val dayList = constraints.mapNotNull { it.day }.distinct().sorted() + errors += ConstraintError("Work group requires not days $dayList, but is on ${schedule.day}!") } } - ConstraintType.NotAtSameTime -> { - val start = schedule.getAbsoluteStartTime() - val end = schedule.getAbsoluteEndTime() - for (s in against) { - if ( - s.workGroup.id == constraint.number && - start <= s.getAbsoluteEndTime() && - s.getAbsoluteStartTime() <= end - ) { - errors += ConstraintError("Work group requires not same time with ${s.workGroup.name}!") + + ConstraintType.OnlyBeforeTime -> { + for (it in constraints) { + if (it.time == null) continue + if (it.day == null) { + if (it.time > schedule.time) { + errors += ConstraintError("Work group requires before time ${it.time}, but is on ${schedule.time}!") + } + } else { + if (it.day == schedule.day && it.time > schedule.time) { + errors += ConstraintError("Work group requires before time ${it.time} on day ${it.day}, but is on ${schedule.time}!") + } } } } + + ConstraintType.OnlyAfterTime -> { + for (it in constraints) { + if (it.time == null) continue + if (it.day == null) { + if (it.time < schedule.time) { + errors += ConstraintError("Work group requires after time ${it.time}, but is on ${schedule.time}!") + } + } else { + if (it.day == schedule.day && it.time < schedule.time) { + errors += ConstraintError("Work group requires after time ${it.time} on day ${it.day}, but is on ${schedule.time}!") + } + } + } + } + + ConstraintType.NotAtSameTime -> { + val start = schedule.getAbsoluteStartTime() + val end = schedule.getAbsoluteEndTime() + for (constraint in constraints) { + for (s in against) { + if ( + s.workGroup.id == constraint.workGroup && + start <= s.getAbsoluteEndTime() && + s.getAbsoluteStartTime() <= end + ) { + errors += ConstraintError("Work group requires not same time with ${s.workGroup.name}!") + } + } + } + } + ConstraintType.OnlyAfterWorkGroup -> { val start = schedule.getAbsoluteStartTime() - for (s in against) { - if ( - s.workGroup.id == constraint.number && - s.getAbsoluteEndTime() > start - ) { - errors += ConstraintError("Work group requires after ${s.workGroup.name}!") + for (constraint in constraints) { + for (s in against) { + if ( + s.workGroup.id == constraint.workGroup && + s.getAbsoluteEndTime() > start + ) { + errors += ConstraintError("Work group requires after ${s.workGroup.name}!") + } } } } diff --git a/src/commonMain/kotlin/de/kif/common/model/Constraint.kt b/src/commonMain/kotlin/de/kif/common/model/Constraint.kt index 2bb39f3..e94a140 100644 --- a/src/commonMain/kotlin/de/kif/common/model/Constraint.kt +++ b/src/commonMain/kotlin/de/kif/common/model/Constraint.kt @@ -5,9 +5,40 @@ import kotlinx.serialization.Serializable @Serializable data class Constraint( val type: ConstraintType, - val number: Long + val day: Int? = null, + val time: Int? = null, + val workGroup: Long? = null ) enum class ConstraintType { - OnlyOnDay, OnlyAfterTime, NotAtSameTime, OnlyAfterWorkGroup + + /** + * Requires day, permits time and workGroup. + */ + OnlyOnDay, + + /** + * Requires day, permits time and workGroup. + */ + NotOnDay, + + /** + * Requires time, optionally allows day, permits workGroup. + */ + OnlyAfterTime, + + /** + * Requires time, optionally allows day, permits workGroup. + */ + OnlyBeforeTime, + + /** + * Requires workGroup, permits day and time. + */ + NotAtSameTime, + + /** + * Requires workGroup, permits day and time. + */ + OnlyAfterWorkGroup } diff --git a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt index 3cb77c4..bdd6fed 100644 --- a/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt +++ b/src/commonMain/kotlin/de/kif/common/model/WorkGroup.kt @@ -18,6 +18,7 @@ data class WorkGroup( val accessible: Boolean, val length: Int, val language: Language, + val leader: List, val constraints: List, override val createdAt: Long = 0, override val updateAt: Long = 0 diff --git a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt index 2183bd9..23342cb 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/WorkGroupConstraints.kt @@ -51,6 +51,44 @@ fun initWorkGroupConstraints() { }.html) } } + addList.textView("Add not on day") { + onClick { + constraints.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("Not day").apply { + classList += "form-btn" + onClick { this@wrap.html.remove() } + }.html) + html.appendChild(InputView(InputType.NUMBER).apply { + classList += "form-control" + html.name = "constraint-not-on-day-${index++}" + min = -1337.0 + max = 1337.0 + }.html) + }.html) + } + } + addList.textView("Add only before time") { + onClick { + constraints.appendChild(View.wrap(createHtmlView()) { + classList += "input-group" + html.appendChild(TextView("Before time").apply { + classList += "form-btn" + onClick { this@wrap.html.remove() } + }.html) + html.appendChild(InputView(InputType.TEXT).apply { + classList += "form-control" + html.name = "constraint-only-before-time-day-${index++}" + }.html) + html.appendChild(InputView(InputType.NUMBER).apply { + classList += "form-control" + html.name = "constraint-only-before-time-${index++}" + min = -1337.0 + max = 133700.0 + }.html) + }.html) + } + } addList.textView("Add only after time") { onClick { constraints.appendChild(View.wrap(createHtmlView()) { @@ -59,6 +97,10 @@ fun initWorkGroupConstraints() { classList += "form-btn" onClick { this@wrap.html.remove() } }.html) + html.appendChild(InputView(InputType.TEXT).apply { + classList += "form-control" + html.name = "constraint-only-after-time-day-${index++}" + }.html) html.appendChild(InputView(InputType.NUMBER).apply { classList += "form-control" html.name = "constraint-only-after-time-${index++}" @@ -125,7 +167,6 @@ fun initWorkGroupConstraints() { } } - console.log(constraints) for (child in constraints.children.iterator()) { console.log(child) if (child.classList.contains("input-group")) { diff --git a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt index 3ef51cc..1a66c49 100644 --- a/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt +++ b/src/jsMain/kotlin/de/kif/frontend/views/table/WorkGroupTableLine.kt @@ -15,7 +15,7 @@ import org.w3c.dom.get class WorkGroupTableLine(view: HTMLElement) : TableLine(view) { - var lineId = dataset["id"]?.toLongOrNull() ?: -1 + private var lineId = dataset["id"]?.toLongOrNull() ?: -1 private val workGroup = RepositoryDelegate(WorkGroupRepository, lineId) diff --git a/src/jsMain/resources/style/components/_form.scss b/src/jsMain/resources/style/components/_form.scss index 8ccf375..2227c4a 100644 --- a/src/jsMain/resources/style/components/_form.scss +++ b/src/jsMain/resources/style/components/_form.scss @@ -12,6 +12,7 @@ margin: 1px; transition: border-color $transitionTime; color: var(--text-primary-color); + min-width: 0; &:focus { border-color: var(--primary-color); @@ -41,6 +42,16 @@ select:-moz-focusring { padding-bottom: 0.3rem; padding-left: 0.2rem; } + + &:after { + content: attr(data-hint); + position: absolute; + left: 100%; + color: var(--text-secondary-color); + line-height: 2.5rem; + margin-left: 1rem; + width: 100%; + } } .form-switch { @@ -177,7 +188,7 @@ form { & > * { margin-right: 0; flex-grow: 1; - flex-shrink: 1; + flex-basis: 0; &:not(:first-child) { border-top-left-radius: 0; diff --git a/src/jsMain/resources/style/components/_table-layout.scss b/src/jsMain/resources/style/components/_table-layout.scss index 3b9e16f..d6e6d52 100644 --- a/src/jsMain/resources/style/components/_table-layout.scss +++ b/src/jsMain/resources/style/components/_table-layout.scss @@ -90,7 +90,9 @@ margin-bottom: 0.5rem; span { - width: 4rem; + width: 8rem; + flex-basis: 8rem; + flex-grow: 0; position: relative; text-align: center; overflow: hidden; diff --git a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt index c22f6dc..0ed1351 100644 --- a/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt +++ b/src/jvmMain/kotlin/de/kif/backend/database/Schema.kt @@ -29,6 +29,7 @@ object DbWorkGroup : Table() { val accessible = bool("accessible") val language = enumeration("language", Language::class) + val leader = text("leader").default("[]") val length = integer("length") val constraints = text("constraints") diff --git a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt index 8b3e5c3..a4ef155 100644 --- a/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt +++ b/src/jvmMain/kotlin/de/kif/backend/repository/WorkGroupRepository.kt @@ -12,6 +12,7 @@ import de.kif.common.model.WorkGroup import de.westermann.kobserve.event.EventHandler import kotlinx.coroutines.runBlocking import kotlinx.serialization.list +import kotlinx.serialization.serializer import org.jetbrains.exposed.sql.* import java.util.Date @@ -35,6 +36,7 @@ object WorkGroupRepository : Repository { val accessible = row[DbWorkGroup.accessible] val length = row[DbWorkGroup.length] val language = row[DbWorkGroup.language] + val leader = Message.json.parse(String.serializer().list, row[DbWorkGroup.leader]) val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints]) val createdAt = row[DbWorkGroup.createdAt] @@ -56,6 +58,7 @@ object WorkGroupRepository : Repository { accessible, length, language, + leader, constraints, createdAt, updatedAt @@ -89,6 +92,7 @@ object WorkGroupRepository : Repository { it[accessible] = model.accessible it[length] = model.length it[language] = model.language + it[leader] = Message.json.stringify(String.serializer().list, model.leader) it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[createdAt] = now @@ -120,6 +124,7 @@ object WorkGroupRepository : Repository { it[accessible] = model.accessible it[length] = model.length it[language] = model.language + it[leader] = Message.json.stringify(String.serializer().list, model.leader) it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints) it[updatedAt] = now diff --git a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt index 5b5a2a4..8af9788 100644 --- a/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/route/WorkGroup.kt @@ -3,15 +3,12 @@ package de.kif.backend.route import de.kif.backend.authenticateOrRedirect import de.kif.backend.repository.TrackRepository import de.kif.backend.repository.WorkGroupRepository -import de.kif.backend.view.MainTemplate -import de.kif.backend.view.MenuTemplate import de.kif.backend.view.TableTemplate import de.kif.backend.view.respondMain import de.kif.common.Search import de.kif.common.model.* import io.ktor.application.call import io.ktor.html.insert -import io.ktor.html.respondHtmlTemplate import io.ktor.request.receiveParameters import io.ktor.response.respondRedirect import io.ktor.routing.Route @@ -23,6 +20,8 @@ import kotlinx.css.Display import kotlinx.html.* import kotlin.collections.set +private const val separator = "###" + fun Route.workGroup() { get("workgroups") { authenticateOrRedirect(Permission.WORK_GROUP) { user -> @@ -152,11 +151,7 @@ fun Route.workGroup() { val tracks = TrackRepository.all() val workGroups = editWorkGroup.constraints.mapNotNull { - when (it.type) { - ConstraintType.NotAtSameTime -> it.number - ConstraintType.OnlyAfterWorkGroup -> it.number - else -> null - } + it.workGroup }.distinct().associateWith { WorkGroupRepository.get(it)!! } @@ -270,6 +265,24 @@ fun Route.workGroup() { } } + div("form-group") { + attributes["data-hint"] = "Use '$separator' to separate multiple leaders" + + label { + htmlFor = "leader" + +"Leader" + } + input( + name = "leader", + classes = "form-control" + ) { + id = "leader" + placeholder = "Leader" + + value = editWorkGroup.leader.joinToString(" $separator ") + } + } + div("form-switch-group") { div("form-group form-switch") { input( @@ -383,22 +396,66 @@ fun Route.workGroup() { classes = "form-control", type = InputType.number ) { - value = constraint.number.toString() + value = constraint.day.toString() min = "-1337" max = "1337" } } + ConstraintType.NotOnDay -> { + span("form-btn") { + +"Not day" + } + input( + name = "constraint-not-on-day-$index", + classes = "form-control", + type = InputType.number + ) { + value = constraint.day.toString() + + min = "-1337" + max = "1337" + } + } + ConstraintType.OnlyBeforeTime -> { + span("form-btn") { + +"Before time" + } + input( + name = "constraint-only-before-time-day-$index", + classes = "form-control" + ) { + value = constraint.day?.toString() ?: "" + placeholder = "day" + } + input( + name = "constraint-only-before-time-$index", + classes = "form-control", + type = InputType.number + ) { + value = constraint.time.toString() + + min = "-1337" + max = "133700" + } + } ConstraintType.OnlyAfterTime -> { span("form-btn") { +"After time" } + input( + name = "constraint-only-after-time-day-$index", + classes = "form-control" + ) { + value = constraint.day?.toString() ?: "" + placeholder = "day" + } input( name = "constraint-only-after-time-$index", classes = "form-control", type = InputType.number ) { - value = constraint.number.toString() + value = constraint.time.toString() min = "-1337" max = "133700" @@ -415,8 +472,8 @@ fun Route.workGroup() { option { selected = true - value = constraint.number.toString() - +(workGroups[constraint.number]?.name ?: "") + value = constraint.workGroup.toString() + +(workGroups[constraint.workGroup!!]?.name ?: "") } } } @@ -431,8 +488,8 @@ fun Route.workGroup() { option { selected = true - value = constraint.number.toString() - +(workGroups[constraint.number]?.name ?: "") + value = constraint.workGroup.toString() + +(workGroups[constraint.workGroup!!]?.name ?: "") } } } @@ -490,25 +547,13 @@ fun Route.workGroup() { editWorkGroup = editWorkGroup.copy(language = Language.values().find { l -> l.code == it } ?: Language.GERMAN) } - - val constraints = params.mapNotNull { (key, value) -> - when { - key.startsWith("constraint-only-on-day") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyOnDay, it) } - } - key.startsWith("constraint-only-after-time") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, it) } - } - key.startsWith("constraint-not-at-same-time") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.NotAtSameTime, it) } - } - key.startsWith("constraint-only-after-work-group") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterWorkGroup, it) } - } - else -> null - } + params["leader"]?.let { + val leader = it.split("\\s*$separator+\\s*".toRegex()) + editWorkGroup = editWorkGroup.copy(leader = leader) } + val constraints = parseConstraintParam(params) + editWorkGroup = editWorkGroup.copy(constraints = constraints) WorkGroupRepository.update(editWorkGroup) @@ -628,6 +673,25 @@ fun Route.workGroup() { } } + div("form-group") { + attributes["data-hint"] = "Use '$separator' to separate multiple leaders" + + label { + htmlFor = "leader" + +"Leader" + } + input( + name = "leader", + classes = "form-control" + ) { + id = "leader" + placeholder = "Leader" + + value = "" + } + } + + div("form-switch-group") { div("form-group form-switch") { input( @@ -763,29 +827,14 @@ fun Route.workGroup() { val language = (params["language"] ?: return@post).let { Language.values().find { l -> l.code == it } ?: Language.GERMAN } + val leader = params["leader"]?.split("\\s*$separator+\\s*".toRegex()) ?: return@post val internet = params["internet"] == "on" val whiteboard = params["whiteboard"] == "on" val blackboard = params["blackboard"] == "on" val accessible = params["accessible"] == "on" - val constraints = params.mapNotNull { (key, value) -> - when { - key.startsWith("constraint-only-on-day") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyOnDay, it) } - } - key.startsWith("constraint-only-after-time") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, it) } - } - key.startsWith("constraint-not-at-same-time") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.NotAtSameTime, it) } - } - key.startsWith("constraint-only-after-work-group") -> { - value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterWorkGroup, it) } - } - else -> null - } - } + val constraints = parseConstraintParam(params) val workGroup = WorkGroup( null, @@ -801,6 +850,7 @@ fun Route.workGroup() { accessible = accessible, length = length, language = language, + leader = leader, constraints = constraints ) @@ -820,3 +870,63 @@ fun Route.workGroup() { } } } + +private fun parseConstraintParam(params: Map) = params.map { (key, value) -> + val id = key.substringAfterLast("-").toIntOrNull() ?: -1 + id to when { + key.startsWith("constraint-only-on-day") -> { + value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyOnDay, day = it) } + } + key.startsWith("constraint-not-on-day") -> { + value?.toIntOrNull()?.let { Constraint(ConstraintType.NotOnDay, day = it) } + } + key.startsWith("constraint-only-after-time") -> { + if ("day" in key) { + value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, day = it) } + } else { + value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyAfterTime, time = it) } + } + } + key.startsWith("constraint-only-before-time") -> { + if ("day" in key) { + value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, day = it) } + } else { + value?.toIntOrNull()?.let { Constraint(ConstraintType.OnlyBeforeTime, time = it) } + } + } + key.startsWith("constraint-not-at-same-time") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.NotAtSameTime, workGroup = it) } + } + key.startsWith("constraint-only-after-work-group") -> { + value?.toLongOrNull()?.let { Constraint(ConstraintType.OnlyAfterWorkGroup, workGroup = it) } + } + else -> null + } +}.groupBy({ it.first }) { + it.second +}.mapNotNull { (_, c) -> + when { + c.isEmpty() -> null + c.size == 1 -> c.first() + c.size == 2 -> { + val c1 = c[0] ?: return@mapNotNull null + val c2 = c[1] ?: return@mapNotNull null + + when { + c1.type != c2.type -> null + c1.type == ConstraintType.OnlyBeforeTime -> Constraint( + ConstraintType.OnlyBeforeTime, + day = c1.day ?: c2.day, + time = c1.time ?: c2.time + ) + c1.type == ConstraintType.OnlyAfterTime -> Constraint( + ConstraintType.OnlyAfterTime, + day = c1.day ?: c2.day, + time = c1.time ?: c2.time + ) + else -> null + } + } + else -> null + } +} diff --git a/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt b/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt index 9c0d72c..8a96cb1 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/Backup.kt @@ -49,25 +49,31 @@ data class Backup( suspend fun import(data: String) { val backup = Message.json.parse(serializer(), data) - val userMap = backup.users.associateWith { UserRepository.create(it) } - val postMap = backup.posts.associateWith { PostRepository.create(it) } + backup.users.forEach { UserRepository.create(it) } + backup.posts.forEach { PostRepository.create(it) } - val roomMap = backup.rooms.associateWith { RoomRepository.create(it) } - val trackMap = backup.tracks.associateWith { TrackRepository.create(it) } - val workGroupMap = backup.workGroups.associateWith { + backup.rooms.forEach { RoomRepository.create(it) } + val roomMap = RoomRepository.all().associateWith { it.id!! } + + backup.tracks.forEach { TrackRepository.create(it) } + val trackMap =TrackRepository.all().associateWith { it.id!! } + + backup.workGroups.forEach { var workGroup = it val track = workGroup.track if (track != null) { - workGroup = workGroup.copy(track = track.copy(id = trackMap[track] ?: return@associateWith -1L)) + workGroup = workGroup.copy(track = track.copy(id = trackMap[track] ?: return@forEach)) } WorkGroupRepository.create(workGroup) } - val scheduleMap = backup.schedules.associateWith { + val workGroupMap = WorkGroupRepository.all().associateWith { it.id!! } + + backup.schedules.forEach { ScheduleRepository.create( it.copy( - room = it.room.copy(id = roomMap[it.room] ?: return@associateWith -1L), - workGroup = it.workGroup.copy(id = workGroupMap[it.workGroup] ?: return@associateWith -1L) + room = it.room.copy(id = roomMap[it.room] ?: return@forEach), + workGroup = it.workGroup.copy(id = workGroupMap[it.workGroup] ?: return@forEach) ) ) } diff --git a/src/jvmMain/kotlin/de/kif/backend/util/WikiImporter.kt b/src/jvmMain/kotlin/de/kif/backend/util/WikiImporter.kt index cb0fb67..f9e4c0e 100644 --- a/src/jvmMain/kotlin/de/kif/backend/util/WikiImporter.kt +++ b/src/jvmMain/kotlin/de/kif/backend/util/WikiImporter.kt @@ -147,6 +147,7 @@ object WikiImporter { accessible = false, length = akLength, language = Language.GERMAN, + leader = listOf(who), constraints = emptyList() ) }