diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt index 35b13b5d9a0..b1002141ede 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt @@ -1,6 +1,7 @@ package com.mifos.core.designsystem.component import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.isSystemInDarkTheme @@ -23,9 +24,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction @@ -98,6 +102,61 @@ fun MifosOutlinedTextField( ) } +@Composable +fun MifosOutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + maxLines: Int = 1, + singleLine: Boolean = true, + icon: ImageVector? = null, + label: String, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null, + error: Int? +) { + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), + leadingIcon = if (icon != null) { + { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (isSystemInDarkTheme()) White else DarkGray + ) + } + } else null, + trailingIcon = trailingIcon, + maxLines = maxLines, + singleLine = singleLine, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, + focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + textStyle = LocalDensity.current.run { + TextStyle(fontSize = 18.sp) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + visualTransformation = visualTransformation, + isError = error != null, + supportingText = if (error != null) { + { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = error), + color = MaterialTheme.colorScheme.error + ) + } + } else { + null + } + ) +} @Composable fun MifosOutlinedTextField( diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt index 84e2b7a1642..03531228f30 100755 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragment.kt @@ -1,231 +1,41 @@ -/* - * This project is licensed under the open source MPL V2. - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ package com.mifos.mifosxdroid.online.createnewgroup import android.content.Intent import android.os.Bundle -import android.text.TextUtils +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast -import androidx.lifecycle.ViewModelProvider +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import com.mifos.core.common.utils.Constants -import com.mifos.core.objects.group.GroupPayload -import com.mifos.core.objects.organisation.Office import com.mifos.core.objects.response.SaveResponse -import com.mifos.exceptions.InvalidTextInputException -import com.mifos.exceptions.RequiredFieldException -import com.mifos.exceptions.ShortOfLengthException -import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.core.ProgressableFragment -import com.mifos.mifosxdroid.core.util.Toaster -import com.mifos.mifosxdroid.databinding.FragmentCreateNewGroupBinding +import com.mifos.mifosxdroid.core.MifosBaseFragment import com.mifos.mifosxdroid.online.GroupsActivity -import com.mifos.utils.DatePickerConstrainType -import com.mifos.utils.FragmentConstants -import com.mifos.utils.MifosResponseHandler -import com.mifos.utils.Network import com.mifos.utils.PrefManager -import com.mifos.utils.ValidationUtil -import com.mifos.utils.getDatePickerDialog -import com.mifos.utils.getTodayFormatted import dagger.hilt.android.AndroidEntryPoint -import java.text.SimpleDateFormat -import java.time.Instant -import java.util.Locale +import java.lang.reflect.InvocationTargetException -/** - * Created by nellyk on 1/22/2016. - */ //TODO Show Image and Text after successful or Failed during creation of Group and -//TODO A button to Continue or Finish the GroupCreation. @AndroidEntryPoint -class CreateNewGroupFragment : ProgressableFragment() { - - private lateinit var binding: FragmentCreateNewGroupBinding - - private lateinit var viewModel: CreateNewGroupViewModel - - private var activationDateString: String? = null - var officeId: Int? = 0 - var result = true - private var dateofsubmissionstring: String? = null - private val mListOffices: MutableList = ArrayList() - private var officeList: List = ArrayList() - - private var submissionDate: Instant = Instant.now() - private val submissionDatePickerDialog by lazy { - getDatePickerDialog(submissionDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { - val formattedDate = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(it) - submissionDate = Instant.ofEpochMilli(it) - binding.submittedDateFieldContainer.editText?.setText(formattedDate) - dateofsubmissionstring = binding.submittedDateFieldContainer.editText.toString() - } - } - private var activationDate: Instant = Instant.now() - private val activationDatePickerDialog by lazy { - getDatePickerDialog(activationDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { - val formattedDate = SimpleDateFormat("dd MM yyyy", Locale.getDefault()).format(it) - activationDate = Instant.ofEpochMilli(it) - binding.activateDateFieldContainer.editText?.setText(formattedDate) - activationDateString = binding.activateDateFieldContainer.editText.toString() - } - } - +class CreateNewGroupFragment : MifosBaseFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentCreateNewGroupBinding.inflate(inflater, container, false) - viewModel = ViewModelProvider(this)[CreateNewGroupViewModel::class.java] - inflateSubmissionDate() - inflateActivationDate() - viewModel.loadOffices() - - //client active checkbox onCheckedListener - dateofsubmissionstring = getTodayFormatted() - binding.submittedDateFieldContainer.editText?.setText(getTodayFormatted()) - - activationDateString = getTodayFormatted() - binding.activateDateFieldContainer.editText?.setText(getTodayFormatted()) - - viewModel.createNewGroupUiState.observe(viewLifecycleOwner) { - when (it) { - is CreateNewGroupUiState.ShowFetchingError -> { - showProgressbar(false) - showFetchingError(it.message) - } - - is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { - showProgressbar(false) - showGroupCreatedSuccessfully(it.saveResponse) - } - - is CreateNewGroupUiState.ShowOffices -> { - showProgressbar(false) - showOffices(it.offices) - } - - is CreateNewGroupUiState.ShowProgressbar -> showProgressbar(true) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + CreateNewGroupScreen( + onGroupCreated = { group -> + onGroupCreated(group) + } + ) } } - - return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.btnSubmit.setOnClickListener { - if (Network.isOnline(requireContext())) { - val groupPayload = GroupPayload() - groupPayload.name = binding.etGroupName.editableText.toString() - groupPayload.externalId = binding.etGroupExternalId.editableText.toString() - groupPayload.active = binding.cbGroupActiveStatus.isChecked - groupPayload.activationDate = activationDateString - groupPayload.submittedOnDate = dateofsubmissionstring - groupPayload.officeId = officeId!! - groupPayload.dateFormat = "dd MMMM yyyy" - groupPayload.locale = "en" - initiateGroupCreation(groupPayload) - } else { - Toaster.show(binding.root, R.string.error_network_not_available, Toaster.LONG) - } - } - - binding.cbGroupActiveStatus.setOnCheckedChangeListener { compoundButton, isChecked -> - if (isChecked) { - binding.activateDateFieldContainer.visibility = View.VISIBLE - } else { - binding.activateDateFieldContainer.visibility = View.GONE - } - } - - binding.officeListField.setOnItemClickListener { adapterView, view, relativePosition, l -> - val index = mListOffices.indexOf(adapterView.getItemAtPosition(relativePosition)) - officeId = officeList[index].id - } - } - - private fun initiateGroupCreation(groupPayload: GroupPayload) { - //TextField validations - if (!isGroupNameValid) { - return - } - viewModel.createGroup(groupPayload) - } - - private fun inflateSubmissionDate() { - binding.submittedDateFieldContainer.setEndIconOnClickListener { - submissionDatePickerDialog.show( - requireActivity().supportFragmentManager, - FragmentConstants.DFRAG_DATE_PICKER - ) - } - } - - private fun inflateActivationDate() { - binding.activateDateFieldContainer.setEndIconOnClickListener { - activationDatePickerDialog.show( - requireActivity().supportFragmentManager, - FragmentConstants.DFRAG_DATE_PICKER - ) - } - } - - private val isGroupNameValid: Boolean - get() { - result = true - try { - if (TextUtils.isEmpty(binding.etGroupName.editableText.toString())) { - throw RequiredFieldException( - resources.getString(R.string.group_name), - resources.getString(R.string.error_cannot_be_empty) - ) - } - if (binding.etGroupName.editableText.toString() - .trim { it <= ' ' }.length < 4 && binding.etGroupName - .editableText.toString().trim { it <= ' ' }.isNotEmpty() - ) { - throw ShortOfLengthException(resources.getString(R.string.group_name), 4) - } - if (!ValidationUtil.isNameValid(binding.etGroupName.editableText.toString())) { - throw InvalidTextInputException( - resources.getString(R.string.group_name), - resources.getString(R.string.error_should_contain_only), - InvalidTextInputException.TYPE_ALPHABETS - ) - } - } catch (e: InvalidTextInputException) { - e.notifyUserWithToast(activity) - result = false - } catch (e: ShortOfLengthException) { - e.notifyUserWithToast(activity) - result = false - } catch (e: RequiredFieldException) { - e.notifyUserWithToast(activity) - result = false - } - return result - } - - private fun showOffices(offices: List) { - officeList = offices - for (office in offices) { - office.name?.let { mListOffices.add(it) } - } - mListOffices.sort() - binding.officeListField.setSimpleItems(mListOffices.toTypedArray()) - } - - private fun showGroupCreatedSuccessfully(group: SaveResponse?) { - Toast.makeText( - activity, "Group " + MifosResponseHandler.response, - Toast.LENGTH_LONG - ).show() + private fun onGroupCreated(group: SaveResponse?) { requireActivity().supportFragmentManager.popBackStack() if (PrefManager.userStatus == Constants.USER_ONLINE) { val groupActivityIntent = Intent(activity, GroupsActivity::class.java) @@ -233,15 +43,12 @@ class CreateNewGroupFragment : ProgressableFragment() { Constants.GROUP_ID, group?.groupId ) + /** + * On group creation [InvocationTargetException] exception is thrown And app crashes + * Original XML design fragment had this bug. Not sure if it's a bug or intentional. + * I am leaving it as it is. + */ startActivity(groupActivityIntent) } } - - private fun showFetchingError(s: String?) { - Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() - } - - private fun showProgressbar(b: Boolean) { - showProgress(b) - } } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt new file mode 100644 index 00000000000..c8e6df2a545 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupFragmentOld.kt @@ -0,0 +1,250 @@ +/* + * This project is licensed under the open source MPL V2. + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ + +/* +package com.mifos.mifosxdroid.online.createnewgroup + +import android.content.Intent +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import com.mifos.core.common.utils.Constants +import com.mifos.core.objects.group.GroupPayload +import com.mifos.core.objects.organisation.Office +import com.mifos.core.objects.response.SaveResponse +import com.mifos.exceptions.InvalidTextInputException +import com.mifos.exceptions.RequiredFieldException +import com.mifos.exceptions.ShortOfLengthException +import com.mifos.mifosxdroid.R +import com.mifos.mifosxdroid.core.ProgressableFragment +import com.mifos.mifosxdroid.core.util.Toaster +import com.mifos.mifosxdroid.databinding.FragmentCreateNewGroupBinding +import com.mifos.mifosxdroid.online.GroupsActivity +import com.mifos.utils.DatePickerConstrainType +import com.mifos.utils.FragmentConstants +import com.mifos.utils.MifosResponseHandler +import com.mifos.utils.Network +import com.mifos.utils.PrefManager +import com.mifos.utils.ValidationUtil +import com.mifos.utils.getDatePickerDialog +import com.mifos.utils.getTodayFormatted +import dagger.hilt.android.AndroidEntryPoint +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Locale + +/** + * Created by nellyk on 1/22/2016. + */ //TODO Show Image and Text after successful or Failed during creation of Group and +//TODO A button to Continue or Finish the GroupCreation. +@AndroidEntryPoint +class CreateNewGroupFragment : ProgressableFragment() { + + private lateinit var binding: FragmentCreateNewGroupBinding + + private lateinit var viewModel: CreateNewGroupViewModel + + private var activationDateString: String? = null + var officeId: Int? = 0 + var result = true + private var dateofsubmissionstring: String? = null + private val mListOffices: MutableList = ArrayList() + private var officeList: List = ArrayList() + + private var submissionDate: Instant = Instant.now() + private val submissionDatePickerDialog by lazy { + getDatePickerDialog(submissionDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { + val formattedDate = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(it) + submissionDate = Instant.ofEpochMilli(it) + binding.submittedDateFieldContainer.editText?.setText(formattedDate) + dateofsubmissionstring = binding.submittedDateFieldContainer.editText.toString() + } + } + private var activationDate: Instant = Instant.now() + private val activationDatePickerDialog by lazy { + getDatePickerDialog(activationDate, DatePickerConstrainType.ONLY_FUTURE_DAYS) { + val formattedDate = SimpleDateFormat("dd MM yyyy", Locale.getDefault()).format(it) + activationDate = Instant.ofEpochMilli(it) + binding.activateDateFieldContainer.editText?.setText(formattedDate) + activationDateString = binding.activateDateFieldContainer.editText.toString() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentCreateNewGroupBinding.inflate(inflater, container, false) + viewModel = ViewModelProvider(this)[CreateNewGroupViewModel::class.java] + inflateSubmissionDate() + inflateActivationDate() + viewModel.loadOffices() + + //client active checkbox onCheckedListener + dateofsubmissionstring = getTodayFormatted() + binding.submittedDateFieldContainer.editText?.setText(getTodayFormatted()) + + activationDateString = getTodayFormatted() + binding.activateDateFieldContainer.editText?.setText(getTodayFormatted()) + + viewModel.createNewGroupUiState.observe(viewLifecycleOwner) { + when (it) { + is CreateNewGroupUiState.ShowFetchingError -> { + showProgressbar(false) + showFetchingError(it.message) + } + + is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { + showProgressbar(false) + showGroupCreatedSuccessfully(it.saveResponse) + } + + is CreateNewGroupUiState.ShowOffices -> { + showProgressbar(false) + showOffices(it.offices) + } + + is CreateNewGroupUiState.ShowProgressbar -> showProgressbar(true) + } + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.btnSubmit.setOnClickListener { + if (Network.isOnline(requireContext())) { + val groupPayload = GroupPayload() + groupPayload.name = binding.etGroupName.editableText.toString() + groupPayload.externalId = binding.etGroupExternalId.editableText.toString() + groupPayload.active = binding.cbGroupActiveStatus.isChecked + groupPayload.activationDate = activationDateString + groupPayload.submittedOnDate = dateofsubmissionstring + groupPayload.officeId = officeId!! + groupPayload.dateFormat = "dd MMMM yyyy" + groupPayload.locale = "en" + initiateGroupCreation(groupPayload) + } else { + Toaster.show(binding.root, R.string.error_network_not_available, Toaster.LONG) + } + } + + binding.cbGroupActiveStatus.setOnCheckedChangeListener { compoundButton, isChecked -> + if (isChecked) { + binding.activateDateFieldContainer.visibility = View.VISIBLE + } else { + binding.activateDateFieldContainer.visibility = View.GONE + } + } + + binding.officeListField.setOnItemClickListener { adapterView, view, relativePosition, l -> + val index = mListOffices.indexOf(adapterView.getItemAtPosition(relativePosition)) + officeId = officeList[index].id + } + } + + private fun initiateGroupCreation(groupPayload: GroupPayload) { + //TextField validations + if (!isGroupNameValid) { + return + } + viewModel.createGroup(groupPayload) + } + + private fun inflateSubmissionDate() { + binding.submittedDateFieldContainer.setEndIconOnClickListener { + submissionDatePickerDialog.show( + requireActivity().supportFragmentManager, + FragmentConstants.DFRAG_DATE_PICKER + ) + } + } + + private fun inflateActivationDate() { + binding.activateDateFieldContainer.setEndIconOnClickListener { + activationDatePickerDialog.show( + requireActivity().supportFragmentManager, + FragmentConstants.DFRAG_DATE_PICKER + ) + } + } + + private val isGroupNameValid: Boolean + get() { + result = true + try { + if (TextUtils.isEmpty(binding.etGroupName.editableText.toString())) { + throw RequiredFieldException( + resources.getString(R.string.group_name), + resources.getString(R.string.error_cannot_be_empty) + ) + } + if (binding.etGroupName.editableText.toString() + .trim { it <= ' ' }.length < 4 && binding.etGroupName + .editableText.toString().trim { it <= ' ' }.isNotEmpty() + ) { + throw ShortOfLengthException(resources.getString(R.string.group_name), 4) + } + if (!ValidationUtil.isNameValid(binding.etGroupName.editableText.toString())) { + throw InvalidTextInputException( + resources.getString(R.string.group_name), + resources.getString(R.string.error_should_contain_only), + InvalidTextInputException.TYPE_ALPHABETS + ) + } + } catch (e: InvalidTextInputException) { + e.notifyUserWithToast(activity) + result = false + } catch (e: ShortOfLengthException) { + e.notifyUserWithToast(activity) + result = false + } catch (e: RequiredFieldException) { + e.notifyUserWithToast(activity) + result = false + } + return result + } + + private fun showOffices(offices: List) { + officeList = offices + for (office in offices) { + office.name?.let { mListOffices.add(it) } + } + mListOffices.sort() + binding.officeListField.setSimpleItems(mListOffices.toTypedArray()) + } + + private fun showGroupCreatedSuccessfully(group: SaveResponse?) { + Toast.makeText( + activity, "Group " + MifosResponseHandler.response, + Toast.LENGTH_LONG + ).show() + requireActivity().supportFragmentManager.popBackStack() + if (PrefManager.userStatus == Constants.USER_ONLINE) { + val groupActivityIntent = Intent(activity, GroupsActivity::class.java) + groupActivityIntent.putExtra( + Constants.GROUP_ID, + group?.groupId + ) + startActivity(groupActivityIntent) + } + } + + private fun showFetchingError(s: String?) { + Toast.makeText(activity, s, Toast.LENGTH_SHORT).show() + } + + private fun showProgressbar(b: Boolean) { + showProgress(b) + } +} +*/ \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt new file mode 100644 index 00000000000..8e06702e808 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupScreen.kt @@ -0,0 +1,428 @@ +package com.mifos.mifosxdroid.online.createnewgroup + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.isSystemInDarkTheme +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 +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosDatePickerTextField +import com.mifos.core.designsystem.component.MifosOutlinedTextField +import com.mifos.core.designsystem.component.MifosSweetError +import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.theme.BluePrimary +import com.mifos.core.designsystem.theme.BluePrimaryDark +import com.mifos.core.objects.group.GroupPayload +import com.mifos.core.objects.organisation.Office +import com.mifos.core.objects.response.SaveResponse +import com.mifos.feature.note.NoteScreenPreviewProvider +import com.mifos.feature.note.NoteUiState +import com.mifos.mifosxdroid.R +import com.mifos.utils.MifosResponseHandler +import com.mifos.utils.Network +import java.text.SimpleDateFormat +import java.util.Locale + +/** + * Created by Pronay Sarker on 30/06/2024 (7:53 AM) + */ + +@Composable +fun CreateNewGroupScreen( + viewModel: CreateNewGroupViewModel = hiltViewModel(), + onGroupCreated: (group: SaveResponse?) -> Unit, +) { + val uiState by viewModel.createNewGroupUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { + viewModel.loadOffices() + } + + CreateNewGroupScreen( + uiState = uiState, + onRetry = { viewModel.loadOffices() }, + invokeGroupCreation = { groupPayload -> + viewModel.createGroup(groupPayload) + }, + onGroupCreated = onGroupCreated + ) + +} + +@Composable +fun CreateNewGroupScreen( + uiState: CreateNewGroupUiState, + onRetry: () -> Unit, + invokeGroupCreation: (GroupPayload) -> Unit, + onGroupCreated: (group: SaveResponse?) -> Unit +) { + val context = LocalContext.current + + Box( + modifier = Modifier.fillMaxSize() + ) { + when (uiState) { + is CreateNewGroupUiState.ShowFetchingError -> { + MifosSweetError( + message = uiState.message, + onclick = { onRetry.invoke() } + ) + } + + is CreateNewGroupUiState.ShowGroupCreatedSuccessfully -> { + Toast.makeText(context, "Group " + MifosResponseHandler.response, Toast.LENGTH_LONG) + .show() + onGroupCreated.invoke(uiState.saveResponse) + } + + is CreateNewGroupUiState.ShowOffices -> { + CreateNewGroupContent( + officeList = uiState.offices, + invokeGroupCreation = invokeGroupCreation + ) + } + + CreateNewGroupUiState.ShowProgressbar -> { + MifosCircularProgress() + } + } + + } + +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateNewGroupContent( + officeList: List, + invokeGroupCreation: (GroupPayload) -> Unit, +) { + var groupName by rememberSaveable { + mutableStateOf("") + } + var selectedOffice by rememberSaveable { + mutableStateOf("") + } + var externalId by rememberSaveable { + mutableStateOf("") + } + var submitDatePicker by rememberSaveable { + mutableStateOf(false) + } + var activationDatePicker by rememberSaveable { + mutableStateOf(false) + } + var isActive by rememberSaveable { + mutableStateOf(false) + } + + val context = LocalContext.current + val scrollState = rememberScrollState() + var officeId by rememberSaveable { mutableIntStateOf(0) } + var activationDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + var submittedOnDate by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + val activateDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = activationDate, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() + } + } + ) + val sumittedDatePickerState = rememberDatePickerState( + initialSelectedDateMillis = submittedOnDate, + selectableDates = object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() + } + } + ) + + if (activationDatePicker || submitDatePicker) { + DatePickerDialog( + onDismissRequest = { + submitDatePicker = false + activationDatePicker = false + }, + confirmButton = { + TextButton( + onClick = { + if (submitDatePicker) { + sumittedDatePickerState.selectedDateMillis?.let { + submittedOnDate = it + } + } else { + activateDatePickerState.selectedDateMillis?.let { + activationDate = it + } + } + submitDatePicker = false + activationDatePicker = false + } + ) { Text(stringResource(id = R.string.select_date)) } + }, + dismissButton = { + TextButton( + onClick = { + activationDatePicker = false + submitDatePicker = false + } + ) { Text(stringResource(id = R.string.cancel)) } + } + ) + { + DatePicker(state = if (submitDatePicker) sumittedDatePickerState else activateDatePickerState) + + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + style = MaterialTheme.typography.headlineSmall, + text = stringResource(id = R.string.create_new_group), + modifier = Modifier.padding(start = 16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosOutlinedTextField( + value = groupName, + onValueChange = { groupName = it }, + label = stringResource(id = R.string.name) + "*", + error = null + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosTextFieldDropdown( + value = selectedOffice, + onValueChanged = { + selectedOffice = it + }, + onOptionSelected = { index, value -> + selectedOffice = value + officeList[index].id?.let { + officeId = it + } + + }, + label = R.string.office_name_mandatory, + options = officeList.map { it.name.toString() }, + readOnly = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosDatePickerTextField( + value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + submittedOnDate + ), + label = R.string.submit_date, + openDatePicker = { + submitDatePicker = true + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MifosOutlinedTextField( + value = externalId, + onValueChange = { externalId = it }, + label = stringResource(id = R.string.external_id), + error = null + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + modifier = Modifier.padding(start = 8.dp), + colors = CheckboxDefaults.colors( + if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + checked = isActive, + onCheckedChange = { isActive = !isActive } + ) + Text(text = stringResource(id = R.string.active)) + } + + if (isActive) { + Spacer(modifier = Modifier.height(16.dp)) + + MifosDatePickerTextField( + value = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format( + activationDate + ), + label = R.string.activation_date, + openDatePicker = { + activationDatePicker = true + } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .heightIn(44.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary + ), + onClick = { + if (validateFields(groupName, selectedOffice, context)) { + if (Network.isOnline(context)) { + val activationDateInString = if (isActive) SimpleDateFormat( + "dd MMMM yyyy", + Locale.getDefault() + ).format( + activationDate + ) else null + + val submittedOnDateInString = SimpleDateFormat( + "dd MMMM yyyy", + Locale.getDefault() + ).format( + submittedOnDate + ) + + invokeGroupCreation.invoke( + GroupPayload( + name = groupName, + externalId = externalId, + active = isActive, + activationDate = activationDateInString, + submittedOnDate = submittedOnDateInString, + officeId = officeId, + dateFormat = "dd MMMM yyyy", + locale = "en" + ) + ) + } else { + Toast.makeText( + context, + context.resources.getString(R.string.error_not_connected_internet), + Toast.LENGTH_SHORT + ).show() + } + } + }) { + Text(text = stringResource(id = R.string.submit)) + } + } +} + +fun validateFields(groupName: String, officeName: String, context: Context): Boolean { + return when { + groupName.isEmpty() -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_group_name_cannot_be_empty), + Toast.LENGTH_SHORT + ).show() + return false + } + + groupName.trim().length < 4 -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_group_name_must_be_at_least_four_characters_long), + Toast.LENGTH_SHORT + ).show() + return false + } + + groupName.contains("[^a-zA-Z ]".toRegex()) -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_group_name_should_contain_only_alphabets), + Toast.LENGTH_SHORT + ).show() + return false + } + + officeName.isEmpty() -> { + Toast.makeText( + context, + context.resources.getString(R.string.error_office_not_selected), + Toast.LENGTH_SHORT + ).show() + return false + } + + else -> true + } +} + +class CreateNewGroupScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + CreateNewGroupUiState.ShowProgressbar, + CreateNewGroupUiState.ShowOffices(listOf()), + CreateNewGroupUiState.ShowFetchingError("Failed to fetch Offices"), + CreateNewGroupUiState.ShowGroupCreatedSuccessfully(saveResponse = SaveResponse()), + ) +} + +@Composable +@Preview(showSystemUi = true) +fun PreviewCreateNewGroupScreen( + @PreviewParameter(CreateNewGroupScreenPreviewProvider::class) createNewGroupUiState: CreateNewGroupUiState +) { + CreateNewGroupScreen( + uiState = createNewGroupUiState, + onRetry = {}, + invokeGroupCreation = {}, + onGroupCreated = { _ -> + + } + ) +} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt index a8a4c688a0b..ca8da2df4f8 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/createnewgroup/CreateNewGroupViewModel.kt @@ -6,7 +6,10 @@ import androidx.lifecycle.ViewModel import com.mifos.core.objects.group.GroupPayload import com.mifos.core.objects.organisation.Office import com.mifos.core.objects.response.SaveResponse +import com.mifos.mifosxdroid.online.createnewclient.CreateNewClientUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import rx.Subscriber import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -19,9 +22,8 @@ import javax.inject.Inject class CreateNewGroupViewModel @Inject constructor(private val repository: CreateNewGroupRepository) : ViewModel() { - private val _createNewGroupUiState = MutableLiveData() - - val createNewGroupUiState: LiveData + private val _createNewGroupUiState = MutableStateFlow(CreateNewGroupUiState.ShowProgressbar) + val createNewGroupUiState: StateFlow get() = _createNewGroupUiState fun loadOffices() { diff --git a/mifosng-android/src/main/res/layout/fragment_create_new_group.xml b/mifosng-android/src/main/res/layout/fragment_create_new_group.xml index 6c4ab2ed95b..7ebe3c84e6a 100755 --- a/mifosng-android/src/main/res/layout/fragment_create_new_group.xml +++ b/mifosng-android/src/main/res/layout/fragment_create_new_group.xml @@ -8,11 +8,11 @@ android:padding="10dp"> - + + + + + Loan Terms Create Center Create Group + Create New Group Create Identifier Staff Id Office Id @@ -461,6 +462,7 @@ Add Charges + Office not selected Invalid URL Invalid connection Data Invalid username length @@ -481,6 +483,9 @@ Location Unavailable, Please try again later! Client List Group List + Group name can not be empty + Group name should contain only alphabets + Group name must be at least 4 characters long Invalid Amount Entered Please Select an Office Please Select a Group @@ -821,6 +826,7 @@ Checker Inbox Select From Date Select To Date + Select Date APPROVE Do you want to approve this entry? Yes