diff --git a/app/src/main/java/io/github/sustainow/MainActivity.kt b/app/src/main/java/io/github/sustainow/MainActivity.kt index b122385..dddb8ec 100644 --- a/app/src/main/java/io/github/sustainow/MainActivity.kt +++ b/app/src/main/java/io/github/sustainow/MainActivity.kt @@ -1,10 +1,12 @@ package io.github.sustainow +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -61,13 +63,16 @@ import dagger.hilt.android.lifecycle.withCreationCallback import io.github.sustainow.domain.model.UserState import io.github.sustainow.presentation.theme.AppTheme import io.github.sustainow.presentation.ui.ConsumptionMainScreen +import io.github.sustainow.presentation.ui.ExpectedCarbonFootprintScreen import io.github.sustainow.presentation.ui.HomeScreen import io.github.sustainow.presentation.ui.LoginScreen import io.github.sustainow.presentation.ui.SignUpScreen import io.github.sustainow.presentation.ui.utils.Route +import io.github.sustainow.presentation.viewmodel.FormularyViewModel import io.github.sustainow.presentation.viewmodel.HomeViewModel import io.github.sustainow.presentation.viewmodel.LoginViewModel import io.github.sustainow.presentation.viewmodel.SignUpViewModel +import io.github.sustainow.repository.formulary.FormularyRepository import io.github.sustainow.service.auth.AuthService import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -109,6 +114,7 @@ import javax.inject.Inject class MainActivity : ComponentActivity() { @Inject lateinit var authService: AuthService + @RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -319,7 +325,19 @@ class MainActivity : ComponentActivity() { // TODO remove placeholder when creating each new screen composable { Text(text = "Consumo de energia") } composable { Text(text = "Consumo de água") } - composable { Text(text = "Pega de carbono") } + composable { + val formularyViewModel: FormularyViewModel by viewModels( + extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { factory -> + factory.create( + area = "carbon_footprint", + type = "expected" + ) + } + } + ) + ExpectedCarbonFootprintScreen(navController, formularyViewModel) + } composable { Text(text = "Consumo de energia real") } composable { Text(text = "Consumo de água real") } } diff --git a/app/src/main/java/io/github/sustainow/presentation/ui/ExpectedCarbonFootprintScreen.kt b/app/src/main/java/io/github/sustainow/presentation/ui/ExpectedCarbonFootprintScreen.kt new file mode 100644 index 0000000..eef7c74 --- /dev/null +++ b/app/src/main/java/io/github/sustainow/presentation/ui/ExpectedCarbonFootprintScreen.kt @@ -0,0 +1,309 @@ +package io.github.sustainow.presentation.ui + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import io.github.sustainow.ConsumptionMainPage +import io.github.sustainow.R +import io.github.sustainow.domain.model.Question +import io.github.sustainow.domain.model.UserState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import io.github.sustainow.presentation.viewmodel.FormularyViewModel +import io.github.sustainow.presentation.ui.components.SingleSelectQuestionCard +import io.github.sustainow.presentation.ui.components.MultiSelectQuestionCard +import io.github.sustainow.presentation.ui.components.NumericalSelectQuestionCard +import java.time.LocalDate + + +@Composable +fun LinearDeterminateIndicator() { + var currentProgress by remember { mutableStateOf(0f) } + var loading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() // Create a coroutine scope + + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Button(onClick = { + loading = true + scope.launch { + loadProgress { progress -> + currentProgress = progress + } + loading = false // Reset loading when the coroutine finishes + } + }, enabled = !loading) { + Text("Start loading") + } + + if (loading) { + LinearProgressIndicator( + progress = { currentProgress }, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +/** Iterate the progress value */ +suspend fun loadProgress(updateProgress: (Float) -> Unit) { + for (i in 1..100) { + updateProgress(i.toFloat() / 100) + delay(100) + } +} + +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun ExpectedCarbonFootprintScreen( + navController: NavController, + viewModel: FormularyViewModel, +) { + val formulary by viewModel.formulary.collectAsState() + val currentQuestion by viewModel.currentQuestion.collectAsState() + val loading by viewModel.loading.collectAsState() + val success by viewModel.success.collectAsState() + val erro by viewModel.error.collectAsState() + + if (loading) { + // Exibir indicador de carregamento enquanto os dados são carregados + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } else if (success) { + val totalValue = viewModel.calculateTotalValue() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.onTertiaryContainer + ), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.result), + style = MaterialTheme.typography.headlineMedium, // Tamanho maior para o texto + modifier = Modifier.padding(bottom = 8.dp), + color = MaterialTheme.colorScheme.inverseOnSurface + ) + Text( + text = "$totalValue kg/mês", + style = MaterialTheme.typography.displayMedium, // Destaque maior para o valor + modifier = Modifier.padding(bottom = 16.dp), + color = MaterialTheme.colorScheme.inverseOnSurface + ) + } + } + + Button( + onClick = { navController.navigate(ConsumptionMainPage) }, + modifier = Modifier.padding(top = 16.dp), // Espaçamento acima do botão + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text(stringResource(R.string.back)) + } + } + } else if (erro != null) { + if(erro!!.source === formulary) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .wrapContentHeight(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.onSurface + ), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.formulary_error), + modifier = Modifier.padding(bottom = 16.dp), + color = MaterialTheme.colorScheme.inverseOnSurface, + ) + Button( + onClick = { navController.navigate(ConsumptionMainPage) }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { + Text(stringResource(R.string.back)) + } + } + } + } else { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .wrapContentHeight(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.onSurface + ), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.answer_error), + modifier = Modifier.padding(bottom = 16.dp), + color = MaterialTheme.colorScheme.inverseOnSurface, + ) + Button( + onClick = { navController.navigate(ConsumptionMainPage) }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + ) { + Text(stringResource(R.string.back)) + } + } + } + } + } else { + val questions = formulary!!.questions + val currentIndex = questions.indexOf(currentQuestion) + val progress = if (questions.isNotEmpty()) (currentIndex + 1) / questions.size.toFloat() else 0f + + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + // Indicador de progresso linear baseado no progresso das questões + LinearProgressIndicator( + progress = { + progress // Corrigido: deve ser um Float + }, + modifier = Modifier.fillMaxWidth(), + ) + + // Exibir a questão atual + currentQuestion?.let { question -> + when (question) { + is Question.SingleSelect -> SingleSelectQuestionCard(question) { + selectedAlternative -> + if(viewModel.userStateLogged is UserState.Logged) { + viewModel.addAnswerToQuestion( + question = question, + selectedAlternative = selectedAlternative, + formId = formulary!!.id, // Certifique-se de passar os valores necessários + uid = viewModel.userStateLogged.user.uid, + groupName = "", + month = LocalDate.now().monthValue, + ) + } + } + is Question.MultiSelect -> MultiSelectQuestionCard(question) { + selectedAlternative -> + if(viewModel.userStateLogged is UserState.Logged) { + viewModel.addAnswerToQuestion( + question = question, + selectedAlternative = selectedAlternative, + formId = formulary!!.id, // Certifique-se de passar os valores necessários + uid = viewModel.userStateLogged.user.uid, + groupName = "", + month = LocalDate.now().monthValue, + ) + } + } + is Question.Numerical -> NumericalSelectQuestionCard(question) { + selectedAlternative -> + if(viewModel.userStateLogged is UserState.Logged) { + viewModel.addAnswerToQuestion( + question = question, + selectedAlternative = selectedAlternative, + formId = formulary!!.id, // Certifique-se de passar os valores necessários + uid = viewModel.userStateLogged.user.uid, + groupName = "", + month = LocalDate.now().monthValue, + ) + } + } + is Question.MultiItem -> { + Text("Question: ${question.text} (Multi Item)") + } + } + } + + // Botões de navegação + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { viewModel.goToPreviousQuestion() }, + enabled = currentIndex > 0, // Desabilitar se estiver na primeira questão + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Text(stringResource(R.string._return)) + } + + Button( + onClick = { + if (currentIndex == questions.size - 1) { + // Última questão, concluir o formulário + viewModel.sendAnswers() + } else { + // Avançar para a próxima questão + viewModel.goToNextQuestion() + } + } + ) { + Text(if (currentIndex == questions.size - 1) stringResource(R.string.conclude) else stringResource(R.string.advance)) + } + } + } + } +} diff --git a/app/src/main/java/io/github/sustainow/presentation/viewmodel/FormularyViewModel.kt b/app/src/main/java/io/github/sustainow/presentation/viewmodel/FormularyViewModel.kt index 95974ba..f941260 100644 --- a/app/src/main/java/io/github/sustainow/presentation/viewmodel/FormularyViewModel.kt +++ b/app/src/main/java/io/github/sustainow/presentation/viewmodel/FormularyViewModel.kt @@ -10,6 +10,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sustainow.domain.model.Formulary import io.github.sustainow.domain.model.FormularyAnswer import io.github.sustainow.domain.model.Question +import io.github.sustainow.domain.model.QuestionAlternative import io.github.sustainow.domain.model.UserState import io.github.sustainow.presentation.ui.utils.DataError import io.github.sustainow.presentation.ui.utils.DataOperation @@ -21,100 +22,205 @@ import kotlinx.coroutines.launch @HiltViewModel(assistedFactory = FormularyViewModel.Factory::class) class FormularyViewModel - @AssistedInject - constructor( - private val repository: FormularyRepository, - private val authService: AuthService, - @Assisted("area") private val area: String, - @Assisted("type") private val type: String, - ) : ViewModel() { - private val _formulary = MutableStateFlow(null) - val formulary = _formulary.asStateFlow() - - private val _currentQuestion = MutableStateFlow(null) - val currentQuestion = _currentQuestion.asStateFlow() - - private val _currentAnswers = MutableStateFlow?>(null) - val currentAnswers = _currentAnswers.asStateFlow() - - private val _loading = MutableStateFlow(false) - val loading = _loading.asStateFlow() - private val _error = MutableStateFlow(null) - val error = _error.asStateFlow() - - init { - viewModelScope.launch { - try { - _formulary.value = repository.getFormulary(area, type) - } catch (e: Exception) { - throw e - Log.e("HomeViewModel", e.message ?: "") - _error.value = formulary.value?.let { DataError(source = it, operation = DataOperation.GET) } - } +@AssistedInject +constructor( + private val repository: FormularyRepository, + private val authService: AuthService, + @Assisted("area") private val area: String, + @Assisted("type") private val type: String, +) : ViewModel() { + private val _formulary = MutableStateFlow(null) + val formulary = _formulary.asStateFlow() + + private val _currentQuestion = MutableStateFlow(null) + val currentQuestion = _currentQuestion.asStateFlow() + + private val _currentAnswers = MutableStateFlow>(emptyList()) + val currentAnswers = _currentAnswers.asStateFlow() + + private val _loading = MutableStateFlow(false) + val loading = _loading.asStateFlow() + private val _error = MutableStateFlow(null) + val error = _error.asStateFlow() + + // Novo estado de sucesso + private val _success = MutableStateFlow(false) + val success = _success.asStateFlow() + val userStateLogged = authService.user.value + + init { + viewModelScope.launch { + _loading.value = true + try { + _formulary.value = repository.getFormulary(area, type) + _currentQuestion.value = formulary.value?.questions?.get(0) + } catch (e: Exception) { + Log.e("HomeViewModel", e.message ?: "") + _error.value = formulary.value?.let { DataError(source = it, operation = DataOperation.GET) } + } finally { + _loading.value = false } } + } - fun retryFormularyFetch() { - viewModelScope.launch { - try { - _formulary.value = repository.getFormulary(area, type) - } catch (e: Exception) { - _error.value = DataError(source = formulary, operation = DataOperation.GET) - } + fun retryFormularyFetch() { + viewModelScope.launch { + _loading.value = true + try { + _formulary.value = repository.getFormulary(area, type) + } catch (e: Exception) { + _error.value = DataError(source = formulary, operation = DataOperation.GET) + } finally { + _loading.value = false } } + } - fun goToNextQuestion() { - if (currentQuestion.value == null) { - _currentQuestion.value = formulary.value?.questions?.first() - } else if (currentQuestion.value?.id == null) { - _error.value = DataError(source = currentQuestion, operation = DataOperation.GET) - } else { - currentQuestion.value?.id?.let { - val nextQuestion = - formulary.value?.questions?.find { question -> question.id == it + 1 } - ?: currentQuestion.value - _currentQuestion.value = nextQuestion - } + fun goToNextQuestion() { + Log.i("question", "${currentQuestion.value}") + if (currentQuestion.value == null) { + _currentQuestion.value = formulary.value?.questions?.first() + } else if (currentQuestion.value?.id == null) { + _error.value = DataError(source = currentQuestion, operation = DataOperation.GET) + } else { + currentQuestion.value?.id?.let { + val nextQuestion = + formulary.value?.questions?.find { question -> question.id == it + 1 } + ?: currentQuestion.value + _currentQuestion.value = nextQuestion } } + } + + fun goToPreviousQuestion() { + Log.i("question", "${currentQuestion.value}") + if (currentQuestion.value?.id == null) { + _error.value = DataError(source = currentQuestion, operation = DataOperation.GET) + } + if (currentQuestion.value == formulary.value?.questions?.first()) { + return + } + currentQuestion.value?.id?.let { + val previousQuestion = formulary.value?.questions?.find { question -> question.id == it - 1 } + _currentQuestion.value = previousQuestion + } + } - fun goToPreviousQuestion() { - if (currentQuestion.value?.id == null) { - _error.value = DataError(source = currentQuestion, operation = DataOperation.GET) + fun calculateTotalValue(): Float { + var total = 0f + Log.i("viewModel", "${_currentAnswers.value}") + for (answer in _currentAnswers.value) { + total += answer.value + } + return total + } + + fun sendAnswers() { + Log.i("viewModel", "${_currentAnswers.value}") + viewModelScope.launch { + _loading.value = true + try { + val currentUserState = authService.user.value + if (currentUserState is UserState.Logged) { + repository.addAnswers(currentAnswers.value ?: emptyList(), currentUserState.user.uid) + _success.value = true // Definindo como true após sucesso + } else { + _error.value = DataError(source = currentUserState, operation = DataOperation.CREATE) + } + } catch (e: Exception) { + _error.value = DataError(source = currentAnswers, operation = DataOperation.CREATE) + } finally { + _loading.value = false } - if (currentQuestion.value == formulary.value?.questions?.first()) { - return + } + } + + fun addAnswerToQuestion( + question: Question, + selectedAlternative: QuestionAlternative, + formId: Int?, + uid: String, + groupName: String?, + month: Int + ) { + // Acessa a lista de respostas atual sem criar uma nova + val existingAnswers = _currentAnswers.value + + when (question) { + is Question.SingleSelect -> { + // Substituir a resposta existente' + _currentAnswers.value = existingAnswers.filter { it.questionId != question.id } + + FormularyAnswer( + formId = formId, + uid = uid, + groupName = "", + questionId = question.id, + value = selectedAlternative.value, + timePeriod = selectedAlternative.timePeriod, + unit = selectedAlternative.unit, + month = month + ) + Log.i("viewModel", "${_currentAnswers.value}") } - currentQuestion.value?.id?.let { - val previousQuestion = formulary.value?.questions?.find { question -> question.id == it - 1 } - _currentQuestion.value = previousQuestion + + is Question.Numerical -> { + // Atualizar a resposta existente ou adicionar nova + _currentAnswers.value = existingAnswers.filter { it.questionId != question.id } + + FormularyAnswer( + formId = formId, + uid = uid, + groupName = "", + questionId = question.id, + value = selectedAlternative.value, + timePeriod = selectedAlternative.timePeriod, + unit = selectedAlternative.unit, + month = month + ) } - } - fun sendAnswers() { - viewModelScope.launch { - _loading.value = true - try { - val currentUserState = authService.user.value - if (currentUserState is UserState.Logged) { - repository.addAnswers(currentAnswers.value ?: emptyList(), currentUserState.user.uid) - } else { - _error.value = DataError(source = currentUserState, operation = DataOperation.CREATE) - } - } catch (e: Exception) { - _error.value = DataError(source = currentAnswers, operation = DataOperation.CREATE) - } finally { - _loading.value = false + is Question.MultiSelect -> { + // Adicionar ou remover a resposta, dependendo se já existe + val updatedAnswers = if (existingAnswers.any { it.questionId == question.id && it.value == selectedAlternative.value }) { + // Se já existe, remover a resposta + existingAnswers.filter { it.questionId != question.id || it.value != selectedAlternative.value } + } else { + // Caso contrário, adicionar a nova resposta + existingAnswers + FormularyAnswer( + formId = formId, + uid = uid, + groupName = "", + questionId = question.id, + value = selectedAlternative.value, + timePeriod = selectedAlternative.timePeriod, + unit = selectedAlternative.unit, + month = month + ) } + _currentAnswers.value = updatedAnswers } - } - @AssistedFactory - interface Factory { - fun create( - @Assisted("area") area: String, - @Assisted("type") type: String, - ): FormularyViewModel + is Question.MultiItem -> { + // Adicionar novas respostas com groupName + _currentAnswers.value = existingAnswers + FormularyAnswer( + formId = formId, + uid = uid, + groupName = groupName ?: "", // Se groupName for nulo, passar uma string vazia + questionId = question.id, + value = selectedAlternative.value, + timePeriod = selectedAlternative.timePeriod, + unit = selectedAlternative.unit, + month = month + ) + } } } + + + @AssistedFactory + interface Factory { + fun create( + @Assisted("area") area: String, + @Assisted("type") type: String, + ): FormularyViewModel + } +} \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3e8b064..cf9a92e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -51,4 +51,11 @@ Erro ao buscar os dados da questão Erro ao submeter respostas, você deve estar autenticado para salvar as respostas do formulário Erro ao salvar as novas respostas do formulário + Resultado + Por favor, tente novamente ou volte à tela anterior + Retornar + Concluir + Avançar + Erro ao carregar o formulário + Erro ao carregar as respostas \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e3779fc..dec8228 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,4 +51,11 @@ Error fetching question data Error submiting new answers, you must be authenticated to save formulary answers Error while saving new formulary answers + Result + Please try again or return to the previous screen + Return + Conclude + Advance + Error loading form + Error loading asnwers \ No newline at end of file