diff --git a/app/src/main/java/com/example/rememberme/di/AppModule.kt b/app/src/main/java/com/example/rememberme/di/AppModule.kt index 8ca94ee..0f555e0 100644 --- a/app/src/main/java/com/example/rememberme/di/AppModule.kt +++ b/app/src/main/java/com/example/rememberme/di/AppModule.kt @@ -15,6 +15,7 @@ import com.example.rememberme.domain.usecases.people.GetAllPeople import com.example.rememberme.domain.usecases.people.GetPersonById import com.example.rememberme.domain.usecases.people.InsertNewPerson import com.example.rememberme.domain.usecases.people.PeopleUseCases +import com.example.rememberme.domain.usecases.people.UpdatePerson import com.example.rememberme.utils.Constants.PEOPLE_DATABASE_NAME import com.example.remindme.database.PeopleDao import dagger.Module @@ -62,7 +63,8 @@ object AppModule { return PeopleUseCases( getAllPeople = GetAllPeople(peopleRepository), getPersonById = GetPersonById(peopleRepository), - insertPerson = InsertNewPerson(peopleRepository) + insertPerson = InsertNewPerson(peopleRepository), + updatePerson = UpdatePerson(peopleRepository) ) } diff --git a/app/src/main/java/com/example/rememberme/domain/usecases/people/PeopleUseCases.kt b/app/src/main/java/com/example/rememberme/domain/usecases/people/PeopleUseCases.kt index 76cdffb..a1914a4 100644 --- a/app/src/main/java/com/example/rememberme/domain/usecases/people/PeopleUseCases.kt +++ b/app/src/main/java/com/example/rememberme/domain/usecases/people/PeopleUseCases.kt @@ -3,5 +3,6 @@ package com.example.rememberme.domain.usecases.people data class PeopleUseCases( val getAllPeople: GetAllPeople, val getPersonById: GetPersonById, - val insertPerson: InsertNewPerson + val insertPerson: InsertNewPerson, + val updatePerson: UpdatePerson ) \ No newline at end of file diff --git a/app/src/main/java/com/example/rememberme/domain/usecases/people/UpdatePerson.kt b/app/src/main/java/com/example/rememberme/domain/usecases/people/UpdatePerson.kt new file mode 100644 index 0000000..231bfed --- /dev/null +++ b/app/src/main/java/com/example/rememberme/domain/usecases/people/UpdatePerson.kt @@ -0,0 +1,17 @@ +package com.example.rememberme.domain.usecases.people + +import android.util.Log +import com.example.rememberme.domain.model.People +import com.example.rememberme.domain.repository.PeopleRepository + +class UpdatePerson( + private val peopleRepository: PeopleRepository +) { + suspend operator fun invoke(person: People) { + Log.i(TAG,"UpdatePersonUseCase: invoked") + peopleRepository.updatePeople(person) + } + companion object { + private const val TAG = "UpdatePerson" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonScreen.kt b/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonScreen.kt index 2e30b2b..134b1c3 100644 --- a/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonScreen.kt +++ b/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonScreen.kt @@ -39,7 +39,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -54,6 +54,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.example.rememberme.R import com.example.rememberme.presentation.common.composables.CustomButton import com.example.rememberme.presentation.common.composables.CustomErrorText @@ -62,23 +63,43 @@ import com.example.rememberme.ui.theme.RememberMeTheme import kotlinx.coroutines.launch import java.util.Calendar -@OptIn(ExperimentalMaterial3Api::class) private const val TAG = "AddPersonScreen" + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AddPersonScreen( viewModel: AddPersonViewModel = hiltViewModel(), popUp: () -> Unit, + personId: Long? ) { - val uiState = viewModel.uiState.collectAsState().value - val isPersonSaved by viewModel.isPersonSaved.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val isPersonSaved by viewModel.isPersonSaved.collectAsStateWithLifecycle() val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() var showBottomSheet by remember { mutableStateOf(false) } - var selectedAvatarResId by remember { mutableIntStateOf(R.drawable.ic_m1) } + var selectedAvatarResId by remember { + if (personId == null) { + Log.i(TAG, "AddPersonScreen: Person is null") + mutableIntStateOf(R.drawable.ic_m1) + } else { + Log.i(TAG, "AddPersonScreen: Person is not null") + mutableIntStateOf(uiState.avatar) + } + } if (isPersonSaved) { popUp() } + + LaunchedEffect(personId) { + if (personId != null) { + Log.i(TAG,"LoadPersonDetails on the addPerson screen") + viewModel.loadPersonDetails(personId) + } else { + Log.i(TAG,"creating new person screen") + viewModel.resetForm() + } + } AddPersonContent( uiState = uiState, onFirstNameChange = { viewModel.onEvent(AddPersonEvents.OnFirstNameChange(it)) }, @@ -174,9 +195,8 @@ fun AddPersonContent( errorText = uiState.placeError ) DateTimePicker( - initialDateTime = uiState.time, + uiState = uiState, onDateTimeChange = onTimeChange, - uiState ) GenderRadioButton( selectedGender = uiState.gender, @@ -209,23 +229,25 @@ fun AddPersonContent( Spacer(modifier = Modifier.size(16.dp)) CustomButton( onClick = onSavePerson, - text = "Save" + text = if (uiState.id != null) "Update" else "Save" ) } } } + @Composable fun DateTimePicker( - initialDateTime: String, + uiState: AddPersonUiState, onDateTimeChange: (String) -> Unit, - uiState: AddPersonUiState ) { val context = LocalContext.current val calendar = Calendar.getInstance() - var selectedDateTime by remember { mutableStateOf(initialDateTime) } + val selectedDateTime = remember { mutableStateOf("") } + selectedDateTime.value = uiState.time + Log.i(TAG,"currentDate from the UiState: ${uiState.time}") - if (selectedDateTime.isEmpty()) { + if (uiState.time.isBlank()) { calendar.timeInMillis = System.currentTimeMillis() } // Create a ContextThemeWrapper with your custom theme @@ -241,8 +263,8 @@ fun DateTimePicker( { _: TimePicker, hourOfDay: Int, minute: Int -> calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) calendar.set(Calendar.MINUTE, minute) - selectedDateTime = "${dayOfMonth}/${month + 1}/${year} ${hourOfDay}:${minute}" - onDateTimeChange(selectedDateTime) + selectedDateTime.value = "${dayOfMonth}/${month + 1}/${year} ${hourOfDay}:${minute}" + onDateTimeChange(selectedDateTime.value) }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), @@ -257,7 +279,11 @@ fun DateTimePicker( Column { OutlinedButton(onClick = { datePickerDialog.show() }) { - Text(text = selectedDateTime.ifEmpty { "Pick Date & Time" }) + if(selectedDateTime.value.isEmpty()) { + Text(text = "Pick Date & Time") + }else{ + Text(text = uiState.time) + } } if (uiState.timeError != null) { Text( @@ -335,7 +361,7 @@ fun GenderRadioButton( modifier: Modifier = Modifier, errorMessage: String? ) { - Log.d(TAG, "GenderRadioButton: $errorMessage") + Log.d(TAG, "GenderRadioButtonError: $errorMessage") val genderOptions = listOf("Male", "Female") Column(modifier = modifier) { Text("Gender", modifier = Modifier.padding(bottom = 4.dp)) diff --git a/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonUiState.kt b/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonUiState.kt index 9abdf2e..202b6f7 100644 --- a/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonUiState.kt +++ b/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonUiState.kt @@ -3,16 +3,17 @@ package com.example.rememberme.presentation.addperson import com.example.rememberme.R data class AddPersonUiState( + val id: Long? = null, val firstName: String = "", - val firstNameError: String? = null, val secondName: String = "", - val secondNameError: String? = null, val place: String = "", - val placeError: String? = null, val time: String = "", - val timeError: String? = null, val note: String? = null, val gender: String = "", - val genderError: String? = null, - val avatar: Int = R.drawable.ic_f1, -) \ No newline at end of file + val avatar: Int = R.drawable.ic_m1, + val firstNameError: String? = null, + val secondNameError: String? = null, + val placeError: String? = null, + val timeError: String? = null, + val genderError: String? = null +) diff --git a/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonViewModel.kt b/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonViewModel.kt index f0b0e92..1c3cc80 100644 --- a/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonViewModel.kt +++ b/app/src/main/java/com/example/rememberme/presentation/addperson/AddPersonViewModel.kt @@ -19,6 +19,7 @@ class AddPersonViewModel @Inject constructor( private val peopleUseCases: PeopleUseCases, private val addPersonUseCases: AddPersonUseCases ) : ViewModel() { + private val _uiState = MutableStateFlow(AddPersonUiState()) val uiState: StateFlow = _uiState @@ -28,56 +29,113 @@ class AddPersonViewModel @Inject constructor( fun onEvent(event: AddPersonEvents) { when (event) { is AddPersonEvents.OnFirstNameChange -> { - Log.i(TAG, "onEvent: OnFirstNameChange -> ${event.firstName}") _uiState.value = _uiState.value.copy(firstName = event.firstName, firstNameError = null) } - is AddPersonEvents.OnSecondNameChange -> { - Log.i(TAG, "onEvent: OnSecondNameChange -> ${event.secondName}") _uiState.value = _uiState.value.copy(secondName = event.secondName, secondNameError = null) } - is AddPersonEvents.OnPlaceChange -> { - Log.i(TAG, "onEvent: OnPlaceChange -> ${event.place}") _uiState.value = _uiState.value.copy(place = event.place, placeError = null) } - is AddPersonEvents.OnTimeChange -> { - Log.i(TAG, "onEvent: OnTimeChange -> ${event.time}") _uiState.value = _uiState.value.copy(time = event.time) } - is AddPersonEvents.OnNoteChange -> { - Log.i(TAG, "onEvent: OnNoteChange -> ${event.note}") _uiState.value = _uiState.value.copy(note = event.note) } - is AddPersonEvents.OnGenderChange -> { - Log.i(TAG, "onEvent: OnGenderChange -> ${event.gender}") _uiState.value = _uiState.value.copy(gender = event.gender, genderError = null) } - is AddPersonEvents.OnAvatarChange -> { - - Log.i(TAG, "onEvent: OnAvatarChange -> ${event.avatar}") _uiState.value = _uiState.value.copy(avatar = event.avatar) } - AddPersonEvents.OnSavePerson -> { - Log.i(TAG, "onEvent: OnSavePerson") - savePerson() + if (_uiState.value.id != null) { + updatePerson() + } else { + savePerson() + } + } + } + } + + fun loadPersonDetails(personId: Long) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val person = peopleUseCases.getPersonById(personId) + person.collect { person -> + if (person != null) { + _uiState.value = _uiState.value.copy( + id = person.id, + firstName = person.firstName, + secondName = person.secondName, + place = person.place, + gender = person.gender, + avatar = person.avatar, + time = person.time, + note = person.note, + ) + } + } } } } + fun resetForm() { + _uiState.value = AddPersonUiState() + } + private fun savePerson() { + if (validateInput()) { + viewModelScope.launch { + withContext(Dispatchers.IO) { + val person = People( + firstName = _uiState.value.firstName, + secondName = _uiState.value.secondName, + place = _uiState.value.place, + time = _uiState.value.time, + note = _uiState.value.note, + gender = _uiState.value.gender, + avatar = _uiState.value.avatar + ) + peopleUseCases.insertPerson(person) + } + _isPersonSaved.value = true + } + } + } + + private fun updatePerson() { + if (validateInput()) { + Log.i(TAG,"validateInput on update: input are valid!") + viewModelScope.launch { + withContext(Dispatchers.IO) { + val person :People = _uiState.value.id?.let { + People( + id = it, + firstName = _uiState.value.firstName, + secondName = _uiState.value.secondName, + place = _uiState.value.place, + time = _uiState.value.time, + note = _uiState.value.note, + gender = _uiState.value.gender, + avatar = _uiState.value.avatar + ) + }!! + peopleUseCases.updatePerson(person) + } + _isPersonSaved.value = true + } + } + } + + private fun validateInput(): Boolean { val firstNameResult = addPersonUseCases.validateFirstNameUseCase(_uiState.value.firstName) - val secondNameResult = - addPersonUseCases.validateSecondNameUseCase(_uiState.value.secondName) + val secondNameResult = addPersonUseCases.validateSecondNameUseCase(_uiState.value.secondName) val placeResult = addPersonUseCases.validatePlaceUseCase(_uiState.value.place) val timeResult = addPersonUseCases.validateTimeUseCase(_uiState.value.time) val genderResult = addPersonUseCases.validateGenderSelectionUseCase(_uiState.value.gender) - Log.d(TAG, "savePerson: $genderResult") + val hasError = listOf( firstNameResult, secondNameResult, @@ -85,9 +143,8 @@ class AddPersonViewModel @Inject constructor( timeResult, genderResult ).any { !it.successful } + if (hasError) { - Log.e(TAG, "savePerson: There are some unvalidated inputs") - Log.e(TAG, "savePerson: ${_uiState.value}") _uiState.value = _uiState.value.copy( firstNameError = firstNameResult.errorMessage, secondNameError = secondNameResult.errorMessage, @@ -96,30 +153,12 @@ class AddPersonViewModel @Inject constructor( genderError = genderResult.errorMessage ) _isPersonSaved.value = false - - } else { - viewModelScope.launch { - withContext(Dispatchers.IO) { - Log.i(TAG, "savePerson: ${_uiState.value}") - val person = People( - firstName = _uiState.value.firstName, - secondName = _uiState.value.secondName, - place = _uiState.value.place, - time = _uiState.value.time, - note = _uiState.value.note, - gender = _uiState.value.gender, - avatar = _uiState.value.avatar - ) - peopleUseCases.insertPerson(person) - } - _isPersonSaved.value = true - - } + return false } + return true } companion object { private const val TAG = "AddPersonViewModel" } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/rememberme/presentation/details/PersonDetails.kt b/app/src/main/java/com/example/rememberme/presentation/details/PersonDetails.kt index b8df60b..483ca96 100644 --- a/app/src/main/java/com/example/rememberme/presentation/details/PersonDetails.kt +++ b/app/src/main/java/com/example/rememberme/presentation/details/PersonDetails.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -20,12 +21,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonColors import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,7 +36,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -53,7 +53,8 @@ private const val TAG = "PersonDetails" fun PersonDetailsScreen( viewModel: PersonDetailsViewModel = hiltViewModel(), personId: Long, - navigateUp: () -> Unit + navigateUp: () -> Unit, + navigateToEditScreen: (Long?) -> Unit ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() @@ -75,7 +76,7 @@ fun PersonDetailsScreen( } uiState.value.person != null -> { Log.d(TAG, "PersonDetailsScreen: ${uiState.value.person}") - PersonDetailsContent(uiState.value.person!!, navigateUp) + PersonDetailsContent(uiState.value.person!!, navigateUp, navigateToEditScreen) } else -> { Log.e(TAG, "PersonDetailsScreen: Person not found") @@ -118,6 +119,7 @@ fun LoadingIndicator() { fun PersonDetailsContent( person: People, navigateUp: () -> Unit, + navigateToEditScreen: (Long?) -> Unit, modifier: Modifier = Modifier ) { val scrollState = rememberScrollState() @@ -143,24 +145,38 @@ fun PersonDetailsContent( .height(200.dp) ) { - IconButton( + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .align(Alignment.TopStart) - .padding(top = 16.dp, start = 8.dp), - colors = IconButtonColors( - contentColor = MaterialTheme.colorScheme.onPrimary, - disabledContentColor = MaterialTheme.colorScheme.onPrimary, - containerColor = Color.Transparent, - disabledContainerColor = Color.Transparent - ), - onClick = { - Log.d(TAG, "PersonDetailsContent: Clicked") - navigateUp() - }) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = null - ) + .fillMaxWidth() + .padding(top = 26.dp, start = 4.dp), + ) { + IconButton( + onClick = { + Log.d(TAG, "PersonDetailsContent: Back arrow Clicked!") + navigateUp() + }) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + } + Row { + IconButton(onClick = { + Log.d(TAG, "PersonDetailsContent: Edit Person Clicked, navigating to edit screen with personId: ${person.id}") + navigateToEditScreen(person.id) + }) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + } } Box( @@ -252,7 +268,8 @@ fun PersonDetailsContentPreview() { ), navigateUp = { Log.d(TAG, "PersonDetailsContentPreview: Clicked") - } + }, + navigateToEditScreen = {} ) } } diff --git a/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt b/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt index 7677ac7..7628aa1 100644 --- a/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt +++ b/app/src/main/java/com/example/rememberme/presentation/navgraph/NavGraph.kt @@ -1,5 +1,6 @@ package com.example.rememberme.presentation.navgraph +import android.util.Log import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -13,12 +14,12 @@ import com.example.rememberme.presentation.details.PersonDetailsScreen import com.example.rememberme.presentation.onboarding.OnBoardingScreen import com.example.rememberme.presentation.peopleList.PeopleScreen +private const val TAG = "NavGraph" @Composable fun NavGraph( startDestination: String, navController: NavHostController = rememberNavController() ) { - NavHost( navController = navController, startDestination = startDestination @@ -30,8 +31,7 @@ fun NavGraph( composable( route = Routes.OnBoardingScreen.route ) { - OnBoardingScreen( - ) + OnBoardingScreen() } } navigation( @@ -41,37 +41,52 @@ fun NavGraph( composable( route = Routes.PeopleListScreen.route ) { - PeopleScreen(navigateToDetailScreen = { personId -> - navController.navigate(Routes.PersonDetailsScreen.route + "/$personId") - }, + PeopleScreen( + navigateToDetailScreen = { personId -> + navController.navigate("${Routes.PersonDetailsScreen.route}/$personId") + }, navigateToAddNewPersonScreen = { navController.navigate(Routes.AddPersonScreen.route) - }) + } + ) + } + composable(route = Routes.AddPersonScreen.route) { + AddPersonScreen( + popUp = { navController.navigateUp() }, + personId = null // Explicitly pass null + ) } composable( - route = Routes.AddPersonScreen.route, + route = "${Routes.AddPersonScreen.route}/{personId}", + arguments = listOf(navArgument("personId") { type = NavType.StringType; nullable = true }) ) { + val personId = it.arguments?.getString("personId")?.toLongOrNull() + AddPersonScreen( popUp = { navController.navigateUp() - } + }, + personId = personId ) } composable( - route = Routes.PersonDetailsScreen.route + "/{personId}", - arguments = listOf(navArgument("personId") { type = NavType.LongType }) - ) { - val personId = it.arguments?.getLong("personId") + route = "${Routes.PersonDetailsScreen.route}/{personId}", + arguments = listOf(navArgument("personId") { type = NavType.StringType }) + ) { it -> + val personId = it.arguments?.getString("personId")?.toLong() if (personId != null) { PersonDetailsScreen( personId = personId, navigateUp = { navController.navigateUp() + }, + navigateToEditScreen = {id -> + Log.i(TAG, "NavGraph: NavigateToEditScreen triggered") + navController.navigate("${Routes.AddPersonScreen.route}/$id") } ) } } } } - -} \ No newline at end of file +}