UI changes, slight database changes, autocompleting text field
This commit is contained in:
parent
2c4de6d8aa
commit
c8b605353c
|
@ -34,7 +34,7 @@ fun App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Surface(Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) {
|
||||||
Box(Modifier.padding(10.dp)) {
|
Box {
|
||||||
stack.last().content { stack += (it) }
|
stack.last().content { stack += (it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,12 +32,14 @@ object Students : UUIDTable("students") {
|
||||||
val note = text("note")
|
val note = text("note")
|
||||||
}
|
}
|
||||||
|
|
||||||
object GroupStudents : CompositeIdTable("grpStudents") {
|
object GroupStudents : UUIDTable("grpStudents") {
|
||||||
val groupId = reference("group_id", Groups.id)
|
val groupId = reference("group_id", Groups.id)
|
||||||
val studentId = reference("student_id", Students.id)
|
val studentId = reference("student_id", Students.id)
|
||||||
val role = varchar("role", 50).nullable()
|
val role = varchar("role", 50).nullable()
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(groupId, studentId)
|
init {
|
||||||
|
uniqueIndex(groupId, studentId) // can't figure out how to make this a composite key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object EditionStudents : Table("editionStudents") {
|
object EditionStudents : Table("editionStudents") {
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Edition(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
val groups by Group referrersOn Groups.editionId
|
val groups by Group referrersOn Groups.editionId
|
||||||
val soloStudents by Student via EditionStudents
|
val soloStudents by Student via EditionStudents
|
||||||
val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId
|
val soloAssignments by SoloAssignment referrersOn SoloAssignments.editionId
|
||||||
// val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
|
val groupAssignments by GroupAssignment referrersOn GroupAssignments.editionId
|
||||||
}
|
}
|
||||||
|
|
||||||
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
|
@ -32,6 +32,15 @@ class Group(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
var edition by Edition referencedOn Groups.editionId
|
var edition by Edition referencedOn Groups.editionId
|
||||||
var name by Groups.name
|
var name by Groups.name
|
||||||
val students by Student via GroupStudents
|
val students by Student via GroupStudents
|
||||||
|
val studentRoles by GroupMember referrersOn GroupStudents.groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupMember(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
|
companion object : EntityClass<UUID, GroupMember>(GroupStudents)
|
||||||
|
|
||||||
|
var student by Student referencedOn GroupStudents.studentId
|
||||||
|
var group by Group referencedOn GroupStudents.groupId
|
||||||
|
var role by GroupStudents.role
|
||||||
}
|
}
|
||||||
|
|
||||||
class Student(id: EntityID<UUID>) : Entity<UUID>(id) {
|
class Student(id: EntityID<UUID>) : Entity<UUID>(id) {
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupAssignmentView(state: GroupAssignmentState) {
|
||||||
|
val (course, edition) = state.editionCourse
|
||||||
|
val name by state.name
|
||||||
|
val task by state.task
|
||||||
|
val allFeedback by state.feedback.entities
|
||||||
|
|
||||||
|
var idx by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
PaneHeader(name, "group assignment", course, edition)
|
||||||
|
if(allFeedback.any { it.second.feedback == null }) {
|
||||||
|
Text("Groups in bold have no feedback yet.", fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text("All groups have feedback.", fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
|
||||||
|
TabRow(idx) {
|
||||||
|
Tab(idx == 0, { idx = 0 }) { Text("Assignment") }
|
||||||
|
allFeedback.forEachIndexed { i, it ->
|
||||||
|
val (group, feedback) = it
|
||||||
|
Tab(idx == i + 1, { idx = i + 1 }) {
|
||||||
|
Text(group.name, fontWeight = feedback.feedback?.let { FontWeight.Normal } ?: FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(idx == 0) {
|
||||||
|
var updTask by remember { mutableStateOf(task) }
|
||||||
|
OutlinedTextField(updTask, { updTask = it }, Modifier.fillMaxWidth().weight(1f), singleLine = false, minLines = 5, label = { Text("Task") })
|
||||||
|
CancelSaveRow(updTask != task, { updTask = task }, "Reset", "Update") { state.updateTask(updTask) }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val (group, feedback, individual) = allFeedback[idx - 1].second
|
||||||
|
groupFeedback(state, allFeedback[idx - 1].second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun groupFeedback(state: GroupAssignmentState, fdbk: GroupAssignmentState.LocalGFeedback) {
|
||||||
|
val (group, feedback, individual) = fdbk
|
||||||
|
var grade by remember(fdbk) { mutableStateOf(feedback?.grade ?: "") }
|
||||||
|
var msg by remember(fdbk) { mutableStateOf(TextFieldValue(feedback?.feedback ?: "")) }
|
||||||
|
var idx by remember(fdbk) { mutableStateOf(0) }
|
||||||
|
val suggestions by state.autofill.entities
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Surface(Modifier.weight(0.25f), tonalElevation = 10.dp) {
|
||||||
|
LazyColumn(Modifier.fillMaxHeight().padding(10.dp)) {
|
||||||
|
item {
|
||||||
|
Surface(
|
||||||
|
Modifier.fillMaxWidth().clickable { idx = 0 },
|
||||||
|
tonalElevation = if (idx == 0) 50.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text("Group feedback", Modifier.padding(5.dp), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(individual.toList()) { i, (student, details) ->
|
||||||
|
val (role, _) = details
|
||||||
|
Surface(
|
||||||
|
Modifier.fillMaxWidth().clickable { idx = i + 1 },
|
||||||
|
tonalElevation = if (idx == i + 1) 50.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Text("${student.name} (${role ?: "no role"})", Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.75f).padding(10.dp)) {
|
||||||
|
if(idx == 0) {
|
||||||
|
Row {
|
||||||
|
Text("Grade: ", Modifier.align(Alignment.CenterVertically))
|
||||||
|
OutlinedTextField(grade, { grade = it }, Modifier.weight(0.2f))
|
||||||
|
Spacer(Modifier.weight(0.6f))
|
||||||
|
Button({ state.upsertGroupFeedback(group, msg.text, grade) }, Modifier.weight(0.2f).align(Alignment.CenterVertically),
|
||||||
|
enabled = grade.isNotBlank() || msg.text.isNotBlank()) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutocompleteLineField(
|
||||||
|
msg, { msg = it }, Modifier.fillMaxWidth().weight(1f), { Text("Feedback") }
|
||||||
|
) { filter ->
|
||||||
|
suggestions.filter { x -> x.trim().startsWith(filter.trim()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,8 @@ package com.jaytux.grader.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.OutlinedTextField
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
@ -18,11 +15,7 @@ import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogWindow
|
|
||||||
import androidx.compose.ui.window.WindowPosition
|
|
||||||
import androidx.compose.ui.window.rememberDialogState
|
|
||||||
import com.jaytux.grader.UiRoute
|
import com.jaytux.grader.UiRoute
|
||||||
import com.jaytux.grader.data.Edition
|
import com.jaytux.grader.data.Edition
|
||||||
import com.jaytux.grader.viewmodel.CourseListState
|
import com.jaytux.grader.viewmodel.CourseListState
|
||||||
|
@ -34,26 +27,14 @@ fun CoursesView(state: CourseListState, push: (UiRoute) -> Unit) {
|
||||||
val data by state.courses.entities
|
val data by state.courses.entities
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if(data.isEmpty()) {
|
ListOrEmpty(
|
||||||
Box(Modifier.fillMaxSize()) {
|
data,
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
{ Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally)) },
|
||||||
Text("You have no courses yet.", Modifier.align(Alignment.CenterHorizontally))
|
{ Text("Add a course") },
|
||||||
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
|
{ showDialog = true },
|
||||||
Text("Add a course")
|
addAfterLazy = false
|
||||||
}
|
) { _, it ->
|
||||||
}
|
CourseWidget(state.getEditions(it), { state.delete(it) }, push)
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LazyColumn(Modifier.fillMaxSize()) {
|
|
||||||
items(data) { CourseWidget(state.getEditions(it), { state.delete(it) }, push) }
|
|
||||||
|
|
||||||
item {
|
|
||||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
|
||||||
Text("Add course")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
if(showDialog) AddStringDialog("Course name", data.map { it.name }, { showDialog = false }) { state.new(it) }
|
||||||
|
|
|
@ -21,82 +21,126 @@ import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.rememberDialogState
|
import androidx.compose.ui.window.rememberDialogState
|
||||||
import com.jaytux.grader.data.*
|
import com.jaytux.grader.data.*
|
||||||
import com.jaytux.grader.viewmodel.EditionState
|
import com.jaytux.grader.viewmodel.EditionState
|
||||||
|
import com.jaytux.grader.viewmodel.GroupAssignmentState
|
||||||
|
import com.jaytux.grader.viewmodel.GroupState
|
||||||
|
import com.jaytux.grader.viewmodel.StudentState
|
||||||
|
|
||||||
|
enum class Panel { Student, Group, Solo, GroupAs }
|
||||||
|
data class Current(val p: Panel, val i: Int)
|
||||||
|
fun Current?.studentIdx() = this?.let { if(p == Panel.Student) i else null }
|
||||||
|
fun Current?.groupIdx() = this?.let { if(p == Panel.Group) i else null }
|
||||||
|
fun Current?.soloIdx() = this?.let { if(p == Panel.Solo) i else null }
|
||||||
|
fun Current?.groupAsIdx() = this?.let { if(p == Panel.GroupAs) i else null }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EditionView(state: EditionState) = Row {
|
fun EditionView(state: EditionState) = Row(Modifier.padding(0.dp)) {
|
||||||
var isGroup by remember { mutableStateOf(false) }
|
var isGroup by remember { mutableStateOf(false) }
|
||||||
|
var idx by remember { mutableStateOf<Current?>(null) }
|
||||||
|
|
||||||
val students by state.students.entities
|
val students by state.students.entities
|
||||||
val groups by state.groups.entities
|
val groups by state.groups.entities
|
||||||
val solo by state.solo.entities
|
val solo by state.solo.entities
|
||||||
val groupAs by state.groupAs//.entities
|
val groupAs by state.groupAs.entities
|
||||||
|
|
||||||
TabLayout(
|
val toggle = { i: Int, p: Panel ->
|
||||||
listOf("Students", "Groups"),
|
idx = if(idx?.p == p && idx?.i == i) null else Current(p, i)
|
||||||
if(isGroup) 1 else 0,
|
}
|
||||||
{ isGroup = it == 1 },
|
|
||||||
{ Text(it) },
|
|
||||||
Modifier.weight(0.25f)
|
Surface(Modifier.weight(0.25f), tonalElevation = 5.dp) {
|
||||||
) {
|
TabLayout(
|
||||||
Column(Modifier.fillMaxSize()) {
|
listOf("Students", "Groups"),
|
||||||
if(isGroup) {
|
if (isGroup) 1 else 0,
|
||||||
Box(Modifier.weight(0.5f)) {
|
{ isGroup = it == 1 },
|
||||||
GroupsWidget(state.course, state.edition, groups, {}) { state.newGroup(it) }
|
{ Text(it) }
|
||||||
}
|
) {
|
||||||
Box(Modifier.weight(0.5f)) { GroupAssignmentsWidget(groupAs, {}) {} }
|
Column(Modifier.fillMaxSize()) {
|
||||||
}
|
if (isGroup) {
|
||||||
else {
|
Box(Modifier.weight(0.5f)) {
|
||||||
Box(Modifier.weight(0.5f)) {
|
GroupsWidget(
|
||||||
StudentsWidget(state.course, state.edition, students, {}) { name, note, contact, addToEdition ->
|
state.course,
|
||||||
state.newStudent(name, note, contact, addToEdition)
|
state.edition,
|
||||||
|
groups,
|
||||||
|
idx.groupIdx(),
|
||||||
|
{ toggle(it, Panel.Group) }) {
|
||||||
|
state.newGroup(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(0.5f)) {
|
||||||
|
GroupAssignmentsWidget(
|
||||||
|
state.course, state.edition, groupAs, idx.groupAsIdx(), { toggle(it, Panel.GroupAs) }
|
||||||
|
) {
|
||||||
|
state.newGroupAssignment(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(Modifier.weight(0.5f)) {
|
||||||
|
StudentsWidget(
|
||||||
|
state.course, state.edition, students, idx.studentIdx(), { toggle(it, Panel.Student) }
|
||||||
|
) { name, note, contact, addToEdition ->
|
||||||
|
state.newStudent(name, contact, note, addToEdition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(0.5f)) {
|
||||||
|
AssignmentsWidget(
|
||||||
|
state.course, state.edition, solo, idx.soloIdx(), { toggle(it, Panel.Solo) }
|
||||||
|
) {
|
||||||
|
state.newSoloAssignment(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(0.5f)) { AssignmentsWidget(solo, {}) {} }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(0.75f)) {}
|
Box(Modifier.weight(0.75f)) {
|
||||||
|
idx?.let { i ->
|
||||||
|
when(i.p) {
|
||||||
|
Panel.Student -> StudentView(StudentState(students[i.i], state.edition))
|
||||||
|
Panel.Group -> GroupView(GroupState(groups[i.i]))
|
||||||
|
Panel.GroupAs -> GroupAssignmentView(GroupAssignmentState(groupAs[i.i]))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> EditionSideWidget(
|
||||||
|
course: Course, edition: Edition, header: String, hasNoX: String, addX: String,
|
||||||
|
data: List<T>, selected: Int?, onSelect: (Int) -> Unit,
|
||||||
|
singleWidget: @Composable (T) -> Unit,
|
||||||
|
dialog: @Composable (onExit: () -> Unit) -> Unit
|
||||||
|
) = Column(Modifier.padding(10.dp)) {
|
||||||
|
Text(header, style = MaterialTheme.typography.headlineMedium)
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ListOrEmpty(
|
||||||
|
data,
|
||||||
|
{ Text("Course ${course.name} (edition ${edition.name})\nhas no $hasNoX yet.", Modifier.align(Alignment.CenterHorizontally), textAlign = TextAlign.Center) },
|
||||||
|
{ Text("Add $addX") },
|
||||||
|
{ showDialog = true }
|
||||||
|
) { idx, it ->
|
||||||
|
Surface(
|
||||||
|
Modifier.fillMaxWidth().clickable { onSelect(idx) },
|
||||||
|
tonalElevation = if (selected == idx) 50.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
singleWidget(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(showDialog) dialog { showDialog = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StudentsWidget(
|
fun StudentsWidget(
|
||||||
course: Course,
|
course: Course, edition: Edition, students: List<Student>, selected: Int?, onSelect: (Int) -> Unit,
|
||||||
edition: Edition,
|
|
||||||
students: List<Student>,
|
|
||||||
onSelect: (Int) -> Unit,
|
|
||||||
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
onAdd: (name: String, note: String, contact: String, addToEdition: Boolean) -> Unit
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = EditionSideWidget(
|
||||||
Text("Student list", style = MaterialTheme.typography.headlineMedium)
|
course, edition, "Student list", "students", "a student", students, selected, onSelect,
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||||
if(students.isEmpty()) {
|
) { onExit ->
|
||||||
Box(Modifier.fillMaxSize()) {
|
StudentDialog(course, edition, onExit, onAdd)
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
|
||||||
Text(
|
|
||||||
"Course ${course.name} (edition ${edition.name})\nhas no students yet.",
|
|
||||||
Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
|
|
||||||
Text("Add a student")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
|
||||||
itemsIndexed(students) { idx, it ->
|
|
||||||
Surface(Modifier.fillMaxWidth().clickable { onSelect(idx) }) {
|
|
||||||
Text(it.name, Modifier.padding(5.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
|
||||||
Text("Add a student")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(showDialog) StudentDialog(course, edition, { showDialog = false }, onAdd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -135,52 +179,33 @@ fun StudentDialog(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupsWidget(
|
fun GroupsWidget(
|
||||||
course: Course,
|
course: Course, edition: Edition, groups: List<Group>, selected: Int?, onSelect: (Int) -> Unit,
|
||||||
edition: Edition,
|
|
||||||
groups: List<Group>,
|
|
||||||
onSelect: (Int) -> Unit,
|
|
||||||
onAdd: (name: String) -> Unit
|
onAdd: (name: String) -> Unit
|
||||||
) = Column(Modifier.padding(10.dp)) {
|
) = EditionSideWidget(
|
||||||
Text("Group list", style = MaterialTheme.typography.headlineMedium)
|
course, edition, "Group list", "groups", "a group", groups, selected, onSelect,
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||||
|
) { onExit ->
|
||||||
if(groups.isEmpty()) {
|
AddStringDialog("Group name", groups.map { it.name }, onExit) { onAdd(it) }
|
||||||
Box(Modifier.fillMaxSize()) {
|
|
||||||
Column(Modifier.align(Alignment.Center)) {
|
|
||||||
Text(
|
|
||||||
"Course ${course.name} (edition ${edition.name})\nhas no groups yet.",
|
|
||||||
Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Button({ showDialog = true }, Modifier.align(Alignment.CenterHorizontally)) {
|
|
||||||
Text("Add a group")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
|
||||||
itemsIndexed(groups) { idx, it ->
|
|
||||||
Surface(Modifier.fillMaxWidth().clickable { onSelect(idx) }) {
|
|
||||||
Text(it.name, Modifier.padding(5.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button({ showDialog = true }, Modifier.fillMaxWidth()) {
|
|
||||||
Text("Add a group")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(showDialog) AddStringDialog("Group name", groups.map { it.name }, { showDialog = false }) { onAdd(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AssignmentsWidget(assignments: List<SoloAssignment>, onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit) {
|
fun AssignmentsWidget(
|
||||||
//
|
course: Course, edition: Edition, assignments: List<SoloAssignment>, selected: Int?,
|
||||||
|
onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit
|
||||||
|
) = EditionSideWidget(
|
||||||
|
course, edition, "Assignment list", "assignments", "an assignment", assignments, selected, onSelect,
|
||||||
|
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||||
|
) { onExit ->
|
||||||
|
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupAssignmentsWidget(assignments: List<GroupAssignment>, onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit) {
|
fun GroupAssignmentsWidget(
|
||||||
//
|
course: Course, edition: Edition, assignments: List<GroupAssignment>, selected: Int?,
|
||||||
|
onSelect: (Int) -> Unit, onAdd: (name: String) -> Unit
|
||||||
|
) = EditionSideWidget(
|
||||||
|
course, edition, "Group assignment list", "group assignments", "an assignment", assignments, selected, onSelect,
|
||||||
|
{ Text(it.name, Modifier.padding(5.dp)) }
|
||||||
|
) { onExit ->
|
||||||
|
AddStringDialog("Assignment title", assignments.map { it.name }, onExit) { onAdd(it) }
|
||||||
}
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogWindow
|
||||||
|
import androidx.compose.ui.window.WindowPosition
|
||||||
|
import androidx.compose.ui.window.rememberDialogState
|
||||||
|
import com.jaytux.grader.viewmodel.GroupState
|
||||||
|
import com.jaytux.grader.viewmodel.StudentState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StudentView(state: StudentState) {
|
||||||
|
val groups by state.groups.entities
|
||||||
|
val courses by state.courseEditions.entities
|
||||||
|
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
PaneHeader(state.student.name, "student", state.editionCourse)
|
||||||
|
Row {
|
||||||
|
Column(Modifier.padding(10.dp).weight(0.45f)) {
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
InteractToEdit(state.student.name, { state.update { this.name = it } }, "Name")
|
||||||
|
InteractToEdit(state.student.contact, { state.update { this.contact = it } }, "Contact")
|
||||||
|
InteractToEdit(state.student.note, { state.update { this.note = it } }, "Note", singleLine = false)
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(0.55f)) {}
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
Column(Modifier.weight(0.5f)) {
|
||||||
|
Text("Courses", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
ListOrEmpty(courses, { Text("Not a member of any course") }) { _, it ->
|
||||||
|
val (ed, course) = it
|
||||||
|
Text("${course.name} (${ed.name})", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(Modifier.weight(0.5f)) {
|
||||||
|
Text("Groups", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it ->
|
||||||
|
Row {
|
||||||
|
val (group, c) = it
|
||||||
|
val (course, ed) = c
|
||||||
|
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Text("(in course $course ($ed))", Modifier.align(Alignment.Bottom), style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupView(state: GroupState) {
|
||||||
|
val members by state.members.entities
|
||||||
|
val available by state.availableStudents.entities
|
||||||
|
val allRoles by state.roles.entities
|
||||||
|
val (course, edition) = state.course
|
||||||
|
|
||||||
|
var pickRole: Pair<String?, (String?) -> Unit>? by remember { mutableStateOf(null) }
|
||||||
|
|
||||||
|
Column(Modifier.padding(10.dp)) {
|
||||||
|
PaneHeader(state.group.name, "group", course, edition)
|
||||||
|
Row {
|
||||||
|
Column(Modifier.weight(0.5f)) {
|
||||||
|
Text("Students", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
ListOrEmpty(members, { Text("No students in this group") }) { _, it ->
|
||||||
|
val (student, role) = it
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
"${student.name} (${role ?: "no role"})",
|
||||||
|
Modifier.weight(0.75f).align(Alignment.CenterVertically),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
IconButton({ pickRole = role to { r -> state.updateRole(student, r) } }, Modifier.weight(0.12f)) {
|
||||||
|
Icon(Icons.Default.Edit, "Change role")
|
||||||
|
}
|
||||||
|
IconButton({ state.removeStudent(student) }, Modifier.weight(0.12f)) {
|
||||||
|
Icon(Icons.Default.Delete, "Remove student")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(0.5f)) {
|
||||||
|
Text("Available students", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
ListOrEmpty(available, { Text("No students available") }) { _, it ->
|
||||||
|
Row(Modifier.padding(5.dp)) {
|
||||||
|
IconButton({ state.addStudent(it) }) {
|
||||||
|
Icon(ChevronLeft, "Add student")
|
||||||
|
}
|
||||||
|
Text(it.name, Modifier.weight(0.75f).align(Alignment.CenterVertically), style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pickRole?.let {
|
||||||
|
val (curr, onPick) = it
|
||||||
|
RolePicker(allRoles, curr, { pickRole = null }, { role -> onPick(role); pickRole = null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RolePicker(used: List<String>, curr: String?, onClose: () -> Unit, onSave: (String?) -> Unit) = DialogWindow(
|
||||||
|
onCloseRequest = onClose,
|
||||||
|
state = rememberDialogState(size = DpSize(400.dp, 500.dp), position = WindowPosition(Alignment.Center))
|
||||||
|
) {
|
||||||
|
Surface(Modifier.fillMaxSize().padding(10.dp)) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
var role by remember { mutableStateOf(curr ?: "") }
|
||||||
|
Column {
|
||||||
|
Text("Used roles:")
|
||||||
|
LazyColumn(Modifier.weight(1.0f).padding(5.dp)) {
|
||||||
|
items(used) {
|
||||||
|
Surface(Modifier.fillMaxWidth().clickable { role = it }, tonalElevation = 5.dp) {
|
||||||
|
Text(it, Modifier.padding(5.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(role, { role = it }, Modifier.fillMaxWidth())
|
||||||
|
CancelSaveRow(true, onClose) {
|
||||||
|
onSave(role.ifBlank { null })
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +1,43 @@
|
||||||
package com.jaytux.grader.ui
|
package com.jaytux.grader.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.capitalize
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogWindow
|
import androidx.compose.ui.window.DialogWindow
|
||||||
import androidx.compose.ui.window.WindowPosition
|
import androidx.compose.ui.window.WindowPosition
|
||||||
import androidx.compose.ui.window.rememberDialogState
|
import androidx.compose.ui.window.rememberDialogState
|
||||||
|
import com.jaytux.grader.data.Course
|
||||||
|
import com.jaytux.grader.data.Edition
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, onSave: () -> Unit) {
|
fun CancelSaveRow(canSave: Boolean, onCancel: () -> Unit, cancelText: String = "Cancel", saveText: String = "Save", onSave: () -> Unit) {
|
||||||
Row {
|
Row {
|
||||||
Button({ onCancel() }, Modifier.weight(0.45f)) { Text("Cancel") }
|
Button({ onCancel() }, Modifier.weight(0.45f)) { Text(cancelText) }
|
||||||
Spacer(Modifier.weight(0.1f))
|
Spacer(Modifier.weight(0.1f))
|
||||||
Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text("Save") }
|
Button({ onSave() }, Modifier.weight(0.45f), enabled = canSave) { Text(saveText) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,3 +80,178 @@ fun AddStringDialog(label: String, taken: List<String>, onClose: () -> Unit, onS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> ListOrEmpty(
|
||||||
|
data: List<T>,
|
||||||
|
emptyText: @Composable ColumnScope.() -> Unit,
|
||||||
|
addText: @Composable RowScope.() -> Unit,
|
||||||
|
onAdd: () -> Unit,
|
||||||
|
addAfterLazy: Boolean = true,
|
||||||
|
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||||
|
) {
|
||||||
|
if(data.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
emptyText()
|
||||||
|
Button(onAdd, Modifier.align(Alignment.CenterHorizontally)) {
|
||||||
|
addText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Column {
|
||||||
|
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
||||||
|
itemsIndexed(data) { idx, it ->
|
||||||
|
item(idx, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!addAfterLazy) {
|
||||||
|
item {
|
||||||
|
Button(onAdd, Modifier.fillMaxWidth()) {
|
||||||
|
addText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addAfterLazy) {
|
||||||
|
Button(onAdd, Modifier.fillMaxWidth()) {
|
||||||
|
addText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> ListOrEmpty(
|
||||||
|
data: List<T>,
|
||||||
|
emptyText: @Composable ColumnScope.() -> Unit,
|
||||||
|
item: @Composable LazyItemScope.(idx: Int, it: T) -> Unit
|
||||||
|
) {
|
||||||
|
if(data.isEmpty()) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Column(Modifier.align(Alignment.Center)) {
|
||||||
|
emptyText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Column {
|
||||||
|
LazyColumn(Modifier.padding(5.dp).weight(1f)) {
|
||||||
|
itemsIndexed(data) { idx, it ->
|
||||||
|
item(idx, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InteractToEdit(
|
||||||
|
content: String, onSave: (String) -> Unit, pre: String, modifier: Modifier = Modifier,
|
||||||
|
w1: Float = 0.75f, w2: Float = 0.25f,
|
||||||
|
singleLine: Boolean = true
|
||||||
|
) {
|
||||||
|
var text by remember(content) { mutableStateOf(content) }
|
||||||
|
|
||||||
|
Row(modifier.padding(5.dp)) {
|
||||||
|
val base = if(singleLine) Modifier.align(Alignment.CenterVertically) else Modifier
|
||||||
|
OutlinedTextField(
|
||||||
|
text, { text = it }, base.weight(w1), label = { Text(pre) },
|
||||||
|
singleLine = singleLine, minLines = if(singleLine) 1 else 5
|
||||||
|
)
|
||||||
|
IconButton({ onSave(text) }, base.weight(w2)) { Icon(Icons.Default.Check, "Save") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PaneHeader(name: String, type: String, course: Course, edition: Edition) = Column {
|
||||||
|
Text(name, style = MaterialTheme.typography.headlineMedium)
|
||||||
|
Text("${type.capitalize(Locale.current)} in ${course.name} (${edition.name})", fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PaneHeader(name: String, type: String, courseEdition: Pair<Course, Edition>) = PaneHeader(name, type, courseEdition.first, courseEdition.second)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AutocompleteLineField(
|
||||||
|
value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
modifier: Modifier = Modifier, label: @Composable (() -> Unit)? = null,
|
||||||
|
onFilter: (String) -> List<String>
|
||||||
|
) = Column(modifier) {
|
||||||
|
var suggestions by remember { mutableStateOf(listOf<String>()) }
|
||||||
|
var selected by remember { mutableStateOf(0) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val scrollState = rememberLazyListState()
|
||||||
|
|
||||||
|
val posToLine = { pos: Int ->
|
||||||
|
(value.text.take(pos).count { it == '\n' }) to (value.text.take(pos).lastIndexOf('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
val autoComplete = { str: String ->
|
||||||
|
val pos = value.selection.start
|
||||||
|
val lines = value.text.split("\n").toMutableList()
|
||||||
|
val (lineno, lineStart) = posToLine(pos)
|
||||||
|
|
||||||
|
lines[lineno] = str
|
||||||
|
onValueChange(value.copy(text = lines.joinToString("\n"), selection = TextRange(lineStart + str.length)))
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentLine = {
|
||||||
|
value.text.split('\n')[posToLine(value.selection.start).first]
|
||||||
|
}
|
||||||
|
|
||||||
|
val gotoOption = { idx: Int ->
|
||||||
|
selected = if(suggestions.isEmpty()) 0 else ((idx + suggestions.size) % suggestions.size)
|
||||||
|
scope.launch {
|
||||||
|
scrollState.animateScrollToItem(if(suggestions.isNotEmpty()) (selected + 1) else 0)
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
val onKey = { kev: KeyEvent ->
|
||||||
|
var res = true
|
||||||
|
if(suggestions.isNotEmpty()) {
|
||||||
|
when (kev.key) {
|
||||||
|
Key.Tab -> autoComplete(suggestions[selected])
|
||||||
|
Key.DirectionUp -> gotoOption(selected - 1)
|
||||||
|
Key.DirectionDown -> gotoOption(selected + 1)
|
||||||
|
Key.Escape -> suggestions = listOf()
|
||||||
|
else -> res = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else res = false
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(value.text) {
|
||||||
|
delay(300)
|
||||||
|
suggestions = onFilter(currentLine())
|
||||||
|
gotoOption(if(suggestions.isEmpty()) 0 else selected % suggestions.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value, onValueChange,
|
||||||
|
Modifier.fillMaxWidth().weight(0.75f).onKeyEvent(onKey), label = label, singleLine = false, minLines = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
if(suggestions.isNotEmpty()) {
|
||||||
|
LazyColumn(Modifier.weight(0.25f), state = scrollState) {
|
||||||
|
stickyHeader {
|
||||||
|
Surface(tonalElevation = 5.dp) {
|
||||||
|
Text("Suggestions", Modifier.padding(5.dp).fillMaxWidth(), fontStyle = FontStyle.Italic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(suggestions) { idx, it ->
|
||||||
|
Surface(Modifier.padding(5.dp).fillMaxWidth(), tonalElevation = if(selected == idx) 50.dp else 0.dp) {
|
||||||
|
Text(it, Modifier.clickable { autoComplete(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,14 +4,13 @@ import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import com.jaytux.grader.data.*
|
import com.jaytux.grader.data.*
|
||||||
import org.jetbrains.exposed.dao.Entity
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.Transaction
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class RawDbState<T: Entity<UUID>>(private val loader: (Transaction.() -> List<T>)) {
|
fun <T> MutableState<T>.immutable(): State<T> = this
|
||||||
private fun <T> MutableState<T>.immutable(): State<T> = this@immutable
|
|
||||||
|
class RawDbState<T: Any>(private val loader: (Transaction.() -> List<T>)) {
|
||||||
|
|
||||||
private val rawEntities by lazy {
|
private val rawEntities by lazy {
|
||||||
mutableStateOf(transaction { loader() })
|
mutableStateOf(transaction { loader() })
|
||||||
|
@ -57,8 +56,8 @@ class EditionState(val edition: Edition) {
|
||||||
val course = transaction { edition.course }
|
val course = transaction { edition.course }
|
||||||
val students = RawDbState { edition.soloStudents.toList() }
|
val students = RawDbState { edition.soloStudents.toList() }
|
||||||
val groups = RawDbState { edition.groups.toList() }
|
val groups = RawDbState { edition.groups.toList() }
|
||||||
val groupAs = mutableStateOf(listOf<GroupAssignment>())
|
|
||||||
val solo = RawDbState { edition.soloAssignments.toList() }
|
val solo = RawDbState { edition.soloAssignments.toList() }
|
||||||
|
val groupAs = RawDbState { edition.groupAssignments.toList() }
|
||||||
|
|
||||||
fun newStudent(name: String, contact: String, note: String, addToEdition: Boolean) {
|
fun newStudent(name: String, contact: String, note: String, addToEdition: Boolean) {
|
||||||
transaction {
|
transaction {
|
||||||
|
@ -78,4 +77,181 @@ class EditionState(val edition: Edition) {
|
||||||
groups.refresh()
|
groups.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newSoloAssignment(name: String) {
|
||||||
|
transaction {
|
||||||
|
SoloAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = "" }
|
||||||
|
solo.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun newGroupAssignment(name: String) {
|
||||||
|
transaction {
|
||||||
|
GroupAssignment.new { this.name = name; this.edition = this@EditionState.edition; assignment = "" }
|
||||||
|
groupAs.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StudentState(val student: Student, edition: Edition) {
|
||||||
|
val editionCourse = transaction { edition.course to edition }
|
||||||
|
val groups = RawDbState { student.groups.map { it to (it.edition.course.name to it.edition.name) }.toList() }
|
||||||
|
val courseEditions = RawDbState { student.courses.map{ it to it.course }.toList() }
|
||||||
|
|
||||||
|
fun update(f: Student.() -> Unit) {
|
||||||
|
transaction {
|
||||||
|
student.f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupState(val group: Group) {
|
||||||
|
val members = RawDbState { group.studentRoles.map{ it.student to it.role }.toList() }
|
||||||
|
val availableStudents = RawDbState { Student.find {
|
||||||
|
// not yet in the group
|
||||||
|
(Students.id notInList group.students.map { it.id }) and
|
||||||
|
// but in the same course (edition)
|
||||||
|
(Students.id inList group.edition.soloStudents.map { it.id })
|
||||||
|
}.toList() }
|
||||||
|
val course = transaction { group.edition.course to group.edition }
|
||||||
|
val roles = RawDbState {
|
||||||
|
GroupStudents.select(GroupStudents.role).where{ GroupStudents.role.isNotNull() }
|
||||||
|
.withDistinct(true).map{ it[GroupStudents.role] ?: "" }.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addStudent(student: Student) {
|
||||||
|
transaction {
|
||||||
|
GroupStudents.insert {
|
||||||
|
it[groupId] = group.id
|
||||||
|
it[studentId] = student.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
members.refresh(); availableStudents.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeStudent(student: Student) {
|
||||||
|
transaction {
|
||||||
|
GroupStudents.deleteWhere { groupId eq group.id and (studentId eq student.id) }
|
||||||
|
}
|
||||||
|
members.refresh(); availableStudents.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateRole(student: Student, role: String?) {
|
||||||
|
transaction {
|
||||||
|
GroupStudents.update({ GroupStudents.groupId eq group.id and (GroupStudents.studentId eq student.id) }) {
|
||||||
|
it[this.role] = role
|
||||||
|
}
|
||||||
|
members.refresh()
|
||||||
|
roles.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupAssignmentState(val assignment: GroupAssignment) {
|
||||||
|
data class LocalFeedback(val feedback: String, val grade: String)
|
||||||
|
data class LocalGFeedback(
|
||||||
|
val group: Group,
|
||||||
|
val feedback: LocalFeedback?,
|
||||||
|
val individuals: Map<Student, Pair<String?, LocalFeedback?>>
|
||||||
|
)
|
||||||
|
|
||||||
|
val editionCourse = transaction { assignment.edition.course to assignment.edition }
|
||||||
|
private val _name = mutableStateOf(assignment.name); val name = _name.immutable()
|
||||||
|
private val _task = mutableStateOf(assignment.assignment); val task = _task.immutable()
|
||||||
|
val feedback = RawDbState { loadFeedback() }
|
||||||
|
|
||||||
|
val autofill = RawDbState {
|
||||||
|
val forGroups = GroupFeedbacks.selectAll().where { GroupFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
|
||||||
|
it[GroupFeedbacks.feedback].split('\n')
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
val forIndividuals = IndividualFeedbacks.selectAll().where { IndividualFeedbacks.groupAssignmentId eq assignment.id }.flatMap {
|
||||||
|
it[IndividualFeedbacks.feedback].split('\n')
|
||||||
|
}.toList()
|
||||||
|
|
||||||
|
(forGroups + forIndividuals).distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Transaction.loadFeedback(): List<Pair<Group, LocalGFeedback>> {
|
||||||
|
val individuals = IndividualFeedbacks.selectAll().where {
|
||||||
|
IndividualFeedbacks.groupAssignmentId eq assignment.id
|
||||||
|
}.map {
|
||||||
|
it[IndividualFeedbacks.studentId] to LocalFeedback(it[IndividualFeedbacks.feedback], it[IndividualFeedbacks.grade])
|
||||||
|
}.associate { it }
|
||||||
|
|
||||||
|
val groupFeedbacks = GroupFeedbacks.selectAll().where {
|
||||||
|
GroupFeedbacks.groupAssignmentId eq assignment.id
|
||||||
|
}.map {
|
||||||
|
it[GroupFeedbacks.groupId] to (it[GroupFeedbacks.feedback] to it[GroupFeedbacks.grade])
|
||||||
|
}.associate { it }
|
||||||
|
|
||||||
|
val groups = Group.find {
|
||||||
|
(Groups.editionId eq assignment.edition.id)
|
||||||
|
}.map { group ->
|
||||||
|
val students = group.studentRoles.associate { sR ->
|
||||||
|
val student = sR.student
|
||||||
|
val role = sR.role
|
||||||
|
val feedback = individuals[student.id]
|
||||||
|
|
||||||
|
student to (role to feedback)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupFeedbacks[group.id]?.let { (f, g) ->
|
||||||
|
group to LocalGFeedback(group, LocalFeedback(f, g), students)
|
||||||
|
} ?: (group to LocalGFeedback(group, null, students))
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upsertGroupFeedback(group: Group, msg: String, grd: String) {
|
||||||
|
transaction {
|
||||||
|
GroupFeedbacks.upsert {
|
||||||
|
it[groupAssignmentId] = assignment.id
|
||||||
|
it[groupId] = group.id
|
||||||
|
it[this.feedback] = msg
|
||||||
|
it[this.grade] = grd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feedback.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upsertIndividualFeedback(student: Student, group: Group, msg: String, grd: String) {
|
||||||
|
transaction {
|
||||||
|
IndividualFeedbacks.upsert {
|
||||||
|
it[groupAssignmentId] = assignment.id
|
||||||
|
it[groupId] = group.id
|
||||||
|
it[studentId] = student.id
|
||||||
|
it[this.feedback] = msg
|
||||||
|
it[this.grade] = grd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feedback.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTask(t: String) {
|
||||||
|
transaction {
|
||||||
|
assignment.assignment = t
|
||||||
|
}
|
||||||
|
_task.value = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ kotlin = "2.1.0"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.1"
|
||||||
exposed = "0.59.0"
|
exposed = "0.59.0"
|
||||||
material3 = "1.7.3"
|
material3 = "1.7.3"
|
||||||
|
ui-android = "1.7.8"
|
||||||
|
foundation-layout-android = "1.7.8"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
|
@ -22,6 +24,8 @@ sqlite = { group = "org.xerial", name = "sqlite-jdbc", version = "3.34.0" }
|
||||||
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
sl4j = { group = "org.slf4j", name = "slf4j-simple", version = "2.0.12" }
|
||||||
material3-core = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "material3" }
|
material3-core = { group = "org.jetbrains.compose.material3", name = "material3", version.ref = "material3" }
|
||||||
material3-desktop = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "material3" }
|
material3-desktop = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "material3" }
|
||||||
|
androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", version.ref = "ui-android" }
|
||||||
|
androidx-foundation-layout-android = { group = "androidx.compose.foundation", name = "foundation-layout-android", version.ref = "foundation-layout-android" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||||
|
|
Loading…
Reference in New Issue