Add overview

This commit is contained in:
Lars Westermann 2019-05-24 14:29:10 +02:00
parent e88db9c75c
commit c28317aefd
Signed by: lars.westermann
GPG key ID: 9D417FA5BB9D5E1D
39 changed files with 1505 additions and 71 deletions

View file

@ -22,7 +22,8 @@ version "0.1.0"
repositories {
jcenter()
maven { url "http://dl.bintray.com/kotlin/ktor" }
maven { url "https://dl.bintray.com/kotlin/ktor" }
maven { url "https://dl.bintray.com/jetbrains/markdown" }
maven { url "https://kotlin.bintray.com/kotlinx" }
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
mavenCentral()
@ -87,6 +88,9 @@ kotlin {
implementation "de.westermann:KObserve-jvm:$observable_version"
//api 'org.jetbrains:markdown:0.1.28'
//implementation 'com.atlassian.commonmark:commonmark:0.12.1'
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
api 'io.github.microutils:kotlin-logging:1.6.23'
api 'ch.qos.logback:logback-classic:1.2.3'
api 'org.fusesource.jansi:jansi:1.8'

View file

@ -0,0 +1,60 @@
package de.kif.common
import de.kif.common.model.Model
import de.westermann.kobserve.event.EventHandler
class CacheRepository<T : Model>(val repository: Repository<T>) : Repository<T> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
var cache: Map<Long, T> = emptyMap()
var cacheComplete: Boolean = false
override suspend fun get(id: Long): T? {
return if (id in cache) {
cache[id]
} else {
val element = repository.get(id)
if (element != null) {
cache = cache + (id to element)
}
element
}
}
override suspend fun create(model: T): Long {
return repository.create(model)
}
override suspend fun update(model: T) {
val id = model.id
if (id != null) {
cache = cache - id
}
repository.update(model)
}
override suspend fun delete(id: Long) {
cache = cache - id
repository.delete(id)
}
override suspend fun all(): List<T> {
if (cacheComplete) {
return cache.values.toList()
} else {
val all = repository.all()
cache = all.associateBy { it.id!! }
cacheComplete = true
return all
}
}
}

View file

@ -0,0 +1,86 @@
package de.kif.common
import de.kif.common.model.ConstraintType
import de.kif.common.model.Schedule
import kotlinx.serialization.Serializable
@Serializable
data class ConstraintError(
val reason: String = ""
)
@Serializable
data class ConstraintMap(
val map: List<Pair<Long, List<ConstraintError>>>
)
fun checkConstraints(
check: List<Schedule>,
against: List<Schedule>
): ConstraintMap {
val map = mutableMapOf<Long, List<ConstraintError>>()
for (schedule in check) {
if (schedule.id == null) continue
val errors = mutableListOf<ConstraintError>()
if (schedule.workGroup.projector && !schedule.room.projector) {
errors += ConstraintError("Work group requires projector, but room does not have one!")
}
if (schedule.workGroup.internet && !schedule.room.internet) {
errors += ConstraintError("Work group requires internet, but room does not have one!")
}
if (schedule.workGroup.whiteboard && !schedule.room.whiteboard) {
errors += ConstraintError("Work group requires whiteboard, but room does not have one!")
}
if (schedule.workGroup.blackboard && !schedule.room.blackboard) {
errors += ConstraintError("Work group requires blackboard, but room does not have one!")
}
if (schedule.workGroup.accessible && !schedule.room.accessible) {
errors += ConstraintError("Work group requires accessible, but room does not have one!")
}
for (constraint in schedule.workGroup.constraints) {
when (constraint.type) {
ConstraintType.OnlyOnDay -> {
if (constraint.number.toInt() != schedule.day) {
errors += ConstraintError("Work group requires day ${constraint.number}, 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.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.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}!")
}
}
}
}
}
map[schedule.id] = errors
}
return ConstraintMap(map.map { it.toPair() })
}

View file

@ -31,5 +31,5 @@ enum class MessageType {
}
enum class RepositoryType {
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP
ROOM, SCHEDULE, TRACK, USER, WORK_GROUP, POST
}

View file

@ -0,0 +1,13 @@
package de.kif.common.model
import kotlinx.serialization.Serializable
@Serializable
data class Constraint(
val type: ConstraintType,
val number: Long
)
enum class ConstraintType {
OnlyOnDay, OnlyAfterTime, NotAtSameTime, OnlyAfterWorkGroup
}

View file

@ -3,5 +3,6 @@ package de.kif.common.model
import de.kif.common.SearchElement
interface Model {
val id : Long?
fun createSearch(): SearchElement
}

View file

@ -1,5 +1,5 @@
package de.kif.common.model
enum class Permission {
USER, SCHEDULE, WORK_GROUP, ROOM, PERSON, ADMIN
USER, SCHEDULE, WORK_GROUP, ROOM, POST, ADMIN
}

View file

@ -0,0 +1,29 @@
package de.kif.common.model
import de.kif.common.SearchElement
import kotlin.random.Random
data class Post(
override val id: Long? = null,
val name: String,
val content: String,
val url: String
) : Model {
override fun createSearch() = SearchElement(
mapOf(
"name" to name
)
)
companion object {
private const val chars = "abcdefghijklmnopqrstuvwxyz"
private const val length = 32
fun generateUrl() = (0 until length).asSequence()
.map { Random.nextInt(chars.length) }
.map { chars[it] }
.map {
if (Random.nextBoolean()) it else it.toUpperCase()
}.joinToString("")
}
}

View file

@ -5,10 +5,14 @@ import kotlinx.serialization.Serializable
@Serializable
data class Room(
val id: Long? = null,
override val id: Long? = null,
val name: String,
val places: Int,
val projector: Boolean
val projector: Boolean,
val internet: Boolean,
val whiteboard: Boolean,
val blackboard: Boolean,
val accessible: Boolean
) : Model {
override fun createSearch() = SearchElement(

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class Schedule(
val id: Long?,
override val id: Long?,
val workGroup: WorkGroup,
val room: Room,
val day: Int,
@ -22,4 +22,7 @@ data class Schedule(
"day" to day.toDouble()
)
)
fun getAbsoluteStartTime(): Int = day * 60 * 24 + time
fun getAbsoluteEndTime(): Int = getAbsoluteStartTime() + workGroup.length
}

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class Track(
val id: Long?,
override val id: Long?,
val name: String,
val color: Color
) : Model {

View file

@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Long?,
override val id: Long?,
val username: String,
val password: String,
val permissions: Set<Permission>

View file

@ -5,14 +5,19 @@ import kotlinx.serialization.Serializable
@Serializable
data class WorkGroup(
val id: Long?,
override val id: Long?,
val name: String,
val interested: Int,
val track: Track?,
val projector: Boolean,
val resolution: Boolean,
val internet: Boolean,
val whiteboard: Boolean,
val blackboard: Boolean,
val accessible: Boolean,
val length: Int,
val language: Language
val language: Language,
val constraints: List<Constraint>
) : Model {
override fun createSearch() = SearchElement(

View file

@ -2,6 +2,7 @@ package de.kif.frontend
import de.kif.frontend.views.calendar.initCalendar
import de.kif.frontend.views.initTableLayout
import de.kif.frontend.views.initWorkGroupConstraints
import de.westermann.kwebview.components.init
import kotlin.browser.document
@ -14,4 +15,7 @@ fun main() = init {
if (document.getElementsByClassName("table-layout").length > 0) {
initTableLayout()
}
if (document.getElementsByClassName("work-group-constraints").length > 0) {
initWorkGroupConstraints()
}
}

View file

@ -1,5 +1,6 @@
package de.kif.frontend.repository
import de.kif.common.ConstraintMap
import de.kif.common.Message
import de.kif.common.Repository
import de.kif.common.RepositoryType
@ -49,4 +50,14 @@ object ScheduleRepository : Repository<Schedule> {
override fun onDelete(id: Long) = onDelete.emit(id)
}
suspend fun checkConstraints(): ConstraintMap {
val json = repositoryGet("/api/constraints")
return parser.parse(json, ConstraintMap.serializer())
}
suspend fun checkConstraintsFor(schedule: Schedule): ConstraintMap {
val json = repositoryGet("/api/constraint/${schedule.id}")
return parser.parse(json, ConstraintMap.serializer())
}
}

View file

@ -0,0 +1,118 @@
package de.kif.frontend.views
import de.kif.frontend.launch
import de.kif.frontend.repository.WorkGroupRepository
import de.westermann.kobserve.event.EventListener
import de.westermann.kwebview.View
import de.westermann.kwebview.async
import de.westermann.kwebview.components.*
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.*
import kotlin.browser.document
fun initWorkGroupConstraints() {
var index = 10000
val constraints =
ListView.wrap<View>(document.getElementsByClassName("work-group-constraints")[0] as HTMLElement)
val addButton =
View.wrap(document.getElementsByClassName("work-group-constraints-add")[0] as HTMLElement)
val addList =
ListView.wrap<View>(document.getElementsByClassName("work-group-constraints-add-list")[0] as HTMLElement)
console.log(constraints.html)
console.log(addButton.html)
console.log(addList.html)
addButton.onClick {
addList.classList += "active"
var listener: EventListener<*>? = null
async {
listener = Body.onClick.reference {
addList.classList -= "active"
listener?.detach()
}
}
}
addList.textView("Add only on day") {
onClick {
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("On day").apply { classList += "form-btn" }.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-on-day-${index++}"
min = -1337.0
max = 1337.0
}.html)
}.html)
}
}
addList.textView("Add only after time") {
onClick {
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("After time").apply { classList += "form-btn" }.html)
html.appendChild(InputView(InputType.NUMBER).apply {
classList += "form-control"
html.name = "constraint-only-after-time-${index++}"
min = -1337.0
max = 133700.0
}.html)
}.html)
}
}
addList.textView("Add not at same time") {
onClick {
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("Not with").apply { classList += "form-btn" }.html)
val select = createHtmlView<HTMLSelectElement>()
select.classList.add("form-control")
select.name = "constraint-not-at-same-time-${index++}"
launch {
val all = WorkGroupRepository.all()
for (wg in all) {
val option = createHtmlView<HTMLOptionElement>()
option.value = wg.id.toString()
option.textContent = wg.name
select.appendChild(option)
}
}
html.appendChild(select)
}.html)
}
}
addList.textView("Add only after work group") {
onClick {
constraints.html.appendChild(View.wrap(createHtmlView<HTMLDivElement>()) {
classList += "input-group"
html.appendChild(TextView("After AK").apply { classList += "form-btn" }.html)
val select = createHtmlView<HTMLSelectElement>()
select.classList.add("form-control")
select.name = "constraint-only-after-work-group-${index++}"
launch {
val all = WorkGroupRepository.all()
for (wg in all) {
val option = createHtmlView<HTMLOptionElement>()
option.value = wg.id.toString()
option.textContent = wg.name
select.appendChild(option)
}
}
html.appendChild(select)
}.html)
}
}
}

