fix(home): resolve OutcomeDay import for outcome dialogs
This commit is contained in:
parent
b84377a8fa
commit
391ea68793
|
|
@ -32,9 +32,12 @@ 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.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.TrendingUp
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.LocalFireDepartment
|
import androidx.compose.material.icons.filled.LocalFireDepartment
|
||||||
|
import app.closer.domain.model.OutcomeDay
|
||||||
|
import app.closer.ui.components.OutcomeCheckInDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
|
@ -43,7 +46,9 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
@ -97,6 +102,60 @@ fun HomeScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showBaselineDialog by remember { mutableStateOf(false) }
|
||||||
|
var showFollowUpDialog by remember { mutableStateOf(false) }
|
||||||
|
var pendingFollowUpDay by remember { mutableStateOf<OutcomeDay?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(state.showOutcomeBaselineDialog) {
|
||||||
|
showBaselineDialog = state.showOutcomeBaselineDialog
|
||||||
|
}
|
||||||
|
LaunchedEffect(state.showOutcomeFollowUpDialog, state.outcomeFollowUpDay) {
|
||||||
|
showFollowUpDialog = state.showOutcomeFollowUpDialog
|
||||||
|
pendingFollowUpDay = state.outcomeFollowUpDay
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBaselineDialog) {
|
||||||
|
OutcomeCheckInDialog(
|
||||||
|
title = "Quick check-in",
|
||||||
|
subtitle = "Before you start, how are you feeling about your relationship right now?",
|
||||||
|
onDismiss = {
|
||||||
|
viewModel.markBaselineOutcomeShown()
|
||||||
|
showBaselineDialog = false
|
||||||
|
},
|
||||||
|
onSubmit = { scores ->
|
||||||
|
viewModel.submitOutcome(OutcomeDay.BASELINE.key, scores)
|
||||||
|
showBaselineDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFollowUpDay?.let { day ->
|
||||||
|
if (showFollowUpDialog) {
|
||||||
|
OutcomeCheckInDialog(
|
||||||
|
title = "${day.label} check-in",
|
||||||
|
subtitle = "How are you feeling now compared to when you started?",
|
||||||
|
onDismiss = {
|
||||||
|
viewModel.markFollowUpOutcomeShown(day.key)
|
||||||
|
showFollowUpDialog = false
|
||||||
|
pendingFollowUpDay = null
|
||||||
|
},
|
||||||
|
onSubmit = { scores ->
|
||||||
|
viewModel.submitOutcome(day.key, scores)
|
||||||
|
showFollowUpDialog = false
|
||||||
|
pendingFollowUpDay = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.outcomeSubmitSuccess, state.outcomeError) {
|
||||||
|
state.outcomeError?.let { snackbarHostState.showSnackbar(it); viewModel.consumeOutcomeError() }
|
||||||
|
if (state.outcomeSubmitSuccess) {
|
||||||
|
snackbarHostState.showSnackbar("Check-in saved")
|
||||||
|
viewModel.consumeOutcomeSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HomeContent(
|
HomeContent(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ import app.closer.domain.repository.LocalAnswerRepository
|
||||||
import app.closer.domain.repository.QuestionRepository
|
import app.closer.domain.repository.QuestionRepository
|
||||||
import app.closer.domain.repository.QuestionSessionRepository
|
import app.closer.domain.repository.QuestionSessionRepository
|
||||||
import app.closer.domain.repository.UserRepository
|
import app.closer.domain.repository.UserRepository
|
||||||
|
import app.closer.domain.model.Outcome
|
||||||
|
import app.closer.domain.model.OutcomeDay
|
||||||
|
import app.closer.domain.model.OutcomeScores
|
||||||
|
import app.closer.domain.repository.OutcomeRepository
|
||||||
|
import app.closer.domain.repository.SettingsRepository
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import com.google.firebase.functions.FirebaseFunctions
|
import com.google.firebase.functions.FirebaseFunctions
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -27,6 +32,7 @@ import app.closer.ui.home.HomePriorityEngine.Input as PriorityInput
|
||||||
import app.closer.ui.home.HomePriorityEngine.Priority
|
import app.closer.ui.home.HomePriorityEngine.Priority
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
|
@ -36,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
data class HomeCategorySummary(
|
data class HomeCategorySummary(
|
||||||
val category: QuestionCategory,
|
val category: QuestionCategory,
|
||||||
|
|
@ -126,7 +133,13 @@ data class HomeUiState(
|
||||||
val hasUpcomingDatePlan: Boolean = false,
|
val hasUpcomingDatePlan: Boolean = false,
|
||||||
val hasUnlockedCapsule: Boolean = false,
|
val hasUnlockedCapsule: Boolean = false,
|
||||||
val weeklyRecapReady: Boolean = false,
|
val weeklyRecapReady: Boolean = false,
|
||||||
val reminderSentEvent: Boolean = false
|
val reminderSentEvent: Boolean = false,
|
||||||
|
// Outcome check-in state
|
||||||
|
val outcomeSubmitSuccess: Boolean = false,
|
||||||
|
val outcomeError: String? = null,
|
||||||
|
val showOutcomeBaselineDialog: Boolean = false,
|
||||||
|
val showOutcomeFollowUpDialog: Boolean = false,
|
||||||
|
val outcomeFollowUpDay: OutcomeDay? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -143,7 +156,9 @@ class HomeViewModel @Inject constructor(
|
||||||
private val challengeDataSource: FirestoreChallengeDataSource,
|
private val challengeDataSource: FirestoreChallengeDataSource,
|
||||||
private val capsuleDataSource: FirestoreCapsuleDataSource,
|
private val capsuleDataSource: FirestoreCapsuleDataSource,
|
||||||
private val datePlanRepository: DatePlanRepository,
|
private val datePlanRepository: DatePlanRepository,
|
||||||
private val sealedRevealManager: SealedRevealManager
|
private val sealedRevealManager: SealedRevealManager,
|
||||||
|
private val outcomeRepository: OutcomeRepository,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(HomeUiState())
|
private val _uiState = MutableStateFlow(HomeUiState())
|
||||||
|
|
@ -196,6 +211,17 @@ class HomeViewModel @Inject constructor(
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outcome check-in due-state calculation
|
||||||
|
val outcomeBaselineShownAt = settingsRepository.settings.first().outcomeBaselineShownAt
|
||||||
|
val outcomes = couple?.let {
|
||||||
|
runCatching { outcomeRepository.getOutcomes(it.id) }
|
||||||
|
.onFailure { Log.w(TAG, "Could not load outcomes", it) }
|
||||||
|
.getOrDefault(emptyList())
|
||||||
|
} ?: emptyList()
|
||||||
|
val baselineRecorded = outcomes.any { it.dayKey == OutcomeDay.BASELINE.key }
|
||||||
|
val showBaselineDialog = couple != null && !baselineRecorded && outcomeBaselineShownAt == 0L
|
||||||
|
val followUpDay = couple?.let { dueFollowUpDay(it.createdAt, outcomes) }
|
||||||
|
|
||||||
// Retention signal fetches — run in parallel, failures silently default to false.
|
// Retention signal fetches — run in parallel, failures silently default to false.
|
||||||
var hasWaitingGame = false
|
var hasWaitingGame = false
|
||||||
var hasActiveChallenge = false
|
var hasActiveChallenge = false
|
||||||
|
|
@ -253,7 +279,10 @@ class HomeViewModel @Inject constructor(
|
||||||
hasWaitingGame = hasWaitingGame,
|
hasWaitingGame = hasWaitingGame,
|
||||||
hasActiveChallenge = hasActiveChallenge,
|
hasActiveChallenge = hasActiveChallenge,
|
||||||
hasUpcomingDatePlan = hasUpcomingDatePlan,
|
hasUpcomingDatePlan = hasUpcomingDatePlan,
|
||||||
hasUnlockedCapsule = hasUnlockedCapsule
|
hasUnlockedCapsule = hasUnlockedCapsule,
|
||||||
|
showOutcomeBaselineDialog = showBaselineDialog,
|
||||||
|
showOutcomeFollowUpDialog = followUpDay != null,
|
||||||
|
outcomeFollowUpDay = followUpDay
|
||||||
).refreshDailyQuestionState().withHomeActions()
|
).refreshDailyQuestionState().withHomeActions()
|
||||||
}
|
}
|
||||||
observePartnerAnswer(couple?.id, couple?.userIds, dailyQuestion?.id)
|
observePartnerAnswer(couple?.id, couple?.userIds, dailyQuestion?.id)
|
||||||
|
|
@ -311,6 +340,63 @@ class HomeViewModel @Inject constructor(
|
||||||
loadHome()
|
loadHome()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dueFollowUpDay(createdAt: Long, outcomes: List<Outcome>): OutcomeDay? {
|
||||||
|
val paired = java.time.Instant.ofEpochMilli(createdAt).atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
|
val today = LocalDate.now()
|
||||||
|
val ageDays = java.time.temporal.ChronoUnit.DAYS.between(paired, today)
|
||||||
|
val due = listOf(
|
||||||
|
OutcomeDay.DAY_30 to 30,
|
||||||
|
OutcomeDay.DAY_60 to 60,
|
||||||
|
OutcomeDay.DAY_90 to 90
|
||||||
|
).firstOrNull { (_, days) -> ageDays >= days } ?: return null
|
||||||
|
return if (outcomes.any { it.dayKey == due.first.key }) null else due.first
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitOutcome(dayKey: String, scores: OutcomeScores) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(outcomeSubmitSuccess = false, outcomeError = null) }
|
||||||
|
try {
|
||||||
|
val uid = authRepository.currentUserId
|
||||||
|
?: throw IllegalStateException("Not signed in.")
|
||||||
|
val couple = coupleRepository.getCoupleForUser(uid)
|
||||||
|
?: throw IllegalStateException("Not paired.")
|
||||||
|
outcomeRepository.submitOutcome(couple.id, dayKey, scores).getOrThrow()
|
||||||
|
settingsRepository.setOutcomeLastPromptedDay(dayKey)
|
||||||
|
if (dayKey == OutcomeDay.BASELINE.key) {
|
||||||
|
settingsRepository.markOutcomeBaselineShown()
|
||||||
|
}
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
outcomeSubmitSuccess = true,
|
||||||
|
showOutcomeBaselineDialog = false,
|
||||||
|
showOutcomeFollowUpDialog = false,
|
||||||
|
outcomeFollowUpDay = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadHome()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.update { it.copy(outcomeError = e.message ?: "Couldn’t save check-in.") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markBaselineOutcomeShown() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.markOutcomeBaselineShown()
|
||||||
|
_uiState.update { it.copy(showOutcomeBaselineDialog = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markFollowUpOutcomeShown(dayKey: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
settingsRepository.setOutcomeLastPromptedDay(dayKey)
|
||||||
|
_uiState.update { it.copy(showOutcomeFollowUpDialog = false, outcomeFollowUpDay = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeOutcomeSuccess() = _uiState.update { it.copy(outcomeSubmitSuccess = false) }
|
||||||
|
fun consumeOutcomeError() = _uiState.update { it.copy(outcomeError = null) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a gentle reminder to the partner that the daily question is waiting.
|
* Sends a gentle reminder to the partner that the daily question is waiting.
|
||||||
* Notification wiring is intentionally a no-op until Batch 6.
|
* Notification wiring is intentionally a no-op until Batch 6.
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
|
||||||
|
import androidx.compose.material.icons.filled.TrendingUp
|
||||||
import androidx.compose.material.icons.filled.Done
|
import androidx.compose.material.icons.filled.Done
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||||
import androidx.compose.material.icons.filled.TrendingUp
|
|
||||||
import androidx.compose.material.icons.filled.Lock
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material.icons.filled.Notifications
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue