Add drag and drop creation of schedule
This commit is contained in:
parent
b7d6476a70
commit
84f8e71239
38 changed files with 1277 additions and 548 deletions
|
@ -29,6 +29,7 @@ repositories {
|
|||
}
|
||||
def ktor_version = '1.1.5'
|
||||
def serialization_version = '0.11.0'
|
||||
def observable_version = '0.9.3'
|
||||
|
||||
kotlin {
|
||||
jvm() {
|
||||
|
@ -53,7 +54,7 @@ kotlin {
|
|||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib-common')
|
||||
implementation "de.westermann:KObserve-metadata:0.9.1"
|
||||
implementation "de.westermann:KObserve-metadata:$observable_version"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ kotlin {
|
|||
|
||||
implementation 'org.mindrot:jbcrypt:0.4'
|
||||
|
||||
implementation "de.westermann:KObserve-jvm:0.9.1"
|
||||
implementation "de.westermann:KObserve-jvm:$observable_version"
|
||||
|
||||
api 'io.github.microutils:kotlin-logging:1.6.23'
|
||||
api 'ch.qos.logback:logback-classic:1.2.3'
|
||||
|
@ -103,7 +104,7 @@ kotlin {
|
|||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
|
||||
|
||||
implementation "de.westermann:KObserve-js:0.9.1"
|
||||
implementation "de.westermann:KObserve-js:$observable_version"
|
||||
}
|
||||
}
|
||||
jsTest {
|
||||
|
|
120
src/commonMain/kotlin/de/kif/common/Search.kt
Normal file
120
src/commonMain/kotlin/de/kif/common/Search.kt
Normal file
|
@ -0,0 +1,120 @@
|
|||
package de.kif.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchElement(
|
||||
val fields: Map<String, String> = emptyMap(),
|
||||
val flags: Map<String, Boolean> = emptyMap(),
|
||||
val numbers: Map<String, Double> = emptyMap()
|
||||
) {
|
||||
|
||||
fun stringify(): String {
|
||||
return Message.json.stringify(serializer(), this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(data: String): SearchElement {
|
||||
return Message.json.parse(serializer(), data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Search {
|
||||
|
||||
private val regex = """
|
||||
((\w+)\s?[:=]\s?)?
|
||||
((\[.*]|\+\w+|-\w+|!\w+)|
|
||||
((\w+)\s?([<=>]+)\s?(\d+))|
|
||||
(\w+|"(.*)"))
|
||||
""".trimIndent().replace("\n", "").toRegex()
|
||||
|
||||
fun match(search: String, element: SearchElement): Boolean {
|
||||
val matches = regex.findAll(search)
|
||||
|
||||
val fields = mutableMapOf<String, String>()
|
||||
val flags = mutableMapOf<String, Boolean>()
|
||||
val numbers = mutableMapOf<String, ClosedRange<Double>>()
|
||||
|
||||
for (match in matches) {
|
||||
val name = match.groups[2]?.value ?: ""
|
||||
val field = match.groups[10]?.value ?: match.groups[9]?.value
|
||||
val flag = match.groups[4]?.value
|
||||
val numberName = match.groups[6]?.value
|
||||
val numberRelation = match.groups[7]?.value
|
||||
val numberDigits = match.groups[8]?.value?.toDoubleOrNull()
|
||||
|
||||
if (flag != null) {
|
||||
val h = flag.replace("[\\[\\]+!\\-]".toRegex(), "")
|
||||
val b = ("-" !in flag && "!" !in flag)
|
||||
flags[h] = b
|
||||
} else if (numberName != null && numberRelation != null && numberDigits != null) {
|
||||
when (numberRelation) {
|
||||
"<" -> numbers[numberName] = Double.NEGATIVE_INFINITY..(numberDigits - Double.MIN_VALUE)
|
||||
"<=" -> numbers[numberName] = Double.NEGATIVE_INFINITY..numberDigits
|
||||
"==" -> numbers[numberName] = numberDigits..numberDigits
|
||||
">" -> numbers[numberName] = (numberDigits + Double.MIN_VALUE)..Double.POSITIVE_INFINITY
|
||||
">=" -> numbers[numberName] = numberDigits..Double.POSITIVE_INFINITY
|
||||
}
|
||||
} else if (field != null) {
|
||||
val old = fields[name]
|
||||
fields[name] = if (old == null) field else "$old $field"
|
||||
}
|
||||
}
|
||||
|
||||
val fieldRadio = if (fields.isEmpty()) 1.0 else fields.count { (searchKey, searchValue) ->
|
||||
for ((elementKey, elementValue) in element.fields) {
|
||||
if (elementKey.contains(searchKey, true) && elementValue.contains(searchValue, true)) {
|
||||
return@count true
|
||||
}
|
||||
}
|
||||
for ((elementKey, elementValue) in element.flags) {
|
||||
if (elementValue && elementKey.contains(searchValue, true)) {
|
||||
return@count true
|
||||
}
|
||||
}
|
||||
for ((elementKey, _) in element.numbers) {
|
||||
if (elementKey.contains(searchValue, true)) {
|
||||
return@count true
|
||||
}
|
||||
}
|
||||
false
|
||||
} / fields.size.toDouble()
|
||||
|
||||
for ((searchKey, searchValue) in flags) {
|
||||
for ((elementKey, elementValue) in element.flags) {
|
||||
if (elementKey.contains(searchKey, true) && searchValue != elementValue)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for ((searchKey, searchValue) in numbers) {
|
||||
for ((elementKey, elementValue) in element.numbers) {
|
||||
if (elementKey.contains(searchKey, true) && elementValue !in searchValue)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//println("$fieldRadio (${fieldRadio >= 0.5}) for $element")
|
||||
return fieldRadio >= 0.5
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fun main() {
|
||||
val element = SearchElement(
|
||||
mapOf(
|
||||
"name" to "lorem"
|
||||
), mapOf(
|
||||
"beamer" to true,
|
||||
"room" to true,
|
||||
"day" to false
|
||||
), mapOf(
|
||||
"places" to 500.0
|
||||
)
|
||||
)
|
||||
println(Search.match("""search text data:"text to search" name = hans""", element))
|
||||
|
||||
println(Search.match("""lorem [beamer] places >= 100 +room -day""", element))
|
||||
}
|
||||
*/
|
|
@ -1,3 +1,7 @@
|
|||
package de.kif.common.model
|
||||
|
||||
interface Model
|
||||
import de.kif.common.SearchElement
|
||||
|
||||
interface Model {
|
||||
fun createSearch(): SearchElement
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package de.kif.common.model
|
||||
|
||||
import de.kif.common.SearchElement
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
@ -8,4 +9,15 @@ data class Room(
|
|||
val name: String,
|
||||
val places: Int,
|
||||
val projector: Boolean
|
||||
) : Model
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
mapOf(
|
||||
"name" to name
|
||||
), mapOf(
|
||||
"projector" to projector
|
||||
), mapOf(
|
||||
"places" to places.toDouble()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package de.kif.common.model
|
||||
|
||||
import de.kif.common.SearchElement
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
@ -9,4 +10,16 @@ data class Schedule(
|
|||
val room: Room,
|
||||
val day: Int,
|
||||
val time: Int
|
||||
) : Model
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
mapOf(
|
||||
"workgroup" to workGroup.name,
|
||||
"room" to room.name,
|
||||
"track" to (workGroup.track?.name ?: ""),
|
||||
"language" to workGroup.language.localeName
|
||||
), mapOf(), mapOf(
|
||||
"day" to day.toDouble()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
package de.kif.common.model
|
||||
|
||||
import de.kif.common.SearchElement
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Track(
|
||||
val id: Long?,
|
||||
var name: String,
|
||||
var color: Color
|
||||
) : Model
|
||||
val name: String,
|
||||
val color: Color
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
mapOf(
|
||||
"name" to name
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package de.kif.common.model
|
||||
|
||||
import de.kif.common.SearchElement
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
@ -13,4 +14,10 @@ data class User(
|
|||
fun checkPermission(permission: Permission): Boolean {
|
||||
return permission in permissions || Permission.ADMIN in permissions
|
||||
}
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
mapOf(
|
||||
"username" to username
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package de.kif.common.model
|
||||
|
||||
import de.kif.common.SearchElement
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
@ -12,4 +13,19 @@ data class WorkGroup(
|
|||
val resolution: Boolean,
|
||||
val length: Int,
|
||||
val language: Language
|
||||
) : Model
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
mapOf(
|
||||
"name" to name,
|
||||
"track" to (track?.name ?: ""),
|
||||
"language" to language.localeName
|
||||
), mapOf(
|
||||
"projector" to projector,
|
||||
"resolution" to resolution
|
||||
), mapOf(
|
||||
"interested" to interested.toDouble(),
|
||||
"length" to length.toDouble()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package de.kif.frontend
|
||||
|
||||
import de.kif.frontend.views.initCalendar
|
||||
import de.kif.frontend.views.calendar.initCalendar
|
||||
import de.kif.frontend.views.initTableLayout
|
||||
import de.westermann.kwebview.components.init
|
||||
import kotlin.browser.document
|
||||
|
||||
|
@ -10,4 +11,7 @@ fun main() = init {
|
|||
if (document.getElementsByClassName("calendar").length > 0) {
|
||||
initCalendar()
|
||||
}
|
||||
if (document.getElementsByClassName("table-layout").length > 0) {
|
||||
initTableLayout()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ object RoomRepository : Repository<Room> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: Room): Long {
|
||||
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))?.toLong()
|
||||
return repositoryPost("/api/rooms", Message.json.stringify(Room.serializer(), model))
|
||||
?: throw IllegalStateException("Cannot create model!")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ object ScheduleRepository : Repository<Schedule> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: Schedule): Long {
|
||||
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))?.toLong()
|
||||
return repositoryPost("/api/schedules", Message.json.stringify(Schedule.serializer(), model))
|
||||
?: throw IllegalStateException("Cannot create model!")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ object TrackRepository : Repository<Track> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: Track): Long {
|
||||
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))?.toLong()
|
||||
return repositoryPost("/api/tracks", Message.json.stringify(Track.serializer(), model))
|
||||
?: throw IllegalStateException("Cannot create model!")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ object UserRepository : Repository<User> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: User): Long {
|
||||
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))?.toLong()
|
||||
return repositoryPost("/api/users", Message.json.stringify(User.serializer(), model))
|
||||
?: throw IllegalStateException("Cannot create model!")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: WorkGroup): Long {
|
||||
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))?.toLong()
|
||||
return repositoryPost("/api/workgroups", Message.json.stringify(WorkGroup.serializer(), model))
|
||||
?: throw IllegalStateException("Cannot create model!")
|
||||
}
|
||||
|
||||
|
|
|
@ -1,359 +0,0 @@
|
|||
package de.kif.frontend.views
|
||||
|
||||
import de.kif.common.CALENDAR_GRID_WIDTH
|
||||
import de.kif.common.model.Room
|
||||
import de.kif.common.model.Schedule
|
||||
import de.kif.frontend.iterator
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.RepositoryDelegate
|
||||
import de.kif.frontend.repository.RoomRepository
|
||||
import de.kif.frontend.repository.ScheduleRepository
|
||||
import de.westermann.kwebview.*
|
||||
import de.westermann.kwebview.components.Body
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.EventListener
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import org.w3c.dom.get
|
||||
import kotlin.browser.document
|
||||
import kotlin.dom.appendText
|
||||
import kotlin.dom.isText
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time - 10))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time - 5))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time + 5))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time + 10))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
ScheduleRepository.delete(entry.scheduleId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarEntry(view: HTMLElement) : View(view) {
|
||||
|
||||
private lateinit var mouseDelta: Point
|
||||
private var newCell: CalendarCell? = null
|
||||
|
||||
private var language by dataset.property("language")
|
||||
|
||||
val scheduleId = dataset["id"]?.toLongOrNull() ?: 0
|
||||
|
||||
val schedule = RepositoryDelegate(ScheduleRepository, scheduleId)
|
||||
|
||||
var pending by classList.property("pending")
|
||||
|
||||
private fun onMove(event: MouseEvent) {
|
||||
val position = event.toPoint() - mouseDelta
|
||||
|
||||
val cell = calendarCells.find {
|
||||
position in it.dimension
|
||||
}
|
||||
|
||||
|
||||
if (cell != null) {
|
||||
cell += this
|
||||
|
||||
if (newCell == null) {
|
||||
style {
|
||||
left = "0"
|
||||
top = "0.1rem"
|
||||
}
|
||||
}
|
||||
|
||||
newCell = cell
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
private fun onFinishMove(event: MouseEvent) {
|
||||
classList -= "drag"
|
||||
|
||||
newCell?.let { cell ->
|
||||
launch {
|
||||
val newTime = cell.time
|
||||
val newRoom = cell.getRoom()
|
||||
|
||||
pending = true
|
||||
|
||||
val s = schedule.get().copy(room = newRoom, time = newTime)
|
||||
|
||||
ScheduleRepository.update(s)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
launch {
|
||||
classList += "drag"
|
||||
|
||||
val s = schedule.get()
|
||||
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
|
||||
|
||||
val p = calendarCells.find {
|
||||
it.day == s.day && it.time == time && it.roomId == s.room.id
|
||||
}?.dimension?.center ?: dimension.center
|
||||
|
||||
mouseDelta = event.toPoint() - p
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(schedule: Schedule) {
|
||||
pending = false
|
||||
|
||||
language = schedule.workGroup.language.code
|
||||
|
||||
this.schedule.set(schedule)
|
||||
|
||||
style {
|
||||
val size = schedule.workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
|
||||
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
|
||||
|
||||
val ps = "${pos * 100}%"
|
||||
val sz = "${size * 100}%"
|
||||
|
||||
left = ps
|
||||
top = "calc($ps + 0.1rem)"
|
||||
|
||||
width = sz
|
||||
height = "calc($sz - 0.2rem)"
|
||||
|
||||
if (schedule.workGroup.track?.color != null) {
|
||||
backgroundColor = schedule.workGroup.track.color.toString()
|
||||
color = schedule.workGroup.track.color.calcTextColor().toString()
|
||||
}
|
||||
}
|
||||
|
||||
for (element in html.childNodes) {
|
||||
if (element.isText) {
|
||||
html.removeChild(element)
|
||||
}
|
||||
}
|
||||
|
||||
html.appendText(schedule.workGroup.name)
|
||||
|
||||
|
||||
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
|
||||
val cell = calendarCells.find {
|
||||
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
|
||||
}
|
||||
|
||||
if (cell != null && cell.html != html.parentElement) {
|
||||
cell += this
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(schedule: Schedule): CalendarEntry {
|
||||
val entry = CalendarEntry(createHtmlView())
|
||||
|
||||
entry.load(schedule)
|
||||
|
||||
return entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
|
||||
val day = dataset["day"]?.toIntOrNull() ?: 0
|
||||
val time = dataset["time"]?.toIntOrNull() ?: 0
|
||||
val roomId = dataset["room"]?.toLongOrNull() ?: 0
|
||||
|
||||
private lateinit var room: Room
|
||||
|
||||
suspend fun getRoom(): Room {
|
||||
if (this::room.isInitialized) {
|
||||
return room
|
||||
}
|
||||
|
||||
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
|
||||
return room
|
||||
}
|
||||
}
|
||||
|
||||
var calendarEntries: List<CalendarEntry> = emptyList()
|
||||
var calendarCells: List<CalendarCell> = emptyList()
|
||||
|
||||
fun initCalendar() {
|
||||
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()
|
||||
|
||||
ScheduleRepository.onCreate {
|
||||
launch {
|
||||
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
|
||||
CalendarEntry.create(schedule)
|
||||
}
|
||||
}
|
||||
ScheduleRepository.onUpdate {
|
||||
launch {
|
||||
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
|
||||
var found = false
|
||||
for (entry in calendarEntries) {
|
||||
if (entry.scheduleId == it) {
|
||||
entry.load(schedule)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
CalendarEntry.create(schedule)
|
||||
}
|
||||
}
|
||||
}
|
||||
ScheduleRepository.onDelete {
|
||||
for (entry in calendarEntries) {
|
||||
if (entry.scheduleId == it) {
|
||||
entry.html.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt
Normal file
32
src/jsMain/kotlin/de/kif/frontend/views/TableLayout.kt
Normal file
|
@ -0,0 +1,32 @@
|
|||
package de.kif.frontend.views
|
||||
|
||||
import de.kif.common.Search
|
||||
import de.kif.common.SearchElement
|
||||
import de.kif.frontend.iterator
|
||||
import de.westermann.kwebview.components.InputView
|
||||
import org.w3c.dom.HTMLFormElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.HTMLTableElement
|
||||
import org.w3c.dom.get
|
||||
import kotlin.browser.document
|
||||
|
||||
fun initTableLayout() {
|
||||
val form = document.getElementsByClassName("table-layout-search")[0] as HTMLFormElement
|
||||
form.onsubmit = { false }
|
||||
|
||||
val table = document.getElementsByClassName("table-layout-table")[0] as HTMLTableElement
|
||||
|
||||
val list = table.getElementsByTagName("tr").iterator().asSequence().filter {
|
||||
it.dataset.get("search") != null
|
||||
}.associateWith {
|
||||
SearchElement.parse(it.dataset.get("search")!!)
|
||||
}
|
||||
|
||||
val input = form.getElementsByTagName("input")[0] as HTMLInputElement
|
||||
val search = InputView.wrap(input)
|
||||
search.valueProperty.onChange {
|
||||
for ((row, s) in list) {
|
||||
row.style.display = if (Search.match(search.value, s)) "table-row" else "none"
|
||||
}
|
||||
}
|
||||
}
|
65
src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt
Normal file
65
src/jsMain/kotlin/de/kif/frontend/views/calendar/Calendar.kt
Normal file
|
@ -0,0 +1,65 @@
|
|||
package de.kif.frontend.views.calendar
|
||||
|
||||
import de.kif.frontend.iterator
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.ScheduleRepository
|
||||
import de.westermann.kwebview.View
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.get
|
||||
import kotlin.browser.document
|
||||
|
||||
|
||||
class Calendar(calendar: HTMLElement): View(calendar) {
|
||||
var calendarEntries: List<CalendarEntry> = emptyList()
|
||||
var calendarCells: List<CalendarCell> = emptyList()
|
||||
|
||||
val day: Int
|
||||
|
||||
init {
|
||||
val editable = calendar.dataset["editable"]?.toBoolean() ?: false
|
||||
day = calendar.dataset["day"]?.toIntOrNull() ?: -1
|
||||
|
||||
calendarEntries = document.getElementsByClassName("calendar-entry")
|
||||
.iterator().asSequence().map{ CalendarEntry(this, it) }.onEach { it.editable = editable }.toList()
|
||||
|
||||
calendarCells = document.getElementsByClassName("calendar-cell")
|
||||
.iterator().asSequence().filter { it.dataset["time"] != null }.map(::CalendarCell).toList()
|
||||
|
||||
if (editable) {
|
||||
CalendarEdit(this, calendar.querySelector(".calendar-edit") as HTMLElement)
|
||||
}
|
||||
|
||||
ScheduleRepository.onCreate {
|
||||
launch {
|
||||
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
|
||||
calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable }
|
||||
}
|
||||
}
|
||||
ScheduleRepository.onUpdate {
|
||||
launch {
|
||||
val schedule = ScheduleRepository.get(it) ?: throw NoSuchElementException()
|
||||
var found = false
|
||||
for (entry in calendarEntries) {
|
||||
if (entry.scheduleId == it) {
|
||||
entry.load(schedule)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
calendarEntries += CalendarEntry.create(this, schedule).also { it.editable = editable }
|
||||
}
|
||||
}
|
||||
}
|
||||
ScheduleRepository.onDelete {
|
||||
for (entry in calendarEntries) {
|
||||
if (entry.scheduleId == it) {
|
||||
entry.html.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initCalendar() {
|
||||
Calendar(document.getElementsByClassName("calendar")[0] as? HTMLElement ?: return)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package de.kif.frontend.views.calendar
|
||||
|
||||
import de.kif.common.model.Room
|
||||
import de.kif.frontend.repository.RoomRepository
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.get
|
||||
|
||||
class CalendarCell(view: HTMLElement) : ViewCollection<CalendarEntry>(view) {
|
||||
val day = dataset["day"]?.toIntOrNull() ?: 0
|
||||
val time = dataset["time"]?.toIntOrNull() ?: 0
|
||||
val roomId = dataset["room"]?.toLongOrNull() ?: 0
|
||||
|
||||
private lateinit var room: Room
|
||||
|
||||
suspend fun getRoom(): Room {
|
||||
if (this::room.isInitialized) {
|
||||
return room
|
||||
}
|
||||
|
||||
room = RoomRepository.get(roomId) ?: throw NoSuchElementException()
|
||||
return room
|
||||
}
|
||||
|
||||
init {
|
||||
(view.getElementsByClassName("calendar-link")[0] as? HTMLElement)?.remove()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package de.kif.frontend.views.calendar
|
||||
|
||||
import de.kif.common.Search
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.WorkGroupRepository
|
||||
import de.westermann.kobserve.list.filterObservable
|
||||
import de.westermann.kobserve.list.observableListOf
|
||||
import de.westermann.kobserve.list.sortObservable
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.components.Button
|
||||
import de.westermann.kwebview.components.InputView
|
||||
import de.westermann.kwebview.components.ListView
|
||||
import de.westermann.kwebview.extra.listFactory
|
||||
import org.w3c.dom.HTMLButtonElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
|
||||
class CalendarEdit(
|
||||
private val calendar: Calendar, view: HTMLElement
|
||||
) : View(view) {
|
||||
|
||||
private val toggleEditButton =
|
||||
Button.wrap(view.querySelector(".calendar-edit-top button") as HTMLButtonElement)
|
||||
|
||||
val search =
|
||||
InputView.wrap(view.querySelector(".calendar-edit-search input") as HTMLInputElement)
|
||||
|
||||
val listView = ListView.wrap<CalendarWorkGroup>(
|
||||
view.querySelector(".calendar-edit-list") as HTMLElement
|
||||
)
|
||||
|
||||
private var loaded = false
|
||||
|
||||
val workGroupList = observableListOf<CalendarWorkGroup>()
|
||||
private val sortedList = workGroupList.sortObservable(compareBy {
|
||||
it.workGroup.name
|
||||
}).filterObservable(search.valueProperty) { entry, search ->
|
||||
val s = entry.workGroup.createSearch()
|
||||
Search.match(search, s)
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
if (loaded) return
|
||||
loaded = true
|
||||
|
||||
launch {
|
||||
for (workGroup in WorkGroupRepository.all()) {
|
||||
workGroupList += CalendarWorkGroup(calendar, this, workGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
toggleEditButton.onClick {
|
||||
calendar.classList.toggle("edit")
|
||||
|
||||
if (!loaded) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
WorkGroupRepository.onCreate {
|
||||
if (loaded) {
|
||||
launch {
|
||||
workGroupList += CalendarWorkGroup(
|
||||
calendar,
|
||||
this,
|
||||
WorkGroupRepository.get(it)!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listView.listFactory(sortedList)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
package de.kif.frontend.views.calendar
|
||||
|
||||
import de.kif.common.CALENDAR_GRID_WIDTH
|
||||
import de.kif.common.model.Schedule
|
||||
import de.kif.common.model.WorkGroup
|
||||
import de.kif.frontend.iterator
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.RepositoryDelegate
|
||||
import de.kif.frontend.repository.ScheduleRepository
|
||||
import de.westermann.kwebview.Point
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.components.Body
|
||||
import de.westermann.kwebview.createHtmlView
|
||||
import de.westermann.kwebview.toPoint
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.MouseEvent
|
||||
import kotlin.dom.appendText
|
||||
import kotlin.dom.isText
|
||||
|
||||
class CalendarEntry(private val calendar: Calendar, view: HTMLElement) : View(view) {
|
||||
|
||||
private lateinit var mouseDelta: Point
|
||||
private var newCell: CalendarCell? = null
|
||||
|
||||
private var language by dataset.property("language")
|
||||
|
||||
var scheduleId = dataset["id"]?.toLongOrNull() ?: -1
|
||||
|
||||
val schedule =
|
||||
RepositoryDelegate(ScheduleRepository, scheduleId)
|
||||
|
||||
private lateinit var workGroup: WorkGroup
|
||||
|
||||
var pending by classList.property("pending")
|
||||
|
||||
var editable: Boolean = false
|
||||
|
||||
private fun onMove(event: MouseEvent) {
|
||||
val position = event.toPoint() - mouseDelta
|
||||
|
||||
val cell = calendar.calendarCells.find {
|
||||
position in it.dimension
|
||||
}
|
||||
|
||||
if (cell != null) {
|
||||
cell += this
|
||||
|
||||
if (newCell == null) {
|
||||
style {
|
||||
left = "0"
|
||||
top = "0.1rem"
|
||||
}
|
||||
}
|
||||
|
||||
newCell = cell
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
private fun onFinishMove(event: MouseEvent) {
|
||||
classList -= "drag"
|
||||
|
||||
newCell?.let { cell ->
|
||||
launch {
|
||||
val newTime = cell.time
|
||||
val newRoom = cell.getRoom()
|
||||
|
||||
pending = true
|
||||
|
||||
if (scheduleId < 0) {
|
||||
ScheduleRepository.create(
|
||||
Schedule(
|
||||
null,
|
||||
workGroup,
|
||||
newRoom,
|
||||
calendar.day,
|
||||
newTime
|
||||
)
|
||||
)
|
||||
html.remove()
|
||||
} else {
|
||||
ScheduleRepository.update(schedule.get().copy(room = newRoom, time = newTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
newCell = null
|
||||
|
||||
for (it in listeners) {
|
||||
it.detach()
|
||||
}
|
||||
listeners = emptyList()
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
private var listeners: List<de.westermann.kobserve.event.EventListener<*>> = emptyList()
|
||||
|
||||
fun startDrag() {
|
||||
listeners = listOf(
|
||||
Body.onMouseMove.reference(this::onMove),
|
||||
Body.onMouseUp.reference(this::onFinishMove),
|
||||
Body.onMouseLeave.reference(this::onFinishMove)
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
onMouseDown { event ->
|
||||
if (!editable || event.target != html || "pending" in classList) {
|
||||
event.stopPropagation()
|
||||
return@onMouseDown
|
||||
}
|
||||
|
||||
launch {
|
||||
classList += "drag"
|
||||
|
||||
val s = schedule.get()
|
||||
val time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
|
||||
|
||||
val p = calendar.calendarCells.find {
|
||||
it.day == s.day && it.time == time && it.roomId == s.room.id
|
||||
}?.dimension?.center ?: dimension.center
|
||||
|
||||
mouseDelta = event.toPoint() - p
|
||||
|
||||
startDrag()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(schedule: Schedule) {
|
||||
pending = false
|
||||
|
||||
if (schedule.id != null) scheduleId = schedule.id
|
||||
this.schedule.set(schedule)
|
||||
|
||||
style {
|
||||
val pos = (schedule.time % CALENDAR_GRID_WIDTH) / CALENDAR_GRID_WIDTH.toDouble()
|
||||
|
||||
val ps = "${pos * 100}%"
|
||||
|
||||
left = ps
|
||||
top = "calc($ps + 0.1rem)"
|
||||
}
|
||||
|
||||
load(schedule.workGroup)
|
||||
|
||||
val time = schedule.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH
|
||||
val cell = calendar.calendarCells.find {
|
||||
it.day == schedule.day && it.time == time && it.roomId == schedule.room.id
|
||||
}
|
||||
|
||||
if (cell != null && cell.html != html.parentElement) {
|
||||
cell += this
|
||||
}
|
||||
}
|
||||
|
||||
fun load(workGroup: WorkGroup) {
|
||||
pending = false
|
||||
|
||||
language = workGroup.language.code
|
||||
this.workGroup = workGroup
|
||||
|
||||
style {
|
||||
val size = workGroup.length / CALENDAR_GRID_WIDTH.toDouble()
|
||||
|
||||
val sz = "${size * 100}%"
|
||||
|
||||
width = sz
|
||||
height = "calc($sz - 0.2rem)"
|
||||
|
||||
if (workGroup.track?.color != null) {
|
||||
backgroundColor = workGroup.track.color.toString()
|
||||
color = workGroup.track.color.calcTextColor().toString()
|
||||
}
|
||||
}
|
||||
|
||||
for (element in html.childNodes) {
|
||||
if (element.isText) {
|
||||
html.removeChild(element)
|
||||
}
|
||||
}
|
||||
|
||||
html.appendText(workGroup.name)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(calendar: Calendar, schedule: Schedule): CalendarEntry {
|
||||
val entry = CalendarEntry(calendar, createHtmlView())
|
||||
|
||||
entry.load(schedule)
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
fun create(calendar: Calendar, workGroup: WorkGroup): CalendarEntry {
|
||||
val entry = CalendarEntry(calendar, createHtmlView())
|
||||
|
||||
entry.load(workGroup)
|
||||
entry.mouseDelta = Point.ZERO
|
||||
entry.startDrag()
|
||||
|
||||
return entry
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package de.kif.frontend.views.calendar
|
||||
|
||||
import de.kif.common.CALENDAR_GRID_WIDTH
|
||||
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.HTMLAnchorElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.events.EventListener
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time - 10))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time - 5))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time / CALENDAR_GRID_WIDTH * CALENDAR_GRID_WIDTH))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time + 5))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
val s = entry.schedule.get()
|
||||
ScheduleRepository.update(s.copy(time = s.time + 10))
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
entry.pending = true
|
||||
launch {
|
||||
ScheduleRepository.delete(entry.scheduleId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package de.kif.frontend.views.calendar
|
||||
|
||||
import de.kif.common.model.WorkGroup
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.WorkGroupRepository
|
||||
import de.westermann.kwebview.View
|
||||
|
||||
class CalendarWorkGroup(
|
||||
private val calendar: Calendar,
|
||||
private val calendarEdit: CalendarEdit,
|
||||
workGroup: WorkGroup
|
||||
) : View() {
|
||||
private var language by dataset.property("language")
|
||||
|
||||
lateinit var workGroup: WorkGroup
|
||||
private set
|
||||
|
||||
private fun load(workGroup: WorkGroup) {
|
||||
this.workGroup = workGroup
|
||||
|
||||
html.textContent = workGroup.name
|
||||
|
||||
language = workGroup.language.code
|
||||
|
||||
style {
|
||||
if (workGroup.track?.color != null) {
|
||||
backgroundColor = workGroup.track.color.toString()
|
||||
color = workGroup.track.color.calcTextColor().toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
load(workGroup)
|
||||
|
||||
val references: MutableList<de.westermann.kobserve.event.EventListener<*>> = mutableListOf()
|
||||
|
||||
references += WorkGroupRepository.onUpdate.reference {
|
||||
if (it == workGroup.id) {
|
||||
launch {
|
||||
load(WorkGroupRepository.get(it)!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
references += WorkGroupRepository.onDelete.reference {
|
||||
if (it == workGroup.id) {
|
||||
calendarEdit.workGroupList -= this
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown {
|
||||
CalendarEntry.create(calendar, workGroup)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import kotlin.dom.clear
|
|||
/**
|
||||
* @author lars
|
||||
*/
|
||||
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Iterable<V> {
|
||||
abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) : View(view), Collection<V> {
|
||||
|
||||
private val children: MutableList<V> = mutableListOf()
|
||||
|
||||
|
@ -29,6 +29,36 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
|
|||
}
|
||||
}
|
||||
|
||||
fun replace(oldView: V, newView: V) {
|
||||
if (children.contains(oldView)) {
|
||||
children.add(children.indexOf(oldView), newView)
|
||||
html.insertBefore(newView.html, oldView.html)
|
||||
children -= oldView
|
||||
html.removeChild(oldView.html)
|
||||
}
|
||||
}
|
||||
|
||||
fun add(view: V) = append(view)
|
||||
|
||||
fun add(index: Int, view: V) {
|
||||
if (index >= size) {
|
||||
append(view)
|
||||
} else {
|
||||
html.insertBefore(view.html, children[index].html)
|
||||
children.add(index, view)
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(index: Int): V {
|
||||
return children[index]
|
||||
}
|
||||
|
||||
fun removeAt(index: Int) {
|
||||
if (index in 0 until size) {
|
||||
remove(children[index])
|
||||
}
|
||||
}
|
||||
|
||||
fun toForeground(view: V) {
|
||||
if (view in children && children.indexOf(view) < children.size - 1) {
|
||||
remove(view)
|
||||
|
@ -48,8 +78,7 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
|
|||
|
||||
operator fun minusAssign(view: V) = remove(view)
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = children.isEmpty()
|
||||
override fun isEmpty(): Boolean = children.isEmpty()
|
||||
|
||||
fun clear() {
|
||||
children.clear()
|
||||
|
@ -58,16 +87,14 @@ abstract class ViewCollection<V : View>(view: HTMLElement = createHtmlView()) :
|
|||
|
||||
override fun iterator(): Iterator<V> = children.iterator()
|
||||
|
||||
val size: Int
|
||||
override val size: Int
|
||||
get() = children.size
|
||||
|
||||
operator fun contains(view: V) = children.contains(view)
|
||||
override fun contains(element: V) = children.contains(element)
|
||||
|
||||
override fun containsAll(elements: Collection<V>): Boolean = children.containsAll(elements)
|
||||
|
||||
operator fun V.unaryPlus() {
|
||||
append(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <V : View> wrap(htmlElement: HTMLElement) = object : ViewCollection<V>(htmlElement) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import org.w3c.dom.HTMLInputElement
|
|||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
abstract class ViewForLabel : View(createHtmlView<HTMLInputElement>()) {
|
||||
abstract class ViewForLabel(
|
||||
view: HTMLInputElement = createHtmlView()
|
||||
) : View(view) {
|
||||
override val html = super.html as HTMLInputElement
|
||||
|
||||
private var label: Label? = null
|
||||
|
|
|
@ -14,7 +14,7 @@ import org.w3c.dom.HTMLButtonElement
|
|||
*
|
||||
* @author lars
|
||||
*/
|
||||
class Button() : ViewCollection<View>(createHtmlView<HTMLButtonElement>()) {
|
||||
class Button(view: HTMLButtonElement = createHtmlView()) : ViewCollection<View>(view) {
|
||||
|
||||
constructor(text: String) : this() {
|
||||
this.text = text
|
||||
|
@ -38,6 +38,10 @@ class Button() : ViewCollection<View>(createHtmlView<HTMLButtonElement>()) {
|
|||
}
|
||||
|
||||
val textProperty: Property<String> = property(this::text)
|
||||
|
||||
companion object {
|
||||
fun wrap(view: HTMLButtonElement) = Button(view)
|
||||
}
|
||||
}
|
||||
|
||||
@KWebViewDsl
|
||||
|
|
|
@ -3,8 +3,8 @@ package de.westermann.kwebview.components
|
|||
import de.westermann.kobserve.Property
|
||||
import de.westermann.kobserve.ReadOnlyProperty
|
||||
import de.westermann.kobserve.ValidationProperty
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kobserve.not
|
||||
import de.westermann.kobserve.property.property
|
||||
import de.westermann.kwebview.*
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.events.Event
|
||||
|
@ -12,9 +12,10 @@ import org.w3c.dom.events.EventListener
|
|||
import org.w3c.dom.events.KeyboardEvent
|
||||
|
||||
class InputView(
|
||||
type: InputType,
|
||||
initValue: String = ""
|
||||
) : ViewForLabel() {
|
||||
type: InputType,
|
||||
initValue: String = "",
|
||||
view: HTMLInputElement = createHtmlView()
|
||||
) : ViewForLabel(view) {
|
||||
|
||||
fun bind(property: ReadOnlyProperty<String>) {
|
||||
valueProperty.bind(property)
|
||||
|
@ -108,12 +109,17 @@ class InputView(
|
|||
html.addEventListener("keyup", changeListener)
|
||||
html.addEventListener("keypress", changeListener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun wrap(view: HTMLInputElement) = InputView(InputType.SEARCH, view.value, view)
|
||||
}
|
||||
}
|
||||
|
||||
enum class InputType(val html: String) {
|
||||
TEXT("text"),
|
||||
NUMBER("number"),
|
||||
PASSWORD("password");
|
||||
PASSWORD("password"),
|
||||
SEARCH("search");
|
||||
|
||||
companion object {
|
||||
fun find(html: String): InputType? = values().find { it.html == html }
|
||||
|
@ -122,33 +128,49 @@ enum class InputType(val html: String) {
|
|||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(text: String = "", init: InputView.() -> Unit = {}) =
|
||||
InputView(InputType.TEXT, text).also(this::append).also(init)
|
||||
InputView(InputType.TEXT, text).also(this::append).also(init)
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(text: ReadOnlyProperty<String>, init: InputView.() -> Unit = {}) =
|
||||
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(text: Property<String>, init: InputView.() -> Unit = {}) =
|
||||
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(text: ValidationProperty<String>, init: InputView.() -> Unit = {}) =
|
||||
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
InputView(InputType.TEXT, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: String = "", init: InputView.() -> Unit = {}) =
|
||||
InputView(type, text).also(this::append).also(init)
|
||||
fun ViewCollection<in InputView>.inputView(
|
||||
type: InputType = InputType.TEXT,
|
||||
text: String = "",
|
||||
init: InputView.() -> Unit = {}
|
||||
) =
|
||||
InputView(type, text).also(this::append).also(init)
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: ReadOnlyProperty<String>, init: InputView.() -> Unit = {}) =
|
||||
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
fun ViewCollection<in InputView>.inputView(
|
||||
type: InputType = InputType.TEXT,
|
||||
text: ReadOnlyProperty<String>,
|
||||
init: InputView.() -> Unit = {}
|
||||
) =
|
||||
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: Property<String>, init: InputView.() -> Unit = {}) =
|
||||
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
fun ViewCollection<in InputView>.inputView(
|
||||
type: InputType = InputType.TEXT,
|
||||
text: Property<String>,
|
||||
init: InputView.() -> Unit = {}
|
||||
) =
|
||||
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
|
||||
@KWebViewDsl
|
||||
fun ViewCollection<in InputView>.inputView(type: InputType = InputType.TEXT, text: ValidationProperty<String>, init: InputView.() -> Unit = {}) =
|
||||
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
fun ViewCollection<in InputView>.inputView(
|
||||
type: InputType = InputType.TEXT,
|
||||
text: ValidationProperty<String>,
|
||||
init: InputView.() -> Unit = {}
|
||||
) =
|
||||
InputView(type, text.value).also(this::append).also { it.bind(text) }.also(init)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package de.westermann.kwebview.components
|
||||
|
||||
import de.westermann.kwebview.KWebViewDsl
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
import de.westermann.kwebview.createHtmlView
|
||||
import org.w3c.dom.HTMLDivElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
class ListView<T : View>(view: HTMLElement = createHtmlView()) : ViewCollection<T>(view) {
|
||||
override val html = super.html as HTMLDivElement
|
||||
|
||||
companion object {
|
||||
fun <T : View> wrap(view: HTMLElement) = ListView<T>(view)
|
||||
}
|
||||
}
|
||||
|
||||
@KWebViewDsl
|
||||
fun <T : View> ViewCollection<in ListView<T>>.listView(
|
||||
vararg classes: String,
|
||||
init: ListView<T>.() -> Unit = {}
|
||||
): ListView<T> {
|
||||
val view = ListView<T>()
|
||||
for (c in classes) {
|
||||
view.classList += c
|
||||
}
|
||||
append(view)
|
||||
init(view)
|
||||
return view
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package de.westermann.kwebview.extra
|
||||
|
||||
import de.westermann.kobserve.list.ObservableReadOnlyList
|
||||
import de.westermann.kwebview.View
|
||||
import de.westermann.kwebview.ViewCollection
|
||||
import de.westermann.kwebview.async
|
||||
|
||||
fun <T, V : View> ViewCollection<in V>.listFactory(
|
||||
list: ObservableReadOnlyList<T>,
|
||||
factory: (T) -> V,
|
||||
animateAdd: Int? = null,
|
||||
animateRemove: Int? = null
|
||||
) {
|
||||
for (element in list) {
|
||||
+factory(element)
|
||||
}
|
||||
list.onAdd { (index, element) ->
|
||||
val view = factory(element)
|
||||
add(index, view)
|
||||
|
||||
if (animateAdd != null) {
|
||||
classList += "animate-add"
|
||||
view.classList += "active"
|
||||
|
||||
async(animateAdd) {
|
||||
classList -= "animate-add"
|
||||
view.classList -= "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
list.onRemove { (index) ->
|
||||
@Suppress("UNCHECKED_CAST") val view = this[index] as V
|
||||
|
||||
if (animateRemove == null) {
|
||||
remove(view)
|
||||
} else {
|
||||
classList += "animate-remove"
|
||||
view.classList += "active"
|
||||
|
||||
async(animateRemove) {
|
||||
classList -= "animate-remove"
|
||||
view.classList -= "active"
|
||||
remove(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
list.onUpdate { (oldIndex, newIndex, element) ->
|
||||
removeAt(oldIndex)
|
||||
add(newIndex, factory(element))
|
||||
}
|
||||
}
|
||||
|
||||
fun <V : View> ViewCollection<in V>.listFactory(
|
||||
list: ObservableReadOnlyList<V>,
|
||||
animateAdd: Int? = null,
|
||||
animateRemove: Int? = null
|
||||
) = listFactory(
|
||||
list,
|
||||
{ it },
|
||||
animateAdd,
|
||||
animateRemove
|
||||
)
|
|
@ -220,28 +220,6 @@ a {
|
|||
.table-layout-search {
|
||||
float: left;
|
||||
padding-bottom: 0.5rem !important;
|
||||
|
||||
input {
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
right: -3px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
margin-top: 1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
input:focus ~ .btn-search {
|
||||
border-color: $primary-color;
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-layout-action {
|
||||
|
@ -470,13 +448,106 @@ form {
|
|||
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-table {
|
||||
float: left;
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: visible;
|
||||
padding-bottom: 1rem;
|
||||
position: relative;
|
||||
transition: width $transitionTime;
|
||||
}
|
||||
|
||||
.calendar-edit {
|
||||
width: 16rem;
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -3rem;
|
||||
padding-top: 3rem;
|
||||
overflow: hidden;
|
||||
|
||||
.calendar-edit-main {
|
||||
position: relative;
|
||||
left: 16rem;
|
||||
opacity: 0;
|
||||
transition: left $transitionTime, opacity$transitionTime;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-edit-top {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.calendar-edit-search {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-work-group {
|
||||
position: relative;
|
||||
display: block;
|
||||
border-radius: 0.2rem;
|
||||
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;
|
||||
margin: 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;
|
||||
}
|
||||
|
||||
@include no-select()
|
||||
}
|
||||
|
||||
.calendar[data-editable = "true"].edit {
|
||||
.calendar-table {
|
||||
width: calc(100% - 16rem);
|
||||
border-right: solid 1px rgba($text-primary-color, 0.1);
|
||||
}
|
||||
|
||||
.calendar-edit-main {
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -507,6 +578,10 @@ form {
|
|||
}
|
||||
}
|
||||
|
||||
.calendar[data-editable=false] .calendar-tools {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
@ -555,6 +630,8 @@ form {
|
|||
background-color: $background-primary-color;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@include no-select()
|
||||
}
|
||||
|
||||
.calendar-table-time-to-room {
|
||||
|
@ -618,7 +695,6 @@ form {
|
|||
}
|
||||
|
||||
.calendar-cell:first-child {
|
||||
position: absolute;
|
||||
width: 6rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
|
@ -663,13 +739,6 @@ form {
|
|||
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;
|
||||
|
@ -677,6 +746,7 @@ form {
|
|||
&::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
|
@ -694,18 +764,22 @@ form {
|
|||
left: 0.1rem !important;
|
||||
right: 0.1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-cell:first-child:not(:empty)::before {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
.calendar-cell:first-child::before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&:nth-child(4n + 2) .calendar-cell::before {
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-cell:first-child {
|
||||
position: absolute;
|
||||
width: 6rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
|
|
|
@ -7,9 +7,7 @@ import de.kif.common.model.User
|
|||
import io.ktor.application.Application
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
object Main {
|
||||
@Suppress("UnusedMainParameter")
|
||||
|
@ -34,6 +32,8 @@ object Main {
|
|||
password = System.console()?.readPassword()?.toString() ?: readLine()
|
||||
}
|
||||
|
||||
println("Create root user '$username' with pw '${"*".repeat(password.length)}'")
|
||||
|
||||
UserRepository.create(
|
||||
User(
|
||||
null,
|
||||
|
|
|
@ -5,11 +5,11 @@ import de.kif.backend.isAuthenticated
|
|||
import de.kif.backend.repository.RoomRepository
|
||||
import de.kif.backend.repository.ScheduleRepository
|
||||
import de.kif.backend.repository.WorkGroupRepository
|
||||
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 de.kif.common.CALENDAR_GRID_WIDTH
|
||||
import de.kif.common.Search
|
||||
import de.kif.common.model.Permission
|
||||
import de.kif.common.model.Room
|
||||
import de.kif.common.model.Schedule
|
||||
|
@ -304,19 +304,21 @@ fun Route.calendar() {
|
|||
a("/calendar/${day + 1}") { +">" }
|
||||
}
|
||||
div("header-right") {
|
||||
a("/calendar/$day/room-to-time") {
|
||||
a("/calendar/$day/rtt") {
|
||||
+"Room to time"
|
||||
}
|
||||
a("/calendar/$day/time-to-room") {
|
||||
a("/calendar/$day/ttr") {
|
||||
+"Time to room"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div("calendar") {
|
||||
val editable = user != null
|
||||
attributes["data-day"] = day.toString()
|
||||
attributes["data-editable"] = editable.toString()
|
||||
|
||||
div {
|
||||
div("calendar-table") {
|
||||
when (orientation) {
|
||||
CalendarOrientation.ROOM_TO_TIME -> renderRoomToTime(
|
||||
day,
|
||||
|
@ -324,7 +326,7 @@ fun Route.calendar() {
|
|||
max,
|
||||
rooms,
|
||||
schedules,
|
||||
user != null
|
||||
editable
|
||||
)
|
||||
CalendarOrientation.TIME_TO_ROOM -> renderTimeToRoom(
|
||||
day,
|
||||
|
@ -332,13 +334,33 @@ fun Route.calendar() {
|
|||
max,
|
||||
rooms,
|
||||
schedules,
|
||||
user != null
|
||||
editable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (editable) {
|
||||
div("calendar-edit") {
|
||||
div("calendar-edit-top") {
|
||||
button(classes = "form-btn") {
|
||||
+"Edit"
|
||||
}
|
||||
}
|
||||
div("calendar-edit-main") {
|
||||
div("calendar-edit-search") {
|
||||
input(InputType.search, name = "search", classes = "form-control") {
|
||||
placeholder = "Search"
|
||||
value = ""
|
||||
}
|
||||
}
|
||||
div("calendar-edit-list") {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -392,10 +414,11 @@ fun Route.calendar() {
|
|||
}
|
||||
|
||||
for (u in list) {
|
||||
if (Search.match(search, u.name)) {
|
||||
val s = u.createSearch()
|
||||
if (Search.match(search, s)) {
|
||||
val href = "/calendar/$day/${room.id}/$time/${u.id}"
|
||||
entry {
|
||||
attributes["data-search"] = Search.pack(u.name)
|
||||
attributes["data-search"] = s.stringify()
|
||||
td {
|
||||
a(href) {
|
||||
+u.name
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.kif.backend.route
|
|||
|
||||
import de.kif.backend.authenticateOrRedirect
|
||||
import de.kif.backend.repository.RoomRepository
|
||||
import de.kif.backend.util.Search
|
||||
import de.kif.common.Search
|
||||
import de.kif.backend.view.MainTemplate
|
||||
import de.kif.backend.view.MenuTemplate
|
||||
import de.kif.backend.view.TableTemplate
|
||||
|
@ -64,9 +64,10 @@ fun Route.room() {
|
|||
}
|
||||
|
||||
for (u in list) {
|
||||
if (Search.match(search, u.name)) {
|
||||
val s = u.createSearch()
|
||||
if (Search.match(search, s)) {
|
||||
entry {
|
||||
attributes["data-search"] = Search.pack(u.name)
|
||||
attributes["data-search"] = s.stringify()
|
||||
td {
|
||||
+u.name
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package de.kif.backend.route
|
|||
|
||||
import de.kif.backend.authenticateOrRedirect
|
||||
import de.kif.backend.repository.TrackRepository
|
||||
import de.kif.backend.util.Search
|
||||
import de.kif.common.Search
|
||||
import de.kif.backend.view.MainTemplate
|
||||
import de.kif.backend.view.MenuTemplate
|
||||
import de.kif.backend.view.TableTemplate
|
||||
|
@ -118,9 +118,10 @@ fun Route.track() {
|
|||
}
|
||||
|
||||
for (u in list) {
|
||||
if (Search.match(search, u.name)) {
|
||||
val s = u.createSearch()
|
||||
if (Search.match(search, s)) {
|
||||
entry {
|
||||
attributes["data-search"] = Search.pack(u.name)
|
||||
attributes["data-search"] = s.stringify()
|
||||
td {
|
||||
+u.name
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ package de.kif.backend.route
|
|||
import de.kif.backend.authenticateOrRedirect
|
||||
import de.kif.backend.hashPassword
|
||||
import de.kif.backend.repository.UserRepository
|
||||
import de.kif.backend.util.Search
|
||||
import de.kif.common.Search
|
||||
import de.kif.backend.view.MainTemplate
|
||||
import de.kif.backend.view.MenuTemplate
|
||||
import de.kif.backend.view.TableTemplate
|
||||
|
@ -66,9 +66,10 @@ fun Route.user() {
|
|||
}
|
||||
|
||||
for (u in list) {
|
||||
if (Search.match(search, u.username)) {
|
||||
val s = u.createSearch()
|
||||
if (Search.match(search, s)) {
|
||||
entry {
|
||||
attributes["data-search"] = Search.pack(u.username)
|
||||
attributes["data-search"] = s.stringify()
|
||||
td {
|
||||
+u.username
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ 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.util.Search
|
||||
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
|
||||
|
@ -20,12 +20,9 @@ import io.ktor.routing.Route
|
|||
import io.ktor.routing.get
|
||||
import io.ktor.routing.post
|
||||
import io.ktor.util.toMap
|
||||
import kotlinx.css.CSSBuilder
|
||||
import kotlinx.css.Display
|
||||
import kotlinx.html.*
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.find
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.mapValues
|
||||
import kotlin.collections.set
|
||||
|
||||
fun Route.workGroup() {
|
||||
|
@ -84,34 +81,36 @@ fun Route.workGroup() {
|
|||
}
|
||||
|
||||
for (u in list) {
|
||||
if (Search.match(search, u.name)) {
|
||||
entry {
|
||||
attributes["data-search"] = Search.pack(u.name)
|
||||
td {
|
||||
+u.name
|
||||
}
|
||||
td {
|
||||
+u.interested.toString()
|
||||
}
|
||||
td {
|
||||
+(u.track?.name ?: "")
|
||||
}
|
||||
td {
|
||||
+u.projector.toString()
|
||||
}
|
||||
td {
|
||||
+u.resolution.toString()
|
||||
}
|
||||
td {
|
||||
+u.length.toString()
|
||||
}
|
||||
td {
|
||||
+u.language.toString()
|
||||
}
|
||||
td(classes = "action") {
|
||||
a("/workgroup/${u.id}") {
|
||||
i("material-icons") { +"edit" }
|
||||
}
|
||||
val s = u.createSearch()
|
||||
entry {
|
||||
attributes["style"] = CSSBuilder().apply {
|
||||
display = if (Search.match(search, s)) Display.tableRow else Display.none
|
||||
}.toString()
|
||||
attributes["data-search"] = s.stringify()
|
||||
td {
|
||||
+u.name
|
||||
}
|
||||
td {
|
||||
+u.interested.toString()
|
||||
}
|
||||
td {
|
||||
+(u.track?.name ?: "")
|
||||
}
|
||||
td {
|
||||
+u.projector.toString()
|
||||
}
|
||||
td {
|
||||
+u.resolution.toString()
|
||||
}
|
||||
td {
|
||||
+u.length.toString()
|
||||
}
|
||||
td {
|
||||
+u.language.localeName
|
||||
}
|
||||
td(classes = "action") {
|
||||
a("/workgroup/${u.id}") {
|
||||
i("material-icons") { +"edit" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -171,30 +170,31 @@ fun Route.workGroup() {
|
|||
htmlFor = "track"
|
||||
+"Track"
|
||||
}
|
||||
div("input-group") {
|
||||
select(
|
||||
classes = "form-control"
|
||||
) {
|
||||
name = "track"
|
||||
//div("input-group") {
|
||||
select(
|
||||
classes = "form-control"
|
||||
) {
|
||||
name = "track"
|
||||
|
||||
option {
|
||||
selected = (editWorkGroup.track?.id ?: -1) < 0
|
||||
value = "-1"
|
||||
+"None"
|
||||
}
|
||||
for (track in tracks) {
|
||||
option {
|
||||
selected = (editWorkGroup.track?.id ?: -1) < 0
|
||||
value = "-1"
|
||||
+"None"
|
||||
}
|
||||
for (track in tracks) {
|
||||
option {
|
||||
selected = editWorkGroup.track?.id == track.id
|
||||
value = track.id.toString()
|
||||
+track.name
|
||||
}
|
||||
selected = editWorkGroup.track?.id == track.id
|
||||
value = track.id.toString()
|
||||
+track.name
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/*
|
||||
a("/tracks", classes = "form-btn") {
|
||||
i("material-icons") { +"edit" }
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
div("form-group") {
|
||||
label {
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
package de.kif.backend.util
|
||||
|
||||
object Search {
|
||||
private fun permute(list: List<String>): List<List<String>> {
|
||||
if (list.size <= 1) return listOf(list)
|
||||
|
||||
val perm = mutableListOf<List<String>>()
|
||||
|
||||
for (elem in list) {
|
||||
val p = permute(list - elem)
|
||||
|
||||
for (x in p) {
|
||||
perm += x + elem
|
||||
}
|
||||
}
|
||||
|
||||
return perm
|
||||
}
|
||||
|
||||
fun match(search: String, vararg params: String): Boolean {
|
||||
val s = search.toLowerCase().replace(" ", "")
|
||||
|
||||
return permute(params.map { it.toLowerCase().replace(" ", "") }).any {
|
||||
it.joinToString("").contains(s)
|
||||
}
|
||||
}
|
||||
|
||||
fun pack(vararg params: String): String = params.joinToString("|") { it.toLowerCase() }
|
||||
}
|
|
@ -15,12 +15,14 @@ class TableTemplate() : Template<FlowContent> {
|
|||
override fun FlowContent.apply() {
|
||||
div("table-layout") {
|
||||
form(classes = "form-group table-layout-search") {
|
||||
input(InputType.search, name = "search", classes = "form-control") {
|
||||
placeholder = "Search"
|
||||
value = searchValue
|
||||
}
|
||||
button(type = ButtonType.submit, classes = "form-btn btn-search") {
|
||||
i("material-icons") { +"search" }
|
||||
div("input-group") {
|
||||
input(InputType.search, name = "search", classes = "form-control") {
|
||||
placeholder = "Search"
|
||||
value = searchValue
|
||||
}
|
||||
button(type = ButtonType.submit, classes = "form-btn btn-search") {
|
||||
i("material-icons") { +"search" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div("table-layout-action") {
|
||||
|
|
75
src/jvmTest/resources/sample.http
Normal file
75
src/jvmTest/resources/sample.http
Normal file
|
@ -0,0 +1,75 @@
|
|||
POST http://localhost:8080/api/authenticate
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
}
|
||||
|
||||
|
||||
> {%
|
||||
client.assert(response.body.OK === true, "Login failed!");
|
||||
client.global.set("auth_token", response.headers.valueOf("Set-Cookie"));
|
||||
%}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:8080/api/rooms
|
||||
Content-Type: application/json
|
||||
Cookie: {{auth_token}}
|
||||
|
||||
{
|
||||
"name": "E006",
|
||||
"places": 20,
|
||||
"projector": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:8080/api/rooms
|
||||
Content-Type: application/json
|
||||
Cookie: {{auth_token}}
|
||||
|
||||
{
|
||||
"name": "E007",
|
||||
"places": 20,
|
||||
"projector": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:8080/api/rooms
|
||||
Content-Type: application/json
|
||||
Cookie: {{auth_token}}
|
||||
|
||||
{
|
||||
"name": "E008",
|
||||
"places": 20,
|
||||
"projector": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:8080/api/rooms
|
||||
Content-Type: application/json
|
||||
Cookie: {{auth_token}}
|
||||
|
||||
{
|
||||
"name": "E009",
|
||||
"places": 20,
|
||||
"projector": true
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST http://localhost:8080/api/rooms
|
||||
Content-Type: application/json
|
||||
Cookie: {{auth_token}}
|
||||
|
||||
{
|
||||
"name": "E010",
|
||||
"places": 20,
|
||||
"projector": true
|
||||
}
|
||||
|
||||
###
|
Loading…
Reference in a new issue