diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt index bcedd60..cf20e70 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/Database.kt @@ -9,6 +9,8 @@ import com.jaytux.grader.data.v2.Courses import com.jaytux.grader.data.v2.NumericGrade import com.jaytux.grader.data.v2.v2Tables import dev.dirs.ProjectDirectories +import org.jetbrains.exposed.v1.core.dao.id.EntityID +import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.SchemaUtils import org.jetbrains.exposed.v1.jdbc.transactions.transaction import kotlin.getValue @@ -18,6 +20,9 @@ import kotlin.io.path.exists import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.batchInsert import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager +import org.jetbrains.exposed.v1.jdbc.update +import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils +import java.util.UUID object Database { val dataDir: String = ProjectDirectories.from("com", "jaytux", "grader").dataDir.also { @@ -43,6 +48,8 @@ object Database { it[CategoricGrades.id] } + var passId: EntityID? = null + var bId: EntityID? = null CategoricGradeOptions.batchInsert( listOf("Pass", "Fail").mapIndexed { idx, it -> it to pf app idx } + listOf("A (Excellent)", "B (Good)", "C (Satisfactory)", "D (Poor)", "F (Fail)").mapIndexed { idx, it -> it to af app idx } @@ -50,7 +57,15 @@ object Database { this[CategoricGradeOptions.option] = it.first this[CategoricGradeOptions.gradeId] = it.second this[CategoricGradeOptions.index] = it.third + }.forEach { + when(it[CategoricGradeOptions.option]) { + "Pass" -> passId = it[CategoricGradeOptions.id] + "B (Good)" -> bId = it[CategoricGradeOptions.id] + } } + + CategoricGrades.update(where = { CategoricGrades.id eq pf }) { it[CategoricGrades.defaultOption] = passId!! } + CategoricGrades.update(where = { CategoricGrades.id eq af }) { it[CategoricGrades.defaultOption] = bId!! } } if(NumericGrade.count() == 0L) { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/DSLv2.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/DSLv2.kt index 5064b80..c73c1db 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/DSLv2.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/DSLv2.kt @@ -142,6 +142,7 @@ object PeerEvaluationS2SEvaluations : UUIDTable("peerEvalS2SEvals") { object CategoricGrades : UUIDTable("categoricGrades") { val name = varchar("name", 50).uniqueIndex() + val defaultOption = reference("default_option_id", CategoricGradeOptions.id) } object CategoricGradeOptions : UUIDTable("categoricGradeOpts") { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/Entitiesv2.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/Entitiesv2.kt index 0c2675c..488fd5a 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/Entitiesv2.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/data/v2/Entitiesv2.kt @@ -101,6 +101,7 @@ class CategoricGrade(id: EntityID) : UUIDEntity(id) { companion object : EntityClass(CategoricGrades) var name by CategoricGrades.name + var default by CategoricGradeOption referencedOn CategoricGrades.defaultOption val options by CategoricGradeOption referrersOn CategoricGradeOptions.gradeId orderBy CategoricGradeOptions.index } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt index 57ecb5c..8bf4f47 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/AssignmentsView.kt @@ -20,6 +20,9 @@ import com.jaytux.grader.GroupGrading import com.jaytux.grader.PeerEvalGrading import com.jaytux.grader.SoloGrading import com.jaytux.grader.data.v2.AssignmentType +import com.jaytux.grader.data.v2.CategoricGrade +import com.jaytux.grader.data.v2.CategoricGradeOption +import com.jaytux.grader.data.v2.CategoricGradeOptions import com.jaytux.grader.viewmodel.EditionVM import com.jaytux.grader.viewmodel.Navigator import com.jaytux.grader.viewmodel.UiGradeType @@ -47,6 +50,8 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil var addingRubric by remember { mutableStateOf(false) } var editingRubric by remember { mutableStateOf(-1) } var updatingGrade by remember { mutableStateOf(false) } + var renaming by remember { mutableStateOf(false) } + var deleting by remember { mutableStateOf(false) } val navToGrading = lambda@{ if(assignment == null) return@lambda @@ -58,7 +63,7 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil } Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { - ListOrEmpty(assignments, { Text("No groups yet.") }) { idx, it -> + ListOrEmpty(assignments, { Text("No assignments yet.") }) { idx, it -> QuickAssignment(idx, it, vm) } } @@ -73,8 +78,21 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil val peerEvalData by vm.asPeerEvaluation.entity var updatingPeerEvalGrade by remember { mutableStateOf(false) } - Text(assignment.assignment.name, style = MaterialTheme.typography.headlineMedium) - Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic) + Column { + Row(Modifier.height(IntrinsicSize.Min)) { + EditableText( + assignment.assignment.name, style = MaterialTheme.typography.headlineMedium, + canSave = { it.isNotBlank() && (it == assignment.assignment.name || !assignments.any { x -> x.assignment.name == it }) } + ) { + vm.modAssignment(assignment.assignment, it, null) + } + Spacer(Modifier.width(10.dp)) + IconButton(Delete, "Delete assignment", Modifier.align(Alignment.CenterVertically)) { + deleting = true + } + } + Text("Deadline: ${assignment.assignment.deadline.format(fmt)}", Modifier.padding(top = 5.dp).clickable { updatingDeadline = true }, fontStyle = FontStyle.Italic) + } Row { Text("${assignment.assignment.type.display} using grading ", Modifier.align(Alignment.CenterVertically)) Surface(shape = MaterialTheme.shapes.small, tonalElevation = 10.dp) { @@ -192,6 +210,15 @@ fun AssignmentsView(vm: EditionVM, token: Navigator.NavToken) = Row(Modifier.fil } } } + + if(deleting) { + if(assignment == null) deleting = false + else { + ConfirmDeleteDialog("an assignment", { deleting = false }, { vm.rmAssignment(assignment.assignment) }) { + Text("${assignment.assignment.type.display} \"${assignment.assignment.name}\"") + } + } + } } val fmt = LocalDateTime.Format { @@ -300,7 +327,7 @@ fun AddCriterionDialog(current: EditionVM.CriterionData?, vm: EditionVM, taken: OutlinedTextField(desc, { desc = it }, Modifier.fillMaxWidth(), label = { Text("Short Description") }, singleLine = true) Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) { Column { - GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } + GradeTypePicker(type, categories, numeric, vm::mkScale, vm::modScale, vm::mkNumericScale, Modifier.weight(1f)) { type = it } CancelSaveRow(name.isNotBlank() && (name !in taken || name == current?.criterion?.name), onClose) { onSave(name, desc, type) @@ -331,7 +358,7 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose: Text("Select a grading scale for $name", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(bottom = 10.dp)) Surface(shape = MaterialTheme.shapes.small, color = Color.White, modifier = Modifier.fillMaxWidth().padding(5.dp)) { Column { - GradeTypePicker(type, categories, numeric, { n, o -> vm.mkScale(n, o) }, { n, m -> vm.mkNumericScale(n, m) }, Modifier.weight(1f)) { type = it } + GradeTypePicker(type, categories, numeric, vm::mkScale, vm::modScale, vm::mkNumericScale, Modifier.weight(1f)) { type = it } CancelSaveRow(true, onClose) { onSave(type) @@ -349,7 +376,9 @@ fun SetGradingDialog(name: String, current: UiGradeType, vm: EditionVM, onClose: @Composable fun GradeTypePicker( type: UiGradeType, categories: List, numeric: List, - mkCat: (String, List) -> Unit, mkNum: (String, Double) -> Unit, + mkCat: (String, List, Int) -> Unit, + modCat: (cat: CategoricGrade, add: List, default: Int) -> Unit, + mkNum: (String, Double) -> Unit, modifier: Modifier = Modifier, onUpdate: (UiGradeType) -> Unit ) = Column(modifier) { @@ -394,19 +423,26 @@ fun GradeTypePicker( } } (type as? UiGradeType.Categoric)?.let { + var updating by remember(type, categories) { mutableStateOf(null) } + LazyColumn(Modifier.weight(1f)) { itemsIndexed(categories) { idx, it -> Surface( tonalElevation = if (selectedCategory == idx) 15.dp else 0.dp, shape = MaterialTheme.shapes.small ) { - Column(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) { - Text(it.grade.name, fontWeight = FontWeight.Bold) - Text( - "(${it.options.size} options)", - Modifier.padding(start = 10.dp), - fontStyle = FontStyle.Italic - ) + Row(Modifier.fillMaxWidth().clickable { selectedCategory = idx; onUpdate(it) }.padding(10.dp)) { + Column(Modifier.weight(1f)) { + Text(it.grade.name, fontWeight = FontWeight.Bold) + Text( + "(${it.options.size} options; default ${it.default?.option ?: "none"})", + Modifier.padding(start = 10.dp), + fontStyle = FontStyle.Italic + ) + } + IconButton(Edit, modifier = Modifier.align(Alignment.CenterVertically)) { + updating = it + } } } } @@ -417,6 +453,11 @@ fun GradeTypePicker( } } } + + if(updating != null) ModCatScaleDialog(updating!!, { updating = null }) { categoric, add, i -> + modCat(categoric.grade, add, i) + updating = null + } } ?: (type as? UiGradeType.Numeric)?.let { LazyColumn(Modifier.weight(1f)) { itemsIndexed(numeric) { idx, it -> @@ -445,8 +486,8 @@ fun GradeTypePicker( if(adding) { when(type) { - is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options -> - mkCat(name, options) + is UiGradeType.Categoric -> AddCatScaleDialog(categories.map { it.grade.name }, { adding = false }) { name, options, idx -> + mkCat(name, options, idx) } is UiGradeType.Numeric -> AddNumScaleDialog(numeric.map { it.grade.name }, { adding = false }) { name, max -> mkNum(name, max) @@ -457,7 +498,7 @@ fun GradeTypePicker( } @Composable -fun AddCatScaleDialog(taken: List, onClose: () -> Unit, onSave: (String, List) -> Unit) = DialogWindow( +fun AddCatScaleDialog(taken: List, onClose: () -> Unit, onSave: (String, List, Int) -> Unit) = DialogWindow( onCloseRequest = onClose, state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center)) ) { @@ -465,6 +506,7 @@ fun AddCatScaleDialog(taken: List, onClose: () -> Unit, onSave: (String, var name by remember { mutableStateOf("") } var options by remember { mutableStateOf(listOf()) } var adding by remember { mutableStateOf("") } + var default by remember { mutableStateOf(0) } Surface(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize().padding(10.dp)) { @@ -474,8 +516,15 @@ fun AddCatScaleDialog(taken: List, onClose: () -> Unit, onSave: (String, LazyColumn(Modifier.weight(1f)) { itemsIndexed(options) { idx, it -> Row(Modifier.fillMaxWidth().padding(5.dp)) { - Text(it, Modifier.weight(1f)) - IconButton({ options = options.filterNot { o -> o == it } }) { + Column(Modifier.weight(1f).align(Alignment.CenterVertically)) { + Text(it) + if(idx == default) Text("(default option)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) + } + IconButton({ default = idx }, Modifier.align(Alignment.CenterVertically)) { + if(default == idx) Icon(CheckboxChecked, "Default option") + else Icon(CheckboxUnchecked, "Set as default") + } + IconButton({ options = options.filterNot { o -> o == it } }, Modifier.align(Alignment.CenterVertically)) { Icon(Delete, "Delete grading option") } } @@ -489,8 +538,77 @@ fun AddCatScaleDialog(taken: List, onClose: () -> Unit, onSave: (String, } } } - CancelSaveRow(name.isNotBlank() && name !in taken, onClose) { - onSave(name, options) + CancelSaveRow(name.isNotBlank() && name !in taken && options.isNotEmpty() && default in options.indices, onClose) { + onSave(name, options, default) + onClose() + } + } + } + } + + LaunchedEffect(Unit) { focus.requestFocus() } +} + +@Composable +fun ModCatScaleDialog( + current: UiGradeType.Categoric, onClose: () -> Unit, + onSave: (UiGradeType.Categoric, List, Int) -> Unit +) = DialogWindow( + onCloseRequest = onClose, + state = rememberDialogState(size = DpSize(750.dp, 600.dp), position = WindowPosition(Alignment.Center)) +) { + val focus = remember { FocusRequester() } + val name = current.grade.name + var default by remember(current) { + mutableStateOf(maxOf(current.options.indexOfFirst { it.id.value == current.default?.id?.value }, 0)) + } + var options by remember(current) { mutableStateOf(current.options) } + var added by remember(current) { mutableStateOf(listOf()) } + var adding by remember(current) { mutableStateOf("") } + + Surface(Modifier.fillMaxSize()) { + Box(Modifier.fillMaxSize().padding(10.dp)) { + Column(Modifier.align(Alignment.Center)) { + OutlinedTextField(name, {}, Modifier.fillMaxWidth(), label = { Text("Grading system name") }, singleLine = true, enabled = false) + Text("Grade options:", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 10.dp)) + LazyColumn(Modifier.weight(1f)) { + itemsIndexed(options) { idx, it -> + Row(Modifier.fillMaxWidth().padding(5.dp)) { + Column(Modifier.weight(1f).align(Alignment.CenterVertically)) { + Text(it.option) + if(idx == default) Text("(default option)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) + } + IconButton({ default = idx }, Modifier.align(Alignment.CenterVertically)) { + if(default == idx) Icon(CheckboxChecked, "Default option") + else Icon(CheckboxUnchecked, "Set as default") + } + } + } + + itemsIndexed(added) { idx, it -> + Row(Modifier.fillMaxWidth().padding(5.dp)) { + Column(Modifier.weight(1f).align(Alignment.CenterVertically)) { + Text(it) + if(idx + options.size == default) Text("(default option)", Modifier.padding(start = 10.dp), fontStyle = FontStyle.Italic) + } + IconButton({ default = idx + options.size }, Modifier.align(Alignment.CenterVertically)) { + if(default == idx) Icon(CheckboxChecked, "Default option") + else Icon(CheckboxUnchecked, "Set as default") + } + } + } + + item { + Row { + OutlinedTextField(adding, { adding = it }, Modifier.weight(1f).align(Alignment.CenterVertically).padding(5.dp), label = { Text("New option") }, isError = adding in options.map { it.option } || adding in added, singleLine = true) + Button({ added = added + adding; adding = "" }, Modifier.align(Alignment.CenterVertically).padding(5.dp), enabled = adding.isNotBlank() && adding !in options.map { it.option } && adding !in added) { + Text("Add") + } + } + } + } + CancelSaveRow(true, onClose) { + onSave(current, added, default) onClose() } } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt index 19143d8..00180a6 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/GroupsView.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -48,6 +49,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.jaytux.grader.data.v2.Group import com.jaytux.grader.data.v2.Student import com.jaytux.grader.startEmail +import com.jaytux.grader.ui.EditableText import com.jaytux.grader.viewmodel.EditionVM import com.jaytux.grader.viewmodel.SnackVM import org.jetbrains.exposed.v1.jdbc.transactions.transaction @@ -63,8 +65,10 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { var swappingRole by remember { mutableStateOf(-1) } val group = remember(groups, focus) { if(focus != -1) groups[focus] else null } + val groupNames = remember(groups) { groups.map { it.group.name } } val grades by vm.groupGrades.entities val snacks = viewModel { SnackVM() } + var deleting by remember { mutableStateOf(false) } Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { ListOrEmpty(groups, { Text("No groups yet.") }) { idx, it -> @@ -80,14 +84,21 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { } else { Column(Modifier.padding(10.dp)) { - Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { - Text(group.group.name, style = MaterialTheme.typography.headlineMedium) + EditableName( + group.group.name, groupNames, + { vm.modGroup(group.group, it) }, + { deleting = true }, + style = MaterialTheme.typography.headlineMedium + ) { if (group.members.any { it.first.contact.isNotBlank() }) { - IconButton({ startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { snacks.show(it) } }) { - Icon(Mail, "Send email", Modifier.fillMaxHeight()) + IconButton(Mail, "Send email", Modifier.align(Alignment.CenterVertically)) { + startEmail(group.members.mapNotNull { it.first.contact.ifBlank { null } }) { + snacks.show(it) + } } } } + Spacer(Modifier.height(5.dp)) Row(Modifier.padding(5.dp)) { var showTargetBorder by remember { mutableStateOf(false) } @@ -224,6 +235,15 @@ fun GroupsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { swappingRole = -1 } } + + if(deleting) { + if(group == null) deleting = false + else { + ConfirmDeleteDialog("a group", { deleting = false }, { vm.rmGroup(group.group) }) { + Text(group.group.name) + } + } + } } private class DDTarget(val onStart: () -> Unit, val onEnd: () -> Unit, val validator: (Transferable) -> T?, val handle: (T) -> Unit) : DragAndDropTarget { diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Icons.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Icons.kt index 5dabcb0..14a0c22 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Icons.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Icons.kt @@ -1429,4 +1429,85 @@ val Mail: ImageVector by lazy { close() } }.build() +} + +val CheckboxUnchecked: ImageVector by lazy { + ImageVector.Builder( + name = "checkbox-unchecked", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = SolidColor(Color.Black) + ) { + moveTo(3f, 6.25f) + curveTo(3f, 4.45507f, 4.45507f, 3f, 6.25f, 3f) + horizontalLineTo(17.75f) + curveTo(19.5449f, 3f, 21f, 4.45507f, 21f, 6.25f) + verticalLineTo(17.75f) + curveTo(21f, 19.5449f, 19.5449f, 21f, 17.75f, 21f) + horizontalLineTo(6.25f) + curveTo(4.45507f, 21f, 3f, 19.5449f, 3f, 17.75f) + verticalLineTo(6.25f) + close() + moveTo(6.25f, 4.5f) + curveTo(5.2835f, 4.5f, 4.5f, 5.2835f, 4.5f, 6.25f) + verticalLineTo(17.75f) + curveTo(4.5f, 18.7165f, 5.2835f, 19.5f, 6.25f, 19.5f) + horizontalLineTo(17.75f) + curveTo(18.7165f, 19.5f, 19.5f, 18.7165f, 19.5f, 17.75f) + verticalLineTo(6.25f) + curveTo(19.5f, 5.2835f, 18.7165f, 4.5f, 17.75f, 4.5f) + horizontalLineTo(6.25f) + close() + } + }.build() +} + +val CheckboxChecked: ImageVector by lazy { + ImageVector.Builder( + name = "checkbox-checked", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = SolidColor(Color.Black) + ) { + moveTo(6.25f, 3f) + curveTo(4.45507f, 3f, 3f, 4.45507f, 3f, 6.25f) + verticalLineTo(17.75f) + curveTo(3f, 19.5449f, 4.45507f, 21f, 6.25f, 21f) + horizontalLineTo(17.75f) + curveTo(19.5449f, 21f, 21f, 19.5449f, 21f, 17.75f) + verticalLineTo(6.25f) + curveTo(21f, 4.45507f, 19.5449f, 3f, 17.75f, 3f) + horizontalLineTo(6.25f) + close() + moveTo(4.5f, 6.25f) + curveTo(4.5f, 5.2835f, 5.2835f, 4.5f, 6.25f, 4.5f) + horizontalLineTo(17.75f) + curveTo(18.7165f, 4.5f, 19.5f, 5.2835f, 19.5f, 6.25f) + verticalLineTo(17.75f) + curveTo(19.5f, 18.7165f, 18.7165f, 19.5f, 17.75f, 19.5f) + horizontalLineTo(6.25f) + curveTo(5.2835f, 19.5f, 4.5f, 18.7165f, 4.5f, 17.75f) + verticalLineTo(6.25f) + close() + moveTo(17.28f, 9.28064f) + curveTo(17.5731f, 8.98791f, 17.5734f, 8.51304f, 17.2806f, 8.21998f) + curveTo(16.9879f, 7.92691f, 16.513f, 7.92664f, 16.22f, 8.21936f) + lineTo(9.99658f, 14.4356f) + lineTo(7.78084f, 12.2197f) + curveTo(7.48795f, 11.9268f, 7.01308f, 11.9268f, 6.72018f, 12.2196f) + curveTo(6.42727f, 12.5125f, 6.42726f, 12.9874f, 6.72014f, 13.2803f) + lineTo(9.46591f, 16.0262f) + curveTo(9.75868f, 16.319f, 10.2333f, 16.3192f, 10.5263f, 16.0266f) + lineTo(17.28f, 9.28064f) + close() + } + }.build() } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt index 33287e2..cf4a405 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/StudentsView.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.Divider import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -48,6 +49,7 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { val students by vm.studentList.entities val focus by vm.focusIndex val snacks = viewModel { SnackVM() } + var deleting by remember { mutableStateOf(false) } Surface(Modifier.weight(0.25f).fillMaxHeight(), tonalElevation = 7.dp) { ListOrEmpty(students, { Text("No students yet.") }) { idx, it -> @@ -69,42 +71,25 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { Surface(Modifier.padding(10.dp).fillMaxWidth(), tonalElevation = 10.dp, shadowElevation = 2.dp, shape = MaterialTheme.shapes.medium) { Column(Modifier.padding(10.dp)) { Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { - Text(students[focus].name, style = MaterialTheme.typography.headlineSmall) - if(students[focus].contact.isNotBlank()) { - IconButton({ startEmail(listOf(students[focus].contact)) { snacks.show(it) } }) { - Icon(Mail, "Send email", Modifier.fillMaxHeight()) - } + EditableText( + students[focus].name, style = MaterialTheme.typography.headlineSmall, + canSave = { it.isNotBlank() && (it == students[focus].name || !students.any { x -> x.name == it }) } + ) { + vm.modStudent(students[focus], it, null, null) + } + Spacer(Modifier.width(10.dp)) + IconButton(Delete, "Delete student", Modifier.align(Alignment.CenterVertically)) { + deleting = true } } Row { - var editing by remember { mutableStateOf(false) } - Text("Contact: ", Modifier.align(Alignment.CenterVertically).padding(start = 15.dp)) - if(!editing) { - if (students[focus].contact.isBlank()) { - Text( - "No contact info.", - Modifier.padding(start = 5.dp), - fontStyle = FontStyle.Italic, - color = LocalTextStyle.current.color.copy(alpha = 0.5f) - ) - } - else { - Text(students[focus].contact, Modifier.padding(start = 5.dp)) - } - Spacer(Modifier.width(5.dp)) - Icon(Edit, "Edit contact info", Modifier.clickable { editing = true }) + + EditableText(students[focus].contact, Modifier.align(Alignment.CenterVertically), displayAdapt = { it.ifBlank { "No contact info." } }) { + vm.modStudent(students[focus], null, it, null) } - else { - var mod by remember(focus, students[focus].contact, students[focus].id.value) { mutableStateOf(students[focus].contact) } - OutlinedTextField(mod, { mod = it }) - Spacer(Modifier.width(5.dp)) - Icon(Check, "Confirm edit", Modifier.align(Alignment.CenterVertically).clickable { - vm.modStudent(students[focus], null, mod, null) - editing = false - }) - Spacer(Modifier.width(5.dp)) - Icon(Close, "Cancel edit", Modifier.align(Alignment.CenterVertically).clickable { editing = false }) + IconButton(Mail, "Send email") { + startEmail(listOf(students[focus].contact)) { snacks.show(it) } } } @@ -163,6 +148,9 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { } items(grades ?: listOf()) { + Column(Modifier.padding(10.dp)) { + Divider() + } Column(Modifier.padding(10.dp)) { Row { Text(it.assignment.name, Modifier.weight(0.66f)) @@ -193,6 +181,15 @@ fun StudentsView(vm: EditionVM) = Row(Modifier.fillMaxSize()) { } } } + + if(deleting) { + if(focus == -1) deleting = false + else { + ConfirmDeleteDialog("a student", { deleting = false }, { vm.rmStudent(students[focus]) }) { + Text(students[focus].name) + } + } + } } @Composable diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt deleted file mode 100644 index a2f935b..0000000 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Views.kt +++ /dev/null @@ -1,200 +0,0 @@ -package com.jaytux.grader.ui - - -//@Composable -//fun StudentView(state: StudentState, nav: Navigators) { -// val groups by state.groups.entities -// val courses by state.courseEditions.entities -// val groupGrades by state.groupGrades.entities -// val soloGrades by state.soloGrades.entities -// -// Column(Modifier.padding(10.dp)) { -// Row { -// Column(Modifier.weight(0.45f)) { -// Column(Modifier.padding(10.dp).weight(0.35f)) { -// 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) -// } -// Column(Modifier.weight(0.20f)) { -// 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.45f)) { -// Text("Groups", style = MaterialTheme.typography.headlineSmall) -// ListOrEmpty(groups, { Text("Not a member of any group") }) { _, it -> -// val (group, c) = it -// val (course, ed) = c -// Row(Modifier.clickable { nav.group(group) }) { -// 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 -// ) -// } -// -// } -// } -// } -// Column(Modifier.weight(0.55f)) { -// Text("Courses", style = MaterialTheme.typography.headlineSmall) -// LazyColumn { -// item { -// Text("As group member", fontWeight = FontWeight.Bold) -// } -// items(groupGrades) { -// groupGradeWidget(it) -// } -// -// item { -// Text("Solo assignments", fontWeight = FontWeight.Bold) -// } -// items(soloGrades) { -// soloGradeWidget(it) -// } -// } -// } -// } -// } -//} -// -//@Composable -//fun groupGradeWidget(gg: StudentState.LocalGroupGrade) { -// val (group, assignment, gGrade, iGrade) = gg -// var expanded by remember { mutableStateOf(false) } -// Row(Modifier.padding(5.dp)) { -// Spacer(Modifier.width(10.dp)) -// Surface( -// Modifier.clickable { expanded = !expanded }.fillMaxWidth(), -// tonalElevation = 5.dp, -// shape = MaterialTheme.shapes.medium -// ) { -// Column(Modifier.padding(5.dp)) { -// Text("${assignment.maxN(25)} (${iGrade ?: gGrade ?: "no grade yet"})") -// -// if (expanded) { -// Row { -// Spacer(Modifier.width(10.dp)) -// Column { -// ItalicAndNormal("Assignment: ", assignment) -// ItalicAndNormal("Group name: ", group) -// ItalicAndNormal("Group grade: ", gGrade ?: "no grade yet") -// ItalicAndNormal("Individual grade: ", iGrade ?: "no individual grade") -// } -// } -// } -// } -// } -// } -//} -// -//@Composable -//fun soloGradeWidget(sg: StudentState.LocalSoloGrade) { -// val (assignment, grade) = sg -// var expanded by remember { mutableStateOf(false) } -// Row(Modifier.padding(5.dp)) { -// Spacer(Modifier.width(10.dp)) -// Surface( -// Modifier.clickable { expanded = !expanded }.fillMaxWidth(), -// tonalElevation = 5.dp, -// shape = MaterialTheme.shapes.medium -// ) { -// Column(Modifier.padding(5.dp)) { -// Text("${assignment.maxN(25)} (${grade ?: "no grade yet"})") -// -// if (expanded) { -// Row { -// Spacer(Modifier.width(10.dp)) -// Column { -// ItalicAndNormal("Assignment: ", assignment) -// ItalicAndNormal("Individual grade: ", grade ?: "no grade yet") -// } -// } -// } -// } -// } -// } -//} -// -//@Composable -//fun GroupView(state: GroupState, nav: Navigators) { -// val members by state.members.entities -// val available by state.availableStudents.entities -// val allRoles by state.roles.entities -// -// var pickRole: Pair Unit>? by remember { mutableStateOf(null) } -// -// Column(Modifier.padding(10.dp)) { -// 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(Modifier.clickable { nav.student(student) }) { -// 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).clickable { nav.student(it) }) { -// 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, 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() -// } -// } -// } -// } -//} \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt index bf03a52..9bda8c1 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/ui/Widgets.kt @@ -7,6 +7,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.ProvideTextStyle import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -16,12 +19,15 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.isUnspecified import androidx.compose.ui.window.* import com.jaytux.grader.maxN import com.jaytux.grader.viewmodel.Grade @@ -274,4 +280,66 @@ fun GradePicker(grade: Grade, modifier: Modifier = Modifier, key: Any = Unit, on } } } +} + +@Composable +fun EditableText( + text: String, modifier: Modifier = Modifier, key: Any = Unit, style: TextStyle = LocalTextStyle.current, + label: (@Composable () -> Unit)? = null, displayAdapt: ((String) -> String)? = null, + canSave: (String) -> Boolean = { true }, + onUpdate: (String) -> Unit +) { + var editing by remember(text, key) { mutableStateOf(false) } + + if(editing) { + var current by remember(text, key) { mutableStateOf(text) } + val enableSave = canSave(current) + + Row(modifier) { + OutlinedTextField(current, { current = it }, Modifier.align(Alignment.CenterVertically), label = label, textStyle = style, isError = !enableSave) + Spacer(Modifier.width(5.dp)) + IconButton(Check, "Confirm edit", Modifier.align(Alignment.CenterVertically), enableSave, iconHeight = style.fontSize.toDp()) { + onUpdate(current) + editing = false + } + Spacer(Modifier.width(5.dp)) + IconButton(Close, "Cancel edit", Modifier.align(Alignment.CenterVertically), iconHeight = style.fontSize.toDp()) { + editing = false + } + } + } + else { + Row(modifier) { + Text(displayAdapt?.let { it(text) } ?: text, Modifier.align(Alignment.CenterVertically), style = style) + Spacer(Modifier.width(5.dp)) + IconButton(Edit, "Edit", iconHeight = style.fontSize.toDp()) { + editing = true + } + } + } +} + +@Composable +fun IconButton(icon: ImageVector, contentDescription: String? = null, modifier: Modifier = Modifier, enabled: Boolean = true, iconHeight: Dp = Dp.Unspecified, onClick: () -> Unit) = + IconButton(onClick, modifier, enabled) { + if(iconHeight.isUnspecified) Icon(icon, contentDescription, modifier = Modifier.height(LocalTextStyle.current.fontSize.toDp())) + else Icon(icon, contentDescription, modifier = Modifier.height(iconHeight)) + } + +@Composable +fun EditableName( + name: String, taken: List, onUpdate: (String) -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, displayAdapt: ((String) -> String)? = null, + addBeforeDelete: (@Composable RowScope.() -> Unit)? = null +) = Row(modifier) { + ProvideTextStyle(style) { + EditableText( + name, style = style, canSave = { it.isNotBlank() && (it == name || it !in taken) }, onUpdate = onUpdate, + displayAdapt = displayAdapt + ) + addBeforeDelete?.invoke(this@Row) + IconButton(Delete, "Delete", Modifier.align(Alignment.CenterVertically)) { + onDelete() + } + } } \ No newline at end of file diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/EditionVM.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/EditionVM.kt index bb6ec0b..054e3ac 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/EditionVM.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/EditionVM.kt @@ -57,7 +57,7 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() { val categoricGrades = RawDbState { CategoricGrade.all().map { - UiGradeType.Categoric(it.options.toList(), it) + UiGradeType.Categoric(it.options.toList(), it.default, it) } } @@ -66,24 +66,35 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() { } val studentGrades = RawDbFocusableState { st: Student -> + println("Loading grade summary for student ${st.name}") val groupIds = st.groups.map { it.group.id }.toSet() edition.assignments.map { asg -> val (grade, memberOf, override) = when(asg.type) { AssignmentType.GROUP -> { - val asGroup = asg.globalCriterion.feedbacks.find { it.asGroupFeedback?.id in groupIds } - val solo = asg.globalCriterion.feedbacks.find { it.forStudentsOverrideIfGroup.any { over -> over.student == st } } - val gr = (solo ?: asGroup)?.let { Grade.fromAssignment(asg.globalCriterion, it) } - gr to asGroup?.asGroupFeedback app (solo != null) + val (asGroup, raw) = asg.globalCriterion.feedbacks.find { it.asGroupFeedback?.id in groupIds }?.let { Grade.fromAssignment(asg.globalCriterion, it) to it } ?: (null to null) + val solo = run findSolo@{ + for(groupLevel in asg.globalCriterion.feedbacks) { + for(override in groupLevel.forStudentsOverrideIfGroup) { + if(override.student.id == st.id) return@findSolo Grade.fromAssignment(asg.globalCriterion, override.feedback) + } + } + null + } + val gr = (solo ?: asGroup)//?.let { Grade.fromAssignment(asg.globalCriterion, it) } + println(" -> For group assignment ${asg.name}: $gr (solo override: $solo, group feedback: $asGroup)") + gr to raw?.asGroupFeedback app (solo != null) } AssignmentType.SOLO -> { val eval = asg.globalCriterion.feedbacks.find { it.asSoloFeedback == st } ?.let { Grade.fromAssignment(asg.globalCriterion, it) } + println(" -> For solo assignment ${asg.name}: $eval") eval to null app false } AssignmentType.PEER_EVALUATION -> { val eval = asg.globalCriterion.feedbacks.find { it.asPeerEvaluationFeedback?.id == st.id } ?.let { Grade.fromAssignment(asg.globalCriterion, it) } + println(" -> For peer evaluation assignment ${asg.name}: $eval") eval to null app false } } @@ -129,6 +140,43 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() { val selectedTab = _selectedTab.immutable() val focusIndex = _focusIndex.immutable() + init { + transaction { + var count0 = 0 + StudentOverrideFeedback.all().forEach { + val group = it.group.name + val student = it.student.name + val assignment = it.feedback.criterion + val assName = assignment.assignment.name + val ogGrade = Grade.fromAssignment(assignment, it.overrides) + val updGrade = Grade.fromAssignment(assignment, it.feedback) + println("OVERRIDE: '$student' in '$group' for '$assName' ('${assignment.name}'): $ogGrade -> $updGrade") + count0++ + } + println(" --> Direct lookup: $count0 overrides") + + var count1 = 0 + GroupAssignment.all().forEach { asg -> + val assignment = asg.base + val baseCrit = assignment.globalCriterion + val overrides = baseCrit.feedbacks.flatMap { it.forStudentsOverrideIfGroup } + if(overrides.isNotEmpty()) { + println("Assignment '${assignment.name}' has ${overrides.size} overrides:") + overrides.forEach { + val group = it.group.name + val student = it.student.name + val assName = assignment.name + val ogGrade = Grade.fromAssignment(baseCrit, it.overrides) + val updGrade = Grade.fromAssignment(baseCrit, it.feedback) + println(" - OVERRIDE: '$student' in '$group' for '$assName': $ogGrade -> $updGrade") + count1++ + } + } + } + println(" --> GroupAssignment lookup: $count1 overrides") + } + } + fun switchTo(tab: Tab) { _selectedTab.value = tab _focusIndex.value = -1 @@ -397,20 +445,38 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() { assignmentList.refresh() } - fun mkScale(name: String, options: List) { + fun mkScale(name: String, options: List, default: Int) { transaction { val grade = CategoricGrade.new { this.name = name } options.forEachIndexed { idx, opt -> - CategoricGradeOption.new { + val x = CategoricGradeOption.new { this.grade = grade this.option = opt this.index = idx } + if(idx == default) grade.default = x } } categoricGrades.refresh() } + fun modScale(grade: CategoricGrade, add: List, default: Int) { + transaction { + val currMax = grade.options.maxOfOrNull { it.index } ?: 0 + add.forEachIndexed { idx, opt -> + CategoricGradeOption.new { + this.grade = grade + this.option = opt + this.index = idx + currMax + } + } + + val default = grade.options.first { it.index == default } + grade.default = default + } + categoricGrades.refresh() + } + fun mkNumericScale(name: String, max: Double) { transaction { NumericGrade.new { @@ -441,8 +507,14 @@ class EditionVM(val edition: Edition, val course: Course) : ViewModel() { fun rmAssignment(assignment: BaseAssignment) { transaction { - assignment.delete() + assignment.criteria.forEach { + it.feedbacks.forEach { f -> + f.delete() + } + it.delete() + } (assignment.asPeerEvaluation ?: assignment.asGroupAssignment ?: assignment.asSoloAssignment)?.delete() + assignment.delete() } unfocus() assignmentList.refresh() diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/Grade.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/Grade.kt index 7b64535..62f9e31 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/Grade.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/Grade.kt @@ -39,10 +39,15 @@ sealed class Grade { companion object { context(trns: Transaction) fun fromAssignment(asg: Criterion, fdb: BaseFeedback): Grade = when(asg.gradeType) { - GradeType.CATEGORIC -> - Categoric(fdb.gradeCategoric!!, asg.categoricGrade!!.options.toList(), asg.categoricGrade!!) + GradeType.CATEGORIC -> { + val option = fdb.gradeCategoric ?: asg.categoricGrade!!.default + Categoric(option, asg.categoricGrade!!.options.toList(), asg.categoricGrade!!) + } - GradeType.NUMERIC -> Numeric(fdb.gradeNumeric!!, asg.numericGrade!!) + GradeType.NUMERIC -> { + val grade = fdb.gradeNumeric ?: -1.0 + Numeric(grade, asg.numericGrade!!) + } GradeType.PERCENTAGE -> Percentage(fdb.gradeNumeric!!) GradeType.NONE -> FreeText(fdb.gradeFreeText!!) } diff --git a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/UiGradeType.kt b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/UiGradeType.kt index c0b6d0d..cd55955 100644 --- a/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/UiGradeType.kt +++ b/composeApp/src/desktopMain/kotlin/com/jaytux/grader/viewmodel/UiGradeType.kt @@ -16,12 +16,12 @@ sealed class UiGradeType { object FreeText : UiGradeType() object Percentage : UiGradeType() data class Numeric(val grade: NumericGrade) : UiGradeType() - data class Categoric(val options: List, val grade: CategoricGrade) : UiGradeType() + data class Categoric(val options: List, val default: CategoricGradeOption?, val grade: CategoricGrade) : UiGradeType() companion object { context(trns: Transaction) fun from(type: GradeType, categoric: CategoricGrade?, numeric: NumericGrade?) = when(type) { - GradeType.CATEGORIC -> Categoric(categoric!!.options.toList(), categoric) + GradeType.CATEGORIC -> Categoric(categoric!!.options.toList(), categoric.default, categoric) GradeType.NUMERIC -> Numeric(numeric!!) GradeType.PERCENTAGE -> Percentage GradeType.NONE -> FreeText