View file

@ -4,6 +4,7 @@ import de.kif.frontend.iterator
import de.kif.frontend.launch
import de.kif.frontend.repository.ScheduleRepository
import de.westermann.kwebview.View
import de.westermann.kwebview.createHtmlView
import org.w3c.dom.*
import kotlin.browser.document
@ -30,7 +31,7 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
}
fun scrollHorizontalTo(pixel: Double) {
calendarTable.scrollTo (ScrollToOptions(pixel, 0.0, ScrollBehavior.SMOOTH))
calendarTable.scrollTo(ScrollToOptions(pixel, 0.0, ScrollBehavior.SMOOTH))
}
init {
@ -76,6 +77,27 @@ class Calendar(calendar: HTMLElement) : View(calendar) {
}
}
}
val cont = document.getElementsByClassName("header-right")[0] as HTMLElement
val view = View.wrap(createHtmlView<HTMLAnchorElement>())
cont.appendChild(view.html)
view.html.textContent = "Check"
view.onClick {
launch {
val errors = ScheduleRepository.checkConstraints()
println(errors)
for ((s, l) in errors.map) {
for (entry in calendarEntries) {
if (entry.scheduleId == s) {
entry.error = l.isNotEmpty()
}
}
}
}
}
}
}

View file

@ -31,6 +31,7 @@ class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(vi
private lateinit var workGroup: WorkGroup
var pending by classList.property("pending")
var error by classList.property("error")
private var nextScroll = 0.0
var editable: Boolean = false

View file

@ -144,6 +144,6 @@ abstract class View(view: HTMLElement = createHtmlView()) {
}
companion object {
fun wrap(htmlElement: HTMLElement) = object : View(htmlElement) {}
fun wrap(htmlElement: HTMLElement, init: View.() -> Unit = {}) = object : View(htmlElement) {}.also(init)
}
}

View file

@ -296,6 +296,7 @@ a {
span {
padding: 0 0.5rem;
&:hover {
background-color: rgba($text-primary-color, 0.06);
}
@ -321,6 +322,12 @@ a {
}
}
textarea.form-control {
height: 16rem;
line-height: 1.1rem;
padding: 0.6rem 1rem;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 $text-primary-color;
@ -671,6 +678,10 @@ form {
opacity: 0.6;
}
&.error {
outline: solid 0.4rem $error-color;
}
@include no-select()
}
@ -909,3 +920,114 @@ form {
}
}
}
.work-group-constraints {
position: relative;
}
.work-group-constraints-add {
position: absolute;
top: 0;
right: 0;
}
.work-group-constraints-add-list {
position: absolute;
top: 0;
right: 0;
z-index: 1;
display: none;
background: $background-primary-color;
border: solid 1px rgba($text-primary-color, 0.1);
span {
padding: 0 0.5rem;
&:hover {
background-color: rgba($text-primary-color, 0.06);
}
}
&.active {
display: block;
}
}
.overview {
display: flex;
}
.overview-main {
flex-grow: 4;
margin-right: 1rem;
}
.overview-side {
min-width: 20%;
}
.overview-shortcuts {
a {
display: block;
}
}
.overview-twitter {
height: 20rem;
background-color: #b3e6f9;
}
.post {
position: relative;
padding-top: 2rem;
}
.post-name {
position: absolute;
top: 0;
left: 0;
font-size: 1.2rem;
color: $primary-color;
line-height: 2rem;
&:empty::before {
display: block;
content: 'No title';
opacity: 0.5;
font-size: 1.2rem;
line-height: 2rem;
color: $text-primary-color;
}
}
.post-edit {
position: absolute;
top: 0;
right: 0;
line-height: 2rem;
}
.post-content {
h1, h2, h3, h4, h5, h6 {
margin: 0;
padding: 0;
}
h1 {
font-size: 1rem;
}
h2 {
font-size: 1rem;
}
h3 {
font-size: 1rem;
}
h4 {
font-size: 1rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 1rem;
}
}

View file

