fix: add answer options to wheel session screen so partners can answer questions
This commit is contained in:
parent
7fb913db99
commit
c6df885e1e
|
|
@ -2,25 +2,39 @@ package app.closer.ui.wheel
|
||||||
|
|
||||||
import app.closer.ui.theme.closerCardColor
|
import app.closer.ui.theme.closerCardColor
|
||||||
import app.closer.ui.theme.closerBackgroundBrush
|
import app.closer.ui.theme.closerBackgroundBrush
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.RadioButtonDefaults
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
|
@ -39,7 +53,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import app.closer.domain.model.ChoiceAnswerConfigImpl
|
||||||
import app.closer.domain.model.Question
|
import app.closer.domain.model.Question
|
||||||
|
import app.closer.domain.model.ScaleAnswerConfigImpl
|
||||||
|
import app.closer.domain.model.ThisOrThatAnswerConfigImpl
|
||||||
|
import app.closer.domain.model.WrittenAnswerConfigImpl
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WheelSessionScreen(
|
fun WheelSessionScreen(
|
||||||
|
|
@ -58,6 +76,9 @@ fun WheelSessionScreen(
|
||||||
|
|
||||||
WheelSessionContent(
|
WheelSessionContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
onSelectOption = viewModel::selectOption,
|
||||||
|
onScaleChanged = viewModel::onScaleChanged,
|
||||||
|
onWrittenTextChanged = viewModel::onWrittenTextChanged,
|
||||||
onNext = viewModel::next,
|
onNext = viewModel::next,
|
||||||
onSkip = viewModel::skip,
|
onSkip = viewModel::skip,
|
||||||
onEnd = viewModel::endEarly
|
onEnd = viewModel::endEarly
|
||||||
|
|
@ -67,6 +88,9 @@ fun WheelSessionScreen(
|
||||||
@Composable
|
@Composable
|
||||||
private fun WheelSessionContent(
|
private fun WheelSessionContent(
|
||||||
state: WheelSessionUiState,
|
state: WheelSessionUiState,
|
||||||
|
onSelectOption: (String) -> Unit,
|
||||||
|
onScaleChanged: (Int) -> Unit,
|
||||||
|
onWrittenTextChanged: (String) -> Unit,
|
||||||
onNext: () -> Unit,
|
onNext: () -> Unit,
|
||||||
onSkip: () -> Unit,
|
onSkip: () -> Unit,
|
||||||
onEnd: () -> Unit
|
onEnd: () -> Unit
|
||||||
|
|
@ -74,9 +98,7 @@ private fun WheelSessionContent(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
.background(closerBackgroundBrush())
|
||||||
closerBackgroundBrush()
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -84,7 +106,7 @@ private fun WheelSessionContent(
|
||||||
.safeDrawingPadding()
|
.safeDrawingPadding()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.padding(horizontal = 24.dp, vertical = 24.dp),
|
.padding(horizontal = 24.dp, vertical = 24.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
EmptySessionCard()
|
EmptySessionCard()
|
||||||
|
|
@ -95,6 +117,7 @@ private fun WheelSessionContent(
|
||||||
val current = state.currentIndex
|
val current = state.currentIndex
|
||||||
val progress = if (total > 0) (current.toFloat() / total) else 0f
|
val progress = if (total > 0) (current.toFloat() / total) else 0f
|
||||||
|
|
||||||
|
// Header row
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
|
@ -125,6 +148,7 @@ private fun WheelSessionContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { progress },
|
progress = { progress },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -132,6 +156,7 @@ private fun WheelSessionContent(
|
||||||
trackColor = Color(0xFFF4E8FF)
|
trackColor = Color(0xFFF4E8FF)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Question card
|
||||||
val question = state.questions.getOrNull(current)
|
val question = state.questions.getOrNull(current)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
|
|
@ -142,24 +167,40 @@ private fun WheelSessionContent(
|
||||||
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
|
colors = CardDefaults.cardColors(containerColor = closerCardColor(alpha = 0.88f)),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(28.dp),
|
.verticalScroll(rememberScrollState())
|
||||||
contentAlignment = Alignment.Center
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||||
) {
|
) {
|
||||||
|
// Question text
|
||||||
Text(
|
Text(
|
||||||
text = question?.text ?: "",
|
text = question?.text ?: "",
|
||||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
lineHeight = MaterialTheme.typography.titleLarge.lineHeight,
|
|
||||||
maxLines = 8,
|
maxLines = 8,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Answer options
|
||||||
|
if (question != null) {
|
||||||
|
AnswerOptions(
|
||||||
|
question = question,
|
||||||
|
selectedOptionIds = state.selectedOptionIds,
|
||||||
|
selectedScaleValue = state.selectedScaleValue,
|
||||||
|
writtenText = state.writtenText,
|
||||||
|
onSelectOption = onSelectOption,
|
||||||
|
onScaleChanged = onScaleChanged,
|
||||||
|
onWrittenTextChanged = onWrittenTextChanged
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
|
@ -204,6 +245,216 @@ private fun WheelSessionContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AnswerOptions(
|
||||||
|
question: Question,
|
||||||
|
selectedOptionIds: List<String>,
|
||||||
|
selectedScaleValue: Int,
|
||||||
|
writtenText: String,
|
||||||
|
onSelectOption: (String) -> Unit,
|
||||||
|
onScaleChanged: (Int) -> Unit,
|
||||||
|
onWrittenTextChanged: (String) -> Unit
|
||||||
|
) {
|
||||||
|
when (question.type) {
|
||||||
|
"written" -> {
|
||||||
|
val config = question.answerConfig as? WrittenAnswerConfigImpl
|
||||||
|
OutlinedTextField(
|
||||||
|
value = writtenText,
|
||||||
|
onValueChange = onWrittenTextChanged,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 120.dp),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = config?.config?.placeholder ?: "Write your answer…",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = false,
|
||||||
|
minLines = 3,
|
||||||
|
maxLines = 6,
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = Color(0xFF56306F),
|
||||||
|
unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface
|
||||||
|
),
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"single_choice" -> {
|
||||||
|
val config = question.answerConfig as? ChoiceAnswerConfigImpl
|
||||||
|
val options = config?.config?.options ?: return
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
options.forEach { option ->
|
||||||
|
val isSelected = option.id in selectedOptionIds
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onSelectOption(option.id) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
border = BorderStroke(
|
||||||
|
width = if (isSelected) 2.dp else 1.dp,
|
||||||
|
color = if (isSelected) Color(0xFF56306F)
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
|
||||||
|
),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = if (isSelected) Color(0xFFF0EDF9)
|
||||||
|
else MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = null,
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = Color(0xFF56306F)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = option.text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"multi_choice" -> {
|
||||||
|
val config = question.answerConfig as? ChoiceAnswerConfigImpl
|
||||||
|
val options = config?.config?.options ?: return
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
options.forEach { option ->
|
||||||
|
val isChecked = option.id in selectedOptionIds
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { onSelectOption(option.id) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
border = BorderStroke(
|
||||||
|
width = if (isChecked) 2.dp else 1.dp,
|
||||||
|
color = if (isChecked) Color(0xFF56306F)
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f)
|
||||||
|
),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = if (isChecked) Color(0xFFF0EDF9)
|
||||||
|
else MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isChecked,
|
||||||
|
onCheckedChange = null,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = Color(0xFF56306F)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = option.text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"scale" -> {
|
||||||
|
val config = question.answerConfig as? ScaleAnswerConfigImpl
|
||||||
|
val cfg = config?.config ?: return
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$selectedScaleValue",
|
||||||
|
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
color = Color(0xFF56306F)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Slider(
|
||||||
|
value = selectedScaleValue.toFloat(),
|
||||||
|
onValueChange = { onScaleChanged(it.toInt()) },
|
||||||
|
valueRange = cfg.minScale.toFloat()..cfg.maxScale.toFloat(),
|
||||||
|
steps = (cfg.maxScale - cfg.minScale - 1).coerceAtLeast(0),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = Color(0xFF56306F),
|
||||||
|
activeTrackColor = Color(0xFF56306F),
|
||||||
|
inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = cfg.minLabel.ifBlank { "${cfg.minScale}" },
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = cfg.maxLabel.ifBlank { "${cfg.maxScale}" },
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"this_or_that" -> {
|
||||||
|
val config = question.answerConfig as? ThisOrThatAnswerConfigImpl
|
||||||
|
val cfg = config?.config ?: return
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
listOf(cfg.optionA, cfg.optionB).forEach { option ->
|
||||||
|
val isSelected = option.id in selectedOptionIds
|
||||||
|
Button(
|
||||||
|
onClick = { onSelectOption(option.id) },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(96.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (isSelected) Color(0xFF56306F)
|
||||||
|
else MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
contentColor = if (isSelected) Color.White
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
),
|
||||||
|
elevation = ButtonDefaults.buttonElevation(
|
||||||
|
defaultElevation = if (isSelected) 4.dp else 0.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = option.text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptySessionCard() {
|
private fun EmptySessionCard() {
|
||||||
Card(
|
Card(
|
||||||
|
|
@ -246,6 +497,9 @@ fun WheelSessionScreenPreview() {
|
||||||
currentIndex = 0,
|
currentIndex = 0,
|
||||||
categoryName = "Trust"
|
categoryName = "Trust"
|
||||||
),
|
),
|
||||||
|
onSelectOption = {},
|
||||||
|
onScaleChanged = {},
|
||||||
|
onWrittenTextChanged = {},
|
||||||
onNext = {},
|
onNext = {},
|
||||||
onSkip = {},
|
onSkip = {},
|
||||||
onEnd = {}
|
onEnd = {}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,13 @@ data class WheelSessionUiState(
|
||||||
val questions: List<Question> = emptyList(),
|
val questions: List<Question> = emptyList(),
|
||||||
val currentIndex: Int = 0,
|
val currentIndex: Int = 0,
|
||||||
val skippedCount: Int = 0,
|
val skippedCount: Int = 0,
|
||||||
|
val answeredCount: Int = 0,
|
||||||
val categoryName: String = "",
|
val categoryName: String = "",
|
||||||
val navigateTo: String? = null,
|
val navigateTo: String? = null,
|
||||||
val isEmpty: Boolean = false
|
val isEmpty: Boolean = false,
|
||||||
|
val selectedOptionIds: List<String> = emptyList(),
|
||||||
|
val selectedScaleValue: Int = 3,
|
||||||
|
val writtenText: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -32,22 +36,67 @@ class WheelSessionViewModel @Inject constructor(
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
_uiState.update { it.copy(isEmpty = true) }
|
_uiState.update { it.copy(isEmpty = true) }
|
||||||
} else {
|
} else {
|
||||||
|
val firstQuestion = session.questions.firstOrNull()
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
questions = session.questions,
|
questions = session.questions,
|
||||||
categoryName = session.categoryName
|
categoryName = session.categoryName,
|
||||||
|
selectedScaleValue = defaultScaleValue(firstQuestion)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defaultScaleValue(question: Question?): Int {
|
||||||
|
val config = question?.answerConfig
|
||||||
|
if (config is app.closer.domain.model.ScaleAnswerConfigImpl) {
|
||||||
|
return (config.config.minScale + config.config.maxScale) / 2
|
||||||
|
}
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectOption(optionId: String) {
|
||||||
|
val question = _uiState.value.questions.getOrNull(_uiState.value.currentIndex) ?: return
|
||||||
|
if (question.type == "multi_choice") {
|
||||||
|
_uiState.update {
|
||||||
|
val current = it.selectedOptionIds.toMutableList()
|
||||||
|
if (optionId in current) current.remove(optionId) else current.add(optionId)
|
||||||
|
it.copy(selectedOptionIds = current)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_uiState.update { it.copy(selectedOptionIds = listOf(optionId)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onScaleChanged(value: Int) {
|
||||||
|
_uiState.update { it.copy(selectedScaleValue = value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onWrittenTextChanged(text: String) {
|
||||||
|
_uiState.update { it.copy(writtenText = text) }
|
||||||
|
}
|
||||||
|
|
||||||
fun next() {
|
fun next() {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
|
val hasSelection = hasValidSelection(state)
|
||||||
val nextIndex = state.currentIndex + 1
|
val nextIndex = state.currentIndex + 1
|
||||||
|
|
||||||
if (nextIndex >= state.questions.size) {
|
if (nextIndex >= state.questions.size) {
|
||||||
finishSession()
|
// Last question — finish
|
||||||
|
sessionStore.lastAnswered = state.answeredCount + (if (hasSelection) 1 else 0)
|
||||||
|
sessionStore.lastTotal = state.questions.size
|
||||||
|
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("session")) }
|
||||||
} else {
|
} else {
|
||||||
_uiState.update { it.copy(currentIndex = nextIndex) }
|
val nextQuestion = state.questions.getOrNull(nextIndex)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
currentIndex = nextIndex,
|
||||||
|
answeredCount = it.answeredCount + (if (hasSelection) 1 else 0),
|
||||||
|
selectedOptionIds = emptyList(),
|
||||||
|
selectedScaleValue = defaultScaleValue(nextQuestion),
|
||||||
|
writtenText = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,23 +104,41 @@ class WheelSessionViewModel @Inject constructor(
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
val nextIndex = state.currentIndex + 1
|
val nextIndex = state.currentIndex + 1
|
||||||
if (nextIndex >= state.questions.size) {
|
if (nextIndex >= state.questions.size) {
|
||||||
finishSession()
|
sessionStore.lastAnswered = state.answeredCount
|
||||||
|
sessionStore.lastTotal = state.questions.size
|
||||||
|
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("session")) }
|
||||||
} else {
|
} else {
|
||||||
_uiState.update { it.copy(currentIndex = nextIndex, skippedCount = state.skippedCount + 1) }
|
val nextQuestion = state.questions.getOrNull(nextIndex)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
currentIndex = nextIndex,
|
||||||
|
skippedCount = state.skippedCount + 1,
|
||||||
|
selectedOptionIds = emptyList(),
|
||||||
|
selectedScaleValue = defaultScaleValue(nextQuestion),
|
||||||
|
writtenText = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endEarly() {
|
fun endEarly() {
|
||||||
finishSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun finishSession() {
|
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
sessionStore.lastAnswered = (state.currentIndex + 1).coerceAtMost(state.questions.size)
|
sessionStore.lastAnswered = state.answeredCount
|
||||||
sessionStore.lastTotal = state.questions.size
|
sessionStore.lastTotal = state.questions.size
|
||||||
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("session")) }
|
_uiState.update { it.copy(navigateTo = AppRoute.wheelComplete("session")) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasValidSelection(state: WheelSessionUiState): Boolean {
|
||||||
|
val question = state.questions.getOrNull(state.currentIndex) ?: return false
|
||||||
|
return when (question.type) {
|
||||||
|
"written" -> state.writtenText.isNotBlank()
|
||||||
|
"single_choice", "this_or_that" -> state.selectedOptionIds.isNotEmpty()
|
||||||
|
"multi_choice" -> state.selectedOptionIds.isNotEmpty()
|
||||||
|
"scale" -> true // always has a default value
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onNavigated() {
|
fun onNavigated() {
|
||||||
_uiState.update { it.copy(navigateTo = null) }
|
_uiState.update { it.copy(navigateTo = null) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue