diff --git a/feature/merchants/.gitignore b/feature/merchants/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/merchants/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/merchants/build.gradle.kts b/feature/merchants/build.gradle.kts new file mode 100644 index 00000000..5ea5696e --- /dev/null +++ b/feature/merchants/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) +} + +android { + namespace = "org.mifospay.feature.merchants" +} + +dependencies { + implementation(projects.core.data) + implementation(libs.compose.material) + + //Todo: Remove these after migration of MerchantTransferActivity + implementation("com.jakewharton:butterknife-annotations:10.2.3") + implementation("com.jakewharton:butterknife:10.2.3@aar") + implementation("com.mifos.mobile:mifos-passcode:0.3.0@aar") + implementation(project(":mifospay")) +} \ No newline at end of file diff --git a/feature/merchants/consumer-rules.pro b/feature/merchants/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/merchants/proguard-rules.pro b/feature/merchants/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/merchants/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/merchants/src/androidTest/java/org/mifospay/feature/merchants/ExampleInstrumentedTest.kt b/feature/merchants/src/androidTest/java/org/mifospay/feature/merchants/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..d6ab736a --- /dev/null +++ b/feature/merchants/src/androidTest/java/org/mifospay/feature/merchants/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.mifospay.feature.merchants + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.mifospay.feature.merchants.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/merchants/src/main/AndroidManifest.xml b/feature/merchants/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/merchants/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantScreen.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantScreen.kt new file mode 100644 index 00000000..e4daad70 --- /dev/null +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantScreen.kt @@ -0,0 +1,284 @@ +package org.mifospay.feature.merchants + +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +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.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import org.mifospay.R +import org.mifospay.common.Constants +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.theme.MifosTheme + +@Composable +fun MerchantScreen( + viewModel: MerchantViewModel = hiltViewModel() +) { + val merchantUiState by viewModel.merchantUiState.collectAsStateWithLifecycle() + val merchantsListUiState by viewModel.merchantsListUiState.collectAsStateWithLifecycle() + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + + MerchantScreen( + merchantUiState = merchantUiState, + merchantListUiState = merchantsListUiState, + updateQuery = { viewModel.updateSearchQuery(it) }, + isRefreshing = isRefreshing, + onRefresh = { viewModel.refresh() }, + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MerchantScreen( + merchantUiState: MerchantUiState, + merchantListUiState: MerchantUiState, + updateQuery: (String) -> Unit, + isRefreshing: Boolean, + onRefresh: () -> Unit, +) { + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) + Box(Modifier.pullRefresh(pullRefreshState)) { + Column(modifier = Modifier.fillMaxSize()) { + when (merchantUiState) { + MerchantUiState.Empty -> { + EmptyContentScreen( + modifier = Modifier, + title = stringResource(id = R.string.empty_no_merchants_title), + subTitle = stringResource(id = R.string.empty_no_merchants_subtitle), + iconTint = Color.Black, + iconImageVector = Icons.Rounded.Info + ) + } + + is MerchantUiState.Error -> { + EmptyContentScreen( + modifier = Modifier, + title = stringResource(id = R.string.error_oops), + subTitle = stringResource(id = R.string.unexpected_error_subtitle), + iconTint = Color.Black, + iconImageVector = Icons.Rounded.Info + ) + } + + MerchantUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.loading), + backgroundColor = Color.White + ) + } + + is MerchantUiState.ShowMerchants -> { + MerchantScreenContent( + merchantList = (merchantListUiState as MerchantUiState.ShowMerchants).merchants, + updateQuery = updateQuery + ) + } + } + } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + } +} + +@Composable +fun MerchantScreenContent( + merchantList: List, + updateQuery: (String) -> Unit +) { + val query by rememberSaveable { mutableStateOf("") } + Box(modifier = Modifier.fillMaxSize()) { + Column { + SearchBarScreen( + query = query, + onQueryChange = { q -> + updateQuery(q) + }, + onSearch = {}, + onClearQuery = { updateQuery("") } + ) + MerchantList(merchantList = merchantList) + } + } +} + +@Composable +fun MerchantList( + merchantList: List +) { + val context = LocalContext.current + val clipboardManager = LocalClipboardManager.current + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + items(merchantList.size) { index -> + MerchantsItem(savingsWithAssociations = merchantList[index], + onMerchantClicked = { + val intent = Intent(context, MerchantTransferActivity::class.java) + intent.putExtra(Constants.MERCHANT_NAME, merchantList[index].clientName) + intent.putExtra(Constants.MERCHANT_VPA, merchantList[index].externalId) + intent.putExtra(Constants.MERCHANT_ACCOUNT_NO, merchantList[index].accountNo) + context.startActivity(intent) + }, + onMerchantLongPressed = { + clipboardManager.setText(AnnotatedString(it ?: "")) + Toast.makeText(context, R.string.vpa_copy_success, Toast.LENGTH_LONG).show() + } + ) + } + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBarScreen( + query: String, + onQueryChange: (String) -> Unit, + onSearch: (String) -> Unit, + onClearQuery: () -> Unit +) { + SearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 16.dp), + query = query, + onQueryChange = onQueryChange, + onSearch = onSearch, + active = false, + onActiveChange = { }, + placeholder = { + Text(text = stringResource(R.string.search)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = stringResource(R.string.search) + ) + }, + trailingIcon = { + IconButton( + onClick = onClearQuery + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.close) + ) + } + } + ) {} +} + +@Preview(showBackground = true) +@Composable +private fun MerchantLoadingPreview() { + MifosTheme { + MerchantScreen(merchantUiState = MerchantUiState.Loading, + merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList), + updateQuery = {}, + false, {} + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun MerchantListPreview() { + MifosTheme { + MerchantScreen( + merchantUiState = MerchantUiState.ShowMerchants(sampleMerchantList), + merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList), + updateQuery = {}, false, {} + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun MerchantErrorPreview() { + MifosTheme { + MerchantScreen( + merchantUiState = MerchantUiState.Error("Error Screen"), + merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList), + updateQuery = {}, true, {} + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun MerchantEmptyPreview() { + MifosTheme { + MerchantScreen( + merchantUiState = MerchantUiState.Empty, + merchantListUiState = MerchantUiState.ShowMerchants(sampleMerchantList), + updateQuery = {}, false, {} + ) + } +} + + +val sampleMerchantList = List(10) { + SavingsWithAssociations( + id = 1L, + accountNo = "123456789", + depositType = null, + externalId = "EXT987654", + clientId = 101, + clientName = "Alice Bob", + savingsProductId = 2001, + savingsProductName = "Premium Savings Account", + fieldOfficerId = 501, + status = null, + timeline = null, + currency = null, + nominalAnnualInterestRate = 3.5, + minRequiredOpeningBalance = 500.0, + lockinPeriodFrequency = 12.0, + withdrawalFeeForTransfers = true, + allowOverdraft = false, + enforceMinRequiredBalance = false, + withHoldTax = true, + lastActiveTransactionDate = listOf(2024, 3, 24), + dormancyTrackingActive = true, + summary = null, + transactions = listOf() + ) +} \ No newline at end of file diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferActivity.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferActivity.kt new file mode 100644 index 00000000..233c99a1 --- /dev/null +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferActivity.kt @@ -0,0 +1,198 @@ +package org.mifospay.feature.merchants + +import android.os.Bundle +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.textfield.TextInputEditText +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import android.view.View +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import dagger.hilt.android.AndroidEntryPoint +import com.mifospay.core.model.domain.Transaction +import org.mifospay.MifosPayApp +import org.mifospay.R +import org.mifospay.base.BaseActivity +import org.mifospay.common.ui.MakeTransferFragment +import org.mifospay.history.ui.adapter.SpecificTransactionsAdapter +import org.mifospay.merchants.presenter.MerchantTransferPresenter +import org.mifospay.common.Constants +import org.mifospay.home.BaseHomeContract +import org.mifospay.utils.TextDrawable +import org.mifospay.utils.Toaster +import javax.inject.Inject + +/** + * Created by Shivansh Tiwari on 06/07/19. + */ +@AndroidEntryPoint +class MerchantTransferActivity : BaseActivity(), BaseHomeContract.MerchantTransferView { + private var mBottomSheetBehavior: BottomSheetBehavior<*>? = null + + @JvmField + @BindView(R.id.nsv_merchant_bottom_sheet_dialog) + var vMerchantBottomSheetDialog: View? = null + + @JvmField + @BindView(R.id.iv_merchant_image) + var ivMerchantImage: ImageView? = null + + @JvmField + @BindView(R.id.tv_pay_to_name) + var tvMerchantName: TextView? = null + + @JvmField + @BindView(R.id.tv_pay_to_vpa) + var tvMerchantVPA: TextView? = null + + @JvmField + @BindView(R.id.et_merchant_amount) + var etAmount: TextInputEditText? = null + + @JvmField + @BindView(R.id.btn_submit) + var btnSubmit: Button? = null + + @JvmField + @BindView(R.id.rv_merchant_history) + var rvMerchantHistory: RecyclerView? = null + + @JvmField + @BindView(R.id.inc_empty_transactions_state_view) + var vEmptyState: View? = null + + @JvmField + @BindView(R.id.iv_empty_no_transaction_history) + var ivTransactionsStateIcon: ImageView? = null + + @JvmField + @BindView(R.id.tv_empty_no_transaction_history_title) + var tvTransactionsStateTitle: TextView? = null + + @JvmField + @BindView(R.id.tv_empty_no_transaction_history_subtitle) + var tvTransactionsStateSubtitle: TextView? = null + + @JvmField + @Inject + var mPresenter: MerchantTransferPresenter? = null + private var mTransferPresenter: BaseHomeContract.MerchantTransferPresenter? = null + private var merchantAccountNumber: String? = null + + @JvmField + @Inject + var mMerchantHistoryAdapter: SpecificTransactionsAdapter? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_merchant_transaction) + ButterKnife.bind(this) + setToolbarTitle("Merchant Transaction") + showColoredBackButton(R.drawable.ic_arrow_back_black_24dp) + setupUI() + mPresenter?.attachView(this) + mPresenter?.fetchMerchantTransfers(merchantAccountNumber) + } + + private fun setupUI() { + setupBottomSheet() + merchantAccountNumber = intent.getStringExtra(Constants.MERCHANT_ACCOUNT_NO) + tvMerchantName?.text = + intent.getStringExtra(Constants.MERCHANT_NAME) + tvMerchantVPA?.text = intent.getStringExtra(Constants.MERCHANT_VPA) + val drawable = intent.getStringExtra(Constants.MERCHANT_NAME) + ?.substring(0, 1)?.let { + TextDrawable.builder().beginConfig() + .width(resources.getDimension(R.dimen.user_profile_image_size).toInt()) + .height(resources.getDimension(R.dimen.user_profile_image_size).toInt()) + .endConfig().buildRound( + it, R.color.colorPrimary + ) + } + ivMerchantImage?.setImageDrawable(drawable) + showTransactionFetching() + setUpRecycleView() + } + + private fun setUpRecycleView() { + mMerchantHistoryAdapter?.setContext(this) + rvMerchantHistory?.layoutManager = + LinearLayoutManager(MifosPayApp.context) + rvMerchantHistory?.adapter = mMerchantHistoryAdapter + } + + override fun setPresenter(presenter: BaseHomeContract.MerchantTransferPresenter?) { + mTransferPresenter = presenter + } + + private fun setupBottomSheet() { + mBottomSheetBehavior = BottomSheetBehavior.from(vMerchantBottomSheetDialog!!) + mBottomSheetBehavior?.setBottomSheetCallback(object : BottomSheetCallback() { + override fun onStateChanged(view: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_COLLAPSED -> {} + else -> {} + } + } + + override fun onSlide(view: View, v: Float) {} + }) + } + + @OnClick(R.id.btn_submit) + fun makeTransaction() { + val externalId = tvMerchantVPA?.text.toString().trim { it <= ' ' } + val amount = etAmount?.text.toString().trim { it <= ' ' } + if (amount.isEmpty()) { + showToast(Constants.PLEASE_ENTER_ALL_THE_FIELDS) + return + } else if (amount.toDouble() <= 0) { + showToast(Constants.PLEASE_ENTER_VALID_AMOUNT) + return + } + mTransferPresenter?.checkBalanceAvailability(externalId, amount.toDouble()) + } + + override fun onBackPressed() { + if (mBottomSheetBehavior?.state != BottomSheetBehavior.STATE_COLLAPSED) { + mBottomSheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + return + } + super.onBackPressed() + } + + override fun showToast(message: String?) { + Toaster.showToast(MifosPayApp.context, message) + } + + override fun showPaymentDetails(externalId: String?, amount: Double) { + val fragment = MakeTransferFragment.newInstance(externalId, amount) + fragment.show(supportFragmentManager, "tag") + } + + override fun showTransactionFetching() { + rvMerchantHistory?.visibility = View.GONE + tvTransactionsStateTitle?.text = resources.getString(R.string.fetching) + tvTransactionsStateSubtitle?.visibility = View.GONE + ivTransactionsStateIcon?.visibility = View.GONE + } + + override fun showTransactions(transactions: List?) { + vEmptyState?.visibility = View.GONE + rvMerchantHistory?.visibility = View.VISIBLE + mMerchantHistoryAdapter?.setData(transactions as List) + } + + override fun showSpecificView(drawable: Int, title: Int, subtitle: Int) { + rvMerchantHistory?.visibility = View.GONE + tvTransactionsStateSubtitle?.visibility = View.VISIBLE + ivTransactionsStateIcon?.visibility = View.VISIBLE + tvTransactionsStateTitle?.setText(title) + tvTransactionsStateSubtitle?.setText(subtitle) + ivTransactionsStateIcon?.setImageDrawable(resources.getDrawable(drawable)) + } +} \ No newline at end of file diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt new file mode 100644 index 00000000..5059b98d --- /dev/null +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt @@ -0,0 +1,144 @@ +package org.mifospay.feature.merchants + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.TaskLooper +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseFactory +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchMerchants +import org.mifospay.core.data.domain.usecase.client.FetchClientDetails +import org.mifospay.core.data.util.Constants +import javax.inject.Inject + +@HiltViewModel +class MerchantViewModel @Inject constructor( + private val mUseCaseHandler: UseCaseHandler, + private val mFetchMerchantsUseCase: FetchMerchants, + private val mUseCaseFactory: UseCaseFactory +) : ViewModel() { + + @Inject + lateinit var mTaskLooper: TaskLooper + + private val _searchQuery = MutableStateFlow("") + private val searchQuery: StateFlow = _searchQuery.asStateFlow() + + private val _merchantUiState = MutableStateFlow(MerchantUiState.Loading) + val merchantUiState: StateFlow = _merchantUiState + + init { + fetchMerchants() + } + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + fetchMerchants() + _isRefreshing.emit(false) + } + } + + + val merchantsListUiState: StateFlow = searchQuery + .map { q -> + when (_merchantUiState.value) { + is MerchantUiState.ShowMerchants -> { + val merchantList = + (merchantUiState.value as MerchantUiState.ShowMerchants).merchants + val filterCards = merchantList.filter { + it.externalId.lowercase().contains(q.lowercase()) + it.savingsProductName?.lowercase()?.contains(q.lowercase()) + it.accountNo?.lowercase()?.contains(q.lowercase()) + it.clientName.lowercase().contains(q.lowercase()) + } + MerchantUiState.ShowMerchants(filterCards) + } + + else -> MerchantUiState.ShowMerchants(arrayListOf()) + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = MerchantUiState.ShowMerchants(arrayListOf()) + ) + + fun updateSearchQuery(query: String) { + _searchQuery.update { query } + } + + + private fun fetchMerchants() { + _merchantUiState.value = MerchantUiState.Loading + mUseCaseHandler.execute(mFetchMerchantsUseCase, + FetchMerchants.RequestValues(), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchMerchants.ResponseValue) { + retrieveMerchantsData(response.savingsWithAssociationsList) + } + + override fun onError(message: String) { + _merchantUiState.value = MerchantUiState.Error(message) + } + }) + } + + fun retrieveMerchantsData( + savingsWithAssociationsList: List + ) { + for (i in savingsWithAssociationsList.indices) { + mTaskLooper.addTask( + useCase = mUseCaseFactory.getUseCase(Constants.FETCH_CLIENT_DETAILS_USE_CASE) + as UseCase, + values = FetchClientDetails.RequestValues( + savingsWithAssociationsList[i].clientId.toLong() + ), + taskData = TaskLooper.TaskData("Client data", i) + ) + } + mTaskLooper.listen(object : TaskLooper.Listener { + override fun onTaskSuccess( + taskData: TaskLooper.TaskData, + response: R + ) { + val responseValue = response as FetchClientDetails.ResponseValue + savingsWithAssociationsList[taskData.taskId].externalId = + responseValue.client.externalId + } + + override fun onComplete() { + if (savingsWithAssociationsList.isEmpty()) { + _merchantUiState.value = MerchantUiState.Empty + } else { + _merchantUiState.value = + MerchantUiState.ShowMerchants(savingsWithAssociationsList) + } + } + + override fun onFailure(message: String?) { + _merchantUiState.value = MerchantUiState.Error(message.toString()) + } + }) + } +} + +sealed class MerchantUiState { + data object Loading : MerchantUiState() + data object Empty : MerchantUiState() + data class Error(val message: String) : MerchantUiState() + data class ShowMerchants(val merchants: List) : MerchantUiState() +} \ No newline at end of file diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantsItem.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantsItem.kt new file mode 100644 index 00000000..776ead9b --- /dev/null +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantsItem.kt @@ -0,0 +1,87 @@ +package org.mifospay.feature.merchants + +import androidx.compose.foundation.gestures.detectTapGestures +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.size +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import org.mifospay.R +import org.mifospay.core.designsystem.component.MifosCard +import org.mifospay.core.designsystem.theme.mifosText +import org.mifospay.core.designsystem.theme.styleMedium16sp + +@Composable +fun MerchantsItem( + savingsWithAssociations: SavingsWithAssociations, + onMerchantClicked: () -> Unit, + onMerchantLongPressed: (String?) -> Unit +) { + MifosCard( + modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { + onMerchantLongPressed(savingsWithAssociations.externalId) + } + ) + }, + onClick = { onMerchantClicked.invoke() }, + colors = CardDefaults.cardColors(Color.White) + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_bank), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 16.dp, end = 16.dp) + .size(39.dp) + ) + + Column { + Text( + text = savingsWithAssociations.clientName, + color = mifosText, + ) + Text( + text = savingsWithAssociations.externalId, + modifier = Modifier.padding(top = 4.dp), + style = styleMedium16sp.copy(mifosText) + ) + } + } + } + HorizontalDivider( + thickness = 1.dp, + modifier = Modifier.padding(8.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun AccountsItemPreview() { + MerchantsItem( + savingsWithAssociations = SavingsWithAssociations(), + onMerchantClicked = {}, + onMerchantLongPressed = {} + ) +} \ No newline at end of file diff --git a/feature/merchants/src/test/java/org/mifospay/feature/merchants/ExampleUnitTest.kt b/feature/merchants/src/test/java/org/mifospay/feature/merchants/ExampleUnitTest.kt new file mode 100644 index 00000000..3a6a0cc5 --- /dev/null +++ b/feature/merchants/src/test/java/org/mifospay/feature/merchants/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package org.mifospay.feature.merchants + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b8e5231a..9e77d79d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -48,6 +48,7 @@ include(":feature:invoices") include(":feature:invoices") include(":feature:settings") include(":feature:profile") +include(":feature:merchants") include(":feature:accounts") include(":feature:standing-instruction") include(":feature:payments")