@ -12,7 +12,9 @@ import io.ktor.http.content.static
import io.ktor.jackson.jackson
import io.ktor.routing.routing
import io.ktor.websocket.WebSockets
import kotlinx.serialization.ImplicitReflectionSerializer
@ImplicitReflectionSerializer
fun Application.main() {
install(DefaultHeaders)
install(CallLogging)
@ -35,7 +37,7 @@ fun Application.main() {
}
// UI routes
dashboard()
overview()
calendar()
login()
account()
@ -53,6 +55,7 @@ fun Application.main() {
trackApi()
userApi()
workGroupApi()
constraintsApi()
// Web socket push notifications
pushService()

View file

@ -8,8 +8,10 @@ import io.ktor.application.Application
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ImplicitReflectionSerializer
object Main {
@ImplicitReflectionSerializer
@Suppress("UnusedMainParameter")
@JvmStatic
fun main(args: Array<String>) {

View file

@ -18,7 +18,8 @@ object Connection {
SchemaUtils.create(
DbTrack, DbWorkGroup,
DbRoom, DbSchedule,
DbUser, DbUserPermission
DbUser, DbUserPermission,
DbPost
)
}
}

View file

@ -12,15 +12,22 @@ object DbTrack : Table() {
object DbWorkGroup : Table() {
val id = long("id").autoIncrement().primaryKey()
val name = varchar("first_name", 64)
val name = varchar("name", 64)
val interested = integer("interested")
val trackId = long("track_id").nullable()
val projector = bool("projector")
val resolution = bool("resolution")
val internet = bool("internet")
val whiteboard = bool("whiteboard")
val blackboard = bool("blackboard")
val accessible = bool("accessible")
val language = enumeration("language", Language::class)
val length = integer("length")
val constraints = text("constraints")
}
object DbRoom : Table() {
@ -29,6 +36,11 @@ object DbRoom : Table() {
val places = integer("places")
val projector = bool("projector")
val internet = bool("internet")
val whiteboard = bool("whiteboard")
val blackboard = bool("blackboard")
val accessible = bool("accessible")
}
object DbSchedule : Table() {
@ -49,3 +61,11 @@ object DbUserPermission : Table() {
val userId = long("id").primaryKey(0)
val permission = enumeration("permission", Permission::class).primaryKey(1)
}
object DbPost : Table() {
val id = long("id").autoIncrement().primaryKey()
val name = varchar("name", 64)
val content = text("content")
val url = varchar("url", 64).uniqueIndex()
}

View file

@ -0,0 +1,107 @@
package de.kif.backend.repository
import de.kif.backend.database.DbPost
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Post
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.*
object PostRepository : Repository<Post> {
override val onCreate = EventHandler<Long>()
override val onUpdate = EventHandler<Long>()
override val onDelete = EventHandler<Long>()
private fun rowToModel(row: ResultRow): Post {
val id = row[DbPost.id]
val name = row[DbPost.name]
val content = row[DbPost.content]
val url = row[DbPost.url]
return Post(id, name, content, url)
}
override suspend fun get(id: Long): Post? {
return dbQuery {
rowToModel(DbPost.select { DbPost.id eq id }.firstOrNull() ?: return@dbQuery null)
}
}
override suspend fun create(model: Post): Long {
return dbQuery {
val id = DbPost.insert {
it[name] = model.name
it[content] = model.content
it[url] = model.url
}[DbPost.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
id
}
}
override suspend fun update(model: Post) {
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
dbQuery {
DbPost.update({ DbPost.id eq model.id }) {
it[name] = model.name
it[content] = model.content
it[url] = model.url
}
onUpdate.emit(model.id)
}
}
override suspend fun delete(id: Long) {
onDelete.emit(id)
dbQuery {
DbPost.deleteWhere { DbPost.id eq id }
}
}
override suspend fun all(): List<Post> {
return dbQuery {
val result = DbPost.selectAll()
result.map(this::rowToModel)
}
}
suspend fun getByUrl(url: String): Post? {
return dbQuery {
val result = DbPost.select { DbPost.url eq url }
runBlocking {
result.firstOrNull()?.let {
rowToModel(it)
}
}
}
}
fun registerPushService() {
onCreate {
runBlocking {
PushService.notify(MessageType.CREATE, RepositoryType.POST, it)
}
}
onUpdate {
runBlocking {
PushService.notify(MessageType.UPDATE, RepositoryType.POST, it)
}
}
onDelete {
runBlocking {
PushService.notify(MessageType.DELETE, RepositoryType.POST, it)
}
}
}
}

View file

@ -20,8 +20,12 @@ object RoomRepository : Repository<Room> {
val name = row[DbRoom.name]
val places = row[DbRoom.places]
val projector = row[DbRoom.projector]
val internet = row[DbRoom.internet]
val whiteboard = row[DbRoom.whiteboard]
val blackboard = row[DbRoom.blackboard]
val accessible = row[DbRoom.accessible]
return Room(id, name, places, projector)
return Room(id, name, places, projector, internet, whiteboard, blackboard, accessible)
}
override suspend fun get(id: Long): Room? {
@ -36,6 +40,10 @@ object RoomRepository : Repository<Room> {
it[name] = model.name
it[places] = model.places
it[projector] = model.projector
it[internet] = model.internet
it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard
it[accessible] = model.accessible
}[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
@ -51,6 +59,10 @@ object RoomRepository : Repository<Room> {
it[name] = model.name
it[places] = model.places
it[projector] = model.projector
it[internet] = model.internet
it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard
it[accessible] = model.accessible
}
onUpdate.emit(model.id)

View file

@ -3,12 +3,15 @@ package de.kif.backend.repository
import de.kif.backend.database.DbWorkGroup
import de.kif.backend.database.dbQuery
import de.kif.backend.util.PushService
import de.kif.common.Message
import de.kif.common.MessageType
import de.kif.common.Repository
import de.kif.common.RepositoryType
import de.kif.common.model.Constraint
import de.kif.common.model.WorkGroup
import de.westermann.kobserve.event.EventHandler
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.list
import org.jetbrains.exposed.sql.*
object WorkGroupRepository : Repository<WorkGroup> {
@ -24,12 +27,31 @@ object WorkGroupRepository : Repository<WorkGroup> {
val trackId = row[DbWorkGroup.trackId]
val projector = row[DbWorkGroup.projector]
val resolution = row[DbWorkGroup.resolution]
val internet = row[DbWorkGroup.internet]
val whiteboard = row[DbWorkGroup.whiteboard]
val blackboard = row[DbWorkGroup.blackboard]
val accessible = row[DbWorkGroup.accessible]
val length = row[DbWorkGroup.length]
val language = row[DbWorkGroup.language]
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
val track = trackId?.let { TrackRepository.get(it) }
return WorkGroup(id, name, interested, track, projector, resolution, length, language)
return WorkGroup(
id,
name,
interested,
track,
projector,
resolution,
internet,
whiteboard,
blackboard,
accessible,
length,
language,
constraints
)
}
override suspend fun get(id: Long): WorkGroup? {
@ -50,8 +72,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[trackId] = model.track?.id
it[projector] = model.projector
it[resolution] = model.resolution
it[internet] = model.internet
it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard
it[accessible] = model.accessible
it[length] = model.length
it[language] = model.language
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
onCreate.emit(id)
@ -69,8 +96,13 @@ object WorkGroupRepository : Repository<WorkGroup> {
it[trackId] = model.track?.id
it[projector] = model.projector
it[resolution] = model.resolution
it[internet] = model.internet
it[whiteboard] = model.whiteboard
it[blackboard] = model.blackboard
it[accessible] = model.accessible
it[length] = model.length
it[language] = model.language
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
}
onUpdate.emit(model.id)

View file

@ -136,7 +136,7 @@ private fun DIV.renderTimeToRoom(
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-time"] = start.toString()
attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString()
@ -203,7 +203,7 @@ private fun DIV.renderRoomToTime(
for (room in rooms) {
div("calendar-cell") {
attributes["data-time"] = time.toString()
attributes["data-time"] = start.toString()
attributes["data-room"] = room.id.toString()
attributes["data-day"] = day.toString()
title = timeString
@ -251,6 +251,7 @@ fun Route.calendar() {
get("/calendar/{day}") {
val user = isAuthenticated(Permission.SCHEDULE)
val editable = user != null
val day = call.parameters["day"]?.toIntOrNull() ?: return@get
@ -280,11 +281,14 @@ fun Route.calendar() {
min = h1
}
if (editable) {
min = min(min, 0)
max = max(max, 24 * 60)
}
min = (min / 60 - 1) * 60
max = (max / 60 + 2) * 60
min = min(min, 0)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -314,7 +318,6 @@ fun Route.calendar() {
}
div("calendar") {
val editable = user != null
attributes["data-day"] = day.toString()
attributes["data-editable"] = editable.toString()

View file

@ -1,27 +0,0 @@
package de.kif.backend.route
import de.kif.backend.PortalSession
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.sessions.get
import io.ktor.sessions.sessions
import kotlinx.html.h1
fun Route.dashboard() {
get("") {
val user = call.sessions.get<PortalSession>()?.getUser(call)
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.DASHBOARD
}
content {
h1 { +"Dashboard" }
}
}
}
}

View file

@ -0,0 +1,281 @@
package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.isAuthenticated
import de.kif.backend.repository.PostRepository
import de.kif.backend.util.markdownToHtml
import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.common.model.Permission
import de.kif.common.model.Post
import io.ktor.application.call
import io.ktor.html.respondHtmlTemplate
import io.ktor.request.receiveParameters
import io.ktor.response.respondRedirect
import io.ktor.routing.Route
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.util.toMap
import kotlinx.html.*
fun Route.overview() {
get("") {
val user = isAuthenticated(Permission.POST)
val editable = user != null
val postList = PostRepository.all().asReversed()
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content {
div("overview") {
div("overview-main") {
if (editable) {
div("overview-new") {
a("post/new", classes = "form-btn") {
+"New"
}
}
}
for (post in postList) {
div("overview-post post") {
span("post-name") {
+post.name
}
if (editable) {
a("/post/${post.id}", classes = "post-edit") {
i("material-icons") { +"edit" }
}
}
div("post-content") {
unsafe {
raw(markdownToHtml(post.content))
}
}
}
}
}
div("overview-side") {
div("overview-shortcuts") {
a {
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
}
a {
+"Wiki"
}
}
div("overview-twitter") {
+"The Twitter Wall"
}
}
}
}
}
}
get("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editPost = PostRepository.get(postId) ?: return@get
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content {
h1 { +"Edit post" }
form(method = FormMethod.post) {
div("form-group") {
label {
htmlFor = "name"
+"Name"
}
input(
name = "name",
classes = "form-control"
) {
id = "name"
placeholder = "Name"
value = editPost.name
}
}
div("form-group") {
label {
htmlFor = "url"
+"Url"
}
input(
name = "url",
classes = "form-control"
) {
id = "places"
placeholder = "Places"
value = editPost.url
}
}
div("form-group") {
label {
htmlFor = "content"
+"Content"
}
textArea(rows = "10", classes = "form-control") {
name = "content"
id = "projector"
+editPost.content
}
}
div("form-group") {
a("/") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Save"
}
}
}
a("/post/${editPost.id}/delete") {
button(classes = "form-btn btn-danger") {
+"Delete"
}
}
}
}
}
}
post("/post/{id}") {
authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
var post = PostRepository.get(postId) ?: return@post
params["name"]?.let { post = post.copy(name = it) }
params["url"]?.let { post = post.copy(url = it) }
params["content"]?.let { post = post.copy(content = it) }
PostRepository.update(post)
call.respondRedirect("/")
}
}
get("/post/new") {
authenticateOrRedirect(Permission.POST) { user ->
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
active = MenuTemplate.Tab.BOARD
}
content {
h1 { +"Create post" }
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") {
label {
htmlFor = "url"
+"Url"
}
input(
name = "url",
classes = "form-control"
) {
id = "places"
placeholder = "Places"
value = Post.generateUrl()
}
}
div("form-group") {
label {
htmlFor = "content"
+"Content"
}
textArea(rows = "10", classes = "form-control") {
name = "content"
id = "projector"
+""
}
}
div("form-group") {
a("/") {
button(classes = "form-btn") {
+"Cancel"
}
}
button(type = ButtonType.submit, classes = "form-btn btn-primary") {
+"Create"
}
}
}
}
}
}
}
post("/post/new") {
authenticateOrRedirect(Permission.POST) { user ->
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
list.firstOrNull()
}
val name = params["name"] ?: return@post
val content = params["content"] ?: return@post
val url = params["url"] ?: return@post
val post = Post(null, name, content, url)
PostRepository.create(post)
call.respondRedirect("/")
}
}
get("/post/{id}/delete") {
authenticateOrRedirect(Permission.POST) { user ->
val postId = call.parameters["id"]?.toLongOrNull() ?: return@get
PostRepository.delete(postId)
call.respondRedirect("/")
}
}
}

View file

@ -36,7 +36,6 @@ fun Route.room() {
active = MenuTemplate.Tab.ROOM
}
content {
h1 { +"Rooms" }
insert(TableTemplate()) {
searchValue = search
@ -169,6 +168,74 @@ fun Route.room() {
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "internet",
classes = "form-control",
type = InputType.checkBox
) {
id = "internet"
checked = editRoom.internet
}
label {
htmlFor = "internet"
+"Internet"
}
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "whiteboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "whiteboard"
checked = editRoom.whiteboard
}
label {
htmlFor = "whiteboard"
+"Whiteboard"
}
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "blackboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "blackboard"
checked = editRoom.blackboard
}
label {
htmlFor = "blackboard"
+"Blackboard"
}
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "accessible",
classes = "form-control",
type = InputType.checkBox
) {
id = "accessible"
checked = editRoom.accessible
}
label {
htmlFor = "accessible"
+"Accessible"
}
}
}
div("form-group") {
a("/room") {
button(classes = "form-btn") {
@ -201,6 +268,10 @@ fun Route.room() {
params["name"]?.let { room = room.copy(name = it) }
params["places"]?.let { room = room.copy(places = it.toIntOrNull() ?: 0) }
params["projector"]?.let { room = room.copy(projector = it == "on") }
params["internet"]?.let { room = room.copy(internet = it == "on") }
params["whiteboard"]?.let { room = room.copy(whiteboard = it == "on") }
params["blackboard"]?.let { room = room.copy(blackboard = it == "on") }
params["accessible"]?.let { room = room.copy(accessible = it == "on") }
RoomRepository.update(room)
@ -268,6 +339,74 @@ fun Route.room() {
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "internet",
classes = "form-control",
type = InputType.checkBox
) {
id = "internet"
checked = false
}
label {
htmlFor = "internet"
+"Internet"
}
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "whiteboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "whiteboard"
checked = false
}
label {
htmlFor = "whiteboard"
+"Whiteboard"
}
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "blackboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "blackboard"
checked = false
}
label {
htmlFor = "blackboard"
+"Blackboard"
}
}
}
div("form-switch-group") {
div("form-group form-switch") {
input(
name = "accessible",
classes = "form-control",
type = InputType.checkBox
) {
id = "accessible"
checked = false
}
label {
htmlFor = "accessible"
+"Accessible"
}
}
}
div("form-group") {
a("/room") {
button(classes = "form-btn") {
@ -293,8 +432,12 @@ fun Route.room() {
val name = params["name"] ?: return@post
val places = (params["places"] ?: return@post).toIntOrNull() ?: 0
val projector = params["projector"] == "on"
val internet = params["internet"] == "on"
val whiteboard = params["whiteboard"] == "on"
val blackboard = params["blackboard"] == "on"
val accessible = params["accessible"] == "on"
val room = Room(null, name, places, projector)
val room = Room(null, name, places, projector, internet, whiteboard, blackboard, accessible)
RoomRepository.create(room)

View file

@ -94,7 +94,6 @@ fun Route.track() {
active = MenuTemplate.Tab.WORK_GROUP
}
content {
h1 { +"Tracks" }
insert(TableTemplate()) {
searchValue = search

View file

@ -37,7 +37,6 @@ fun Route.user() {
active = MenuTemplate.Tab.USER
}
content {
h1 { +"Users" }
insert(TableTemplate()) {
searchValue = search

View file

@ -1,6 +1,5 @@
package de.kif.backend.route
import de.kif.backend.authenticateOrRedirect
import de.kif.backend.repository.TrackRepository
import de.kif.backend.repository.WorkGroupRepository
@ -8,9 +7,7 @@ import de.kif.backend.view.MainTemplate
import de.kif.backend.view.MenuTemplate
import de.kif.backend.view.TableTemplate
import de.kif.common.Search
import de.kif.common.model.Language
import de.kif.common.model.Permission
import de.kif.common.model.WorkGroup
import de.kif.common.model.*
import io.ktor.application.call
import io.ktor.html.insert
import io.ktor.html.respondHtmlTemplate
@ -36,7 +33,6 @@ fun Route.workGroup() {
active = MenuTemplate.Tab.WORK_GROUP
}
content {
h1 { +"Work groups" }
insert(TableTemplate()) {
searchValue = search
@ -157,6 +153,17 @@ fun Route.workGroup() {
val workGroupId = call.parameters["id"]?.toLongOrNull() ?: return@get
val editWorkGroup = WorkGroupRepository.get(workGroupId) ?: return@get
val tracks = TrackRepository.all()
val workGroups = editWorkGroup.constraints.mapNotNull {
when (it.type) {
ConstraintType.NotAtSameTime -> it.number
ConstraintType.OnlyAfterWorkGroup -> it.number
else -> null
}
}.distinct().associateWith {
WorkGroupRepository.get(it)!!
}
call.respondHtmlTemplate(MainTemplate()) {
menuTemplate {
this.user = user
@ -201,7 +208,6 @@ fun Route.workGroup() {
htmlFor = "track"
+"Track"
}
//div("input-group") {
select(
classes = "form-control"
) {
@ -220,12 +226,6 @@ fun Route.workGroup() {
}
}
}
/*
a("/tracks", classes = "form-btn") {
i("material-icons") { +"edit" }
}
}
*/
}
div("form-group") {
label {
@ -294,6 +294,149 @@ fun Route.workGroup() {
+"Resolution"
}
}
div("form-group form-switch") {
input(
name = "internet",
classes = "form-control",
type = InputType.checkBox
) {
id = "internet"
checked = editWorkGroup.internet
}
label {
htmlFor = "internet"
+"Internet"
}
}
div("form-group form-switch") {
input(
name = "whiteboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "whiteboard"
checked = editWorkGroup.whiteboard
}
label {
htmlFor = "whiteboard"
+"Whiteboard"
}
}
div("form-group form-switch") {
input(
name = "blackboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "blackboard"
checked = editWorkGroup.blackboard
}
label {
htmlFor = "blackboard"
+"Blackboard"
}
}
div("form-group form-switch") {
input(
name = "accessible",
classes = "form-control",
type = InputType.checkBox
) {
id = "accessible"
checked = editWorkGroup.accessible
}
label {
htmlFor = "accessible"
+"Accessible"
}
}
div("work-group-constraints")
}
div("form-group work-group-constraints") {
label {
+"Constraints"
}
span("form-btn work-group-constraints-add") {
i("material-icons") { +"add" }
}
div("work-group-constraints-add-list") {
}
for ((index, constraint) in editWorkGroup.constraints.withIndex()) {
div("input-group") {
when (constraint.type) {
ConstraintType.OnlyOnDay -> {
span("form-btn") {
+"On day"
}
input(
name = "constraint-only-on-day-$index",
classes = "form-control",
type = InputType.number
) {
value = constraint.number.toString()
min = "-1337"
max = "1337"
}
}
ConstraintType.OnlyAfterTime -> {
span("form-btn") {
+"After time"
}
input(
name = "constraint-only-after-time-$index",
classes = "form-control",
type = InputType.number
) {
value = constraint.number.toString()
min = "-1337"
max = "133700"
}
}
ConstraintType.NotAtSameTime -> {
span("form-btn") {
+"Not with"
}
select(
classes = "form-control"
) {
name = "constraint-not-at-same-time-$index"
option {
selected = true
value = constraint.number.toString()
+(workGroups[constraint.number]?.name ?: "")
}
}
}
ConstraintType.OnlyAfterWorkGroup -> {
span("form-btn") {
+"After AK"
}
select(
classes = "form-control"
) {
name = "constraint-only-after-work-group-$index"
option {
selected = true
value = constraint.number.toString()
+(workGroups[constraint.number]?.name ?: "")
}
}
}
}
}
}
}
div("form-group") {
@ -333,12 +476,37 @@ fun Route.workGroup() {
}
params["projector"]?.let { editWorkGroup = editWorkGroup.copy(projector = it == "on") }
params["resolution"]?.let { editWorkGroup = editWorkGroup.copy(resolution = it == "on") }
params["internet"]?.let { editWorkGroup = editWorkGroup.copy(internet = it == "on") }
params["whiteboard"]?.let { editWorkGroup = editWorkGroup.copy(whiteboard = it == "on") }
params["blackboard"]?.let { editWorkGroup = editWorkGroup.copy(blackboard = it == "on") }
params["accessible"]?.let { editWorkGroup = editWorkGroup.copy(accessible = it == "on") }
params["length"]?.toIntOrNull()?.let { editWorkGroup = editWorkGroup.copy(length = it) }
params["language"]?.let {
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
}
}
editWorkGroup = editWorkGroup.copy(constraints = constraints)
WorkGroupRepository.update(editWorkGroup)
call.respondRedirect("/workgroups")
@ -477,6 +645,78 @@ fun Route.workGroup() {
+"Resolution"
}
}
div("form-group form-switch") {
input(
name = "internet",
classes = "form-control",
type = InputType.checkBox
) {
id = "internet"
checked = false
}
label {
htmlFor = "internet"
+"Internet"
}
}
div("form-group form-switch") {
input(
name = "whiteboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "whiteboard"
checked = false
}
label {
htmlFor = "whiteboard"
+"Whiteboard"
}
}
div("form-group form-switch") {
input(
name = "blackboard",
classes = "form-control",
type = InputType.checkBox
) {
id = "blackboard"
checked = false
}
label {
htmlFor = "blackboard"
+"Blackboard"
}
}
div("form-group form-switch") {
input(
name = "accessible",
classes = "form-control",
type = InputType.checkBox
) {
id = "accessible"
checked = false
}
label {
htmlFor = "accessible"
+"Accessible"
}
}
}
div("form-group work-group-constraints") {
label {
+"Constraints"
}
span("form-btn work-group-constraints-add") {
i("material-icons") { +"add" }
}
div("work-group-constraints-add-list") {
}
}
div("form-group") {
@ -511,6 +751,29 @@ fun Route.workGroup() {
Language.values().find { l -> l.code == it } ?: Language.GERMAN
}
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 workGroup = WorkGroup(
null,
name = name,
@ -518,8 +781,13 @@ fun Route.workGroup() {
track = track,
projector = projector,
resolution = resolution,
internet = internet,
whiteboard = whiteboard,
blackboard = blackboard,
accessible = accessible,
length = length,
language = language
language = language,
constraints = constraints
)
WorkGroupRepository.create(workGroup)

View file

@ -0,0 +1,47 @@
package de.kif.backend.route.api
import de.kif.backend.authenticate
import de.kif.backend.repository.ScheduleRepository
import de.kif.common.checkConstraints
import de.kif.common.model.Permission
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.routing.Route
import io.ktor.routing.get
fun Route.constraintsApi() {
get("/api/constraints") {
try {
authenticate(Permission.SCHEDULE) {
val schedules = ScheduleRepository.all()
val errors = checkConstraints(schedules, schedules)
call.success(errors)
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
get("/api/constraint/{id}") {
try {
authenticate(Permission.SCHEDULE) {
val id = call.parameters["id"]?.toLongOrNull()
val schedules = ScheduleRepository.all()
val check = schedules.filter { it.workGroup.id == id }
val errors = checkConstraints(check, schedules)
call.success(errors)
} onFailure {
call.error(HttpStatusCode.Unauthorized)
}
} catch (_: Exception) {
call.error(HttpStatusCode.InternalServerError)
}
}
}

View file

@ -0,0 +1,60 @@
package de.kif.backend.util
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
import com.vladsch.flexmark.ext.emoji.EmojiExtension
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.ext.tables.TablesExtension
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.options.MutableDataSet;
import java.util.*
/*
import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor
import org.intellij.markdown.html.HtmlGenerator
import org.intellij.markdown.parser.MarkdownParser
fun markdownToHtml(content: String): String {
val flavour = CommonMarkFlavourDescriptor()
val parsedTree = MarkdownParser(flavour).buildMarkdownTreeFromString(content)
val html = HtmlGenerator(content, parsedTree, flavour).generateHtml()
return html
}
*/
/*
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
fun markdownToHtml(content: String): String {
val parser = Parser.builder().build()
val document = parser.parse(content)
val renderer = HtmlRenderer.builder().build()
return renderer.render(document)
}
*/
fun markdownToHtml(content: String): String {
val options = MutableDataSet()
options.set(Parser.EXTENSIONS, Arrays.asList(
TablesExtension.create(),
StrikethroughExtension.create(),
TaskListExtension.create(),
EmojiExtension.create(),
AutolinkExtension.create()
));
//options.set(HtmlRenderer.SOFT_BREAK, "<br />\n");
val parser = Parser.builder(options).build()
val renderer = HtmlRenderer.builder(options).build()
// You can re-use parser and renderer instances
val document = parser.parse(content)
val html = renderer.render(document)
return html
}

View file

@ -50,4 +50,5 @@ fun Route.pushService() {
TrackRepository.registerPushService()
UserRepository.registerPushService()
WorkGroupRepository.registerPushService()
PostRepository.registerPushService()
}

View file

@ -7,14 +7,14 @@ import kotlinx.html.*
class MenuTemplate() : Template<FlowContent> {
var active: Tab = Tab.DASHBOARD
var active: Tab = Tab.BOARD
var user: User? = null
override fun FlowContent.apply() {
nav("menu") {
div("container") {
div("menu-left") {
a("/", classes = if (active == Tab.DASHBOARD) "active" else null) {
a("/", classes = if (active == Tab.BOARD) "active" else null) {
+"Dashboard"
}
a("/calendar", classes = if (active == Tab.CALENDAR) "active" else null) {
@ -42,7 +42,7 @@ class MenuTemplate() : Template<FlowContent> {
+"Rooms"
}
}
if (user.checkPermission(Permission.PERSON)) {
if (user.checkPermission(Permission.USER)) {
a("/users", classes = if (active == Tab.USER) "active" else null) {
+"Users"
}
@ -58,6 +58,6 @@ class MenuTemplate() : Template<FlowContent> {
}
enum class Tab {
DASHBOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER
BOARD, CALENDAR, ACCOUNT, WORK_GROUP, ROOM, PERSON, USER
}
}