Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/ui/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,9 @@
<string name="client_share_accounts_share_product">Share Product</string>
<string name="client_share_accounts_pending_for_approval_shares">Pending For Approval Shares</string>
<string name="client_share_accounts_approved_shares">Approved Shares</string>





</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ object TextFieldsValidator {
}

fun doubleNumberValidator(input: String): StringResource? {
val trimmedInput = input.trim()
return when {
input.isBlank() -> Res.string.error_field_empty
input.count { it == '.' } > 1 -> Res.string.error_invalid_number
input.any { !it.isDigit() && it != '.' } -> Res.string.error_digits_only
input.toDoubleOrNull() == null -> Res.string.error_invalid_number
input == "0" || input == "0.0" || input.toDoubleOrNull() == 0.0 -> Res.string.error_number_zero
trimmedInput.isBlank() -> Res.string.error_field_empty
trimmedInput.count { it == '.' } > 1 -> Res.string.error_invalid_number
trimmedInput.any { !it.isDigit() && it != '.' } -> Res.string.error_digits_only
trimmedInput.toDoubleOrNull() == null -> Res.string.error_invalid_number
trimmedInput == "0" || trimmedInput == "0.0" || trimmedInput.toDoubleOrNull() == 0.0 -> Res.string.error_number_zero
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,11 @@
<string name="feature_recurring_deposit_next_button">Next Button</string>
<string name="feature_recurring_deposit_interest_page">Interest Page</string>
<string name="feature_recurring_deposit_charges_page">Charges Page</string>
<string name="recurring_step_charges_edit_charge">Edit Charge</string>
<string name="recurring_step_charges_add_new">Add New</string>
<string name="recurring_step_charges_add">Add</string>
<string name="recurring_step_charges_view">View</string>
<string name="recurring_step_charges_active">Active</string>
<string name="recurring_step_charges_submit">Submit</string>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,65 @@
package com.mifos.feature.recurringDeposit.newRecurringDepositAccount

import androidclient.feature.recurringdeposit.generated.resources.Res
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_back
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_cancel
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_create_recurring_deposit_account
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_next
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_next_button
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_step_charges
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_step_details
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_step_interest
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_step_settings
import androidclient.feature.recurringdeposit.generated.resources.feature_recurring_deposit_step_terms
import androidclient.feature.recurringdeposit.generated.resources.recurring_step_charges_add
import androidclient.feature.recurringdeposit.generated.resources.recurring_step_charges_add_new
import androidclient.feature.recurringdeposit.generated.resources.recurring_step_charges_edit_charge
import androidclient.feature.recurringdeposit.generated.resources.recurring_step_charges_view
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.mifos.core.common.utils.DateHelper
import com.mifos.core.designsystem.component.MifosBottomSheet
import com.mifos.core.designsystem.component.MifosScaffold
import com.mifos.core.designsystem.theme.DesignToken
import com.mifos.core.designsystem.theme.MifosTypography
import com.mifos.core.ui.components.Actions
import com.mifos.core.ui.components.AddChargeBottomSheet
import com.mifos.core.ui.components.MifosActionsChargeListingComponent
import com.mifos.core.ui.components.MifosBreadcrumbNavBar
import com.mifos.core.ui.components.MifosErrorComponent
import com.mifos.core.ui.components.MifosProgressIndicator
import com.mifos.core.ui.components.MifosStepper
import com.mifos.core.ui.components.MifosTwoButtonRow
import com.mifos.core.ui.components.Step
import com.mifos.core.ui.util.EventsEffect
import com.mifos.core.ui.util.TextFieldsValidator.doubleNumberValidator
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.RecurringAccountAction.NavigateToStep
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.pages.ChargesPage
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.pages.DetailsPage
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.pages.InterestPage
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.pages.SettingPage
import com.mifos.feature.recurringDeposit.newRecurringDepositAccount.pages.TermsPage
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

@Composable
internal fun RecurringAccountScreen(
Expand All @@ -55,14 +85,23 @@ internal fun RecurringAccountScreen(
RecurringAccountEvent.NavigateBack -> onNavigateBack()
RecurringAccountEvent.Finish -> onFinish()
}

}


RecurringAccountScaffold(
navController = navController,
modifier = modifier,
state = state,
onAction = { viewModel.trySendAction(it) },
)
val snackbarHostState = remember { SnackbarHostState() }
NewRecurringAccountDialog(
state = state,
onAction = {viewModel.trySendAction(it)},
snackbarHostState = snackbarHostState

)
Comment on lines +98 to +104
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

snackbarHostState is not connected to a SnackbarHost - snackbars won't display.

The SnackbarHostState is created and passed to NewRecurringAccountDialog, but it's never passed to a MifosScaffold or SnackbarHost. The snackbar messages in SuccessResponseStatus (lines 206-208) will not be visible to users.

Either pass snackbarHostState to RecurringAccountScaffold to be used in MifosScaffold, or use a different mechanism for displaying success feedback.

🤖 Prompt for AI Agents
In
feature/recurringDeposit/src/commonMain/kotlin/com/mifos/feature/recurringDeposit/newRecurringDepositAccount/RecurringAccountScreen.kt
around lines 98-104, the created snackbarHostState is passed into
NewRecurringAccountDialog but not wired to any Scaffold/SnackbarHost so
snackbars will never appear; update the call site to pass snackbarHostState into
RecurringAccountScaffold (or directly into the MifosScaffold) and ensure that
scaffold uses it via SnackbarHost(hostState = snackbarHostState) (or the
scaffold's snackbarHost parameter) so SuccessResponseStatus snackbars (lines
~206-208) are displayed to the user.

}

@Composable
Expand Down Expand Up @@ -97,7 +136,8 @@ private fun RecurringAccountScaffold(
},
Step(name = stringResource(Res.string.feature_recurring_deposit_step_charges)) {
ChargesPage(
onNext = { onAction(RecurringAccountAction.OnNextPress) },
state = state,
onAction = onAction,
)
},
)
Expand Down Expand Up @@ -142,3 +182,205 @@ private fun RecurringAccountScaffold(
}
}
}
@Composable
private fun NewRecurringAccountDialog(
state: RecurringAccountState,
onAction: (RecurringAccountAction) -> Unit,
snackbarHostState: SnackbarHostState,
) {
when (state.dialogState) {
is RecurringAccountState.DialogState.AddNewCharge -> AddNewChargeDialog(
isEdit = state.dialogState.edit,
state = state,
onAction = onAction,
index = state.dialogState.index,
)

is RecurringAccountState.DialogState.showCharges -> ShowChargesDialog(
state = state,
onAction = onAction,
)
Comment on lines +199 to +202
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Class name showCharges violates Kotlin naming convention.

Sealed class/interface members should use PascalCase. Rename showCharges to ShowCharges for consistency with AddNewCharge and SuccessResponseStatus.

-        is RecurringAccountState.DialogState.showCharges -> ShowChargesDialog(
+        is RecurringAccountState.DialogState.ShowCharges -> ShowChargesDialog(

This also requires updating the sealed class definition in the state file.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
feature/recurringDeposit/src/commonMain/kotlin/com/mifos/feature/recurringDeposit/newRecurringDepositAccount/RecurringAccountScreen.kt
around lines 199–202 the sealed-class member name `showCharges` violates Kotlin
PascalCase naming; rename the member to `ShowCharges` (both usages and
constructor/calls) and update the sealed class definition in the corresponding
state file to use `ShowCharges` as well so all references compile and match the
existing PascalCase pattern like `AddNewCharge` and `SuccessResponseStatus`.


is RecurringAccountState.DialogState.SuccessResponseStatus -> {
LaunchedEffect(state.launchEffectKey) {
snackbarHostState.showSnackbar(
message = state.dialogState.msg,
)

if (state.dialogState.successStatus) {
delay(1000)
onAction(RecurringAccountAction.Finish)
}
}
}

null -> Unit
}
}
@OptIn(ExperimentalTime::class, ExperimentalTime::class)
@Composable
private fun AddNewChargeDialog(
isEdit: Boolean,
index: Int = -1,
state: RecurringAccountState,
onAction: (RecurringAccountAction) -> Unit,
) {
var isAmountDirty by rememberSaveable { mutableStateOf(false) }

LaunchedEffect(state.chargeAmount, isAmountDirty) {
if (isAmountDirty) {
if (state.chargeAmount.isNotEmpty()) {
val amountError = doubleNumberValidator(state.chargeAmount)
onAction(RecurringAccountAction.RecurringAccountChargesAction.OnChargesAmountChangeError(amountError))
} else {
onAction(RecurringAccountAction.RecurringAccountChargesAction.OnChargesAmountChangeError(null))
}
}
}
fun isSelectableDate(utcTimeMillis: Long): Boolean {
return utcTimeMillis >= Clock.System.now().toEpochMilliseconds().minus(86_400_000L)
}
AddChargeBottomSheet(
title = if (isEdit) {
stringResource(Res.string.recurring_step_charges_edit_charge)
} else {
stringResource(Res.string.recurring_step_charges_add_new) + " " + stringResource(Res.string.feature_recurring_deposit_step_charges)
},
confirmText = if (isEdit) {
stringResource(Res.string.recurring_step_charges_edit_charge)
} else {
stringResource(Res.string.recurring_step_charges_add)
},
dismissText = stringResource(Res.string.feature_recurring_deposit_cancel),
showDatePicker = state.showChargesDatePick,
selectedChargeName = if (state.chooseChargeIndex == -1) {
""
} else {
state.template.chargeOptions?.getOrNull(state.chooseChargeIndex)?.name ?: ""
},
selectedDate = state.chargeDate,
chargeAmount = state.chargeAmount,
chargeType = if (state.chooseChargeIndex == -1) {
""
} else {
state.template.chargeOptions?.get(state.chooseChargeIndex)?.chargeCalculationType?.value
?: ""
},
Comment on lines +263 to +268
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential IndexOutOfBoundsException - use getOrNull consistently.

Line 266 uses .get(state.chooseChargeIndex) which throws if the index is out of bounds, while lines 259 and 272 correctly use .getOrNull(). Use getOrNull consistently to avoid crashes.

         chargeType = if (state.chooseChargeIndex == -1) {
             ""
         } else {
-            state.template.chargeOptions?.get(state.chooseChargeIndex)?.chargeCalculationType?.value
+            state.template.chargeOptions?.getOrNull(state.chooseChargeIndex)?.chargeCalculationType?.value
                 ?: ""
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
chargeType = if (state.chooseChargeIndex == -1) {
""
} else {
state.template.chargeOptions?.get(state.chooseChargeIndex)?.chargeCalculationType?.value
?: ""
},
chargeType = if (state.chooseChargeIndex == -1) {
""
} else {
state.template.chargeOptions?.getOrNull(state.chooseChargeIndex)?.chargeCalculationType?.value
?: ""
},
🤖 Prompt for AI Agents
In
feature/recurringDeposit/src/commonMain/kotlin/com/mifos/feature/recurringDeposit/newRecurringDepositAccount/RecurringAccountScreen.kt
around lines 263 to 268, the code uses .get(state.chooseChargeIndex) which can
throw IndexOutOfBoundsException; change that call to
.getOrNull(state.chooseChargeIndex) and keep the safe-chain access so the
expression becomes
.getOrNull(state.chooseChargeIndex)?.chargeCalculationType?.value ?: "" to match
the surrounding usage and avoid crashes.

chargeCollectedOn = if (state.chooseChargeIndex == -1) {
""
} else {
state.template.chargeOptions?.getOrNull(state.chooseChargeIndex)?.chargeTimeType?.value ?: ""
},
chargeOptions = state.template.chargeOptions?.map { it.name ?: "" } ?: emptyList(),
onConfirm = {
isAmountDirty = true
if (state.chargeAmount.isNotEmpty() && state.chargeAmountError == null) {
if (isEdit) {
onAction(RecurringAccountAction.RecurringAccountChargesAction.EditCharge(index))
} else {
onAction(RecurringAccountAction.RecurringAccountChargesAction.AddChargeToList)
}
}
},
onDismiss = { onAction(RecurringAccountAction.RecurringAccountChargesAction.DismissDialog) },
onChargeSelected = { index, _ ->
onAction(RecurringAccountAction.RecurringAccountChargesAction.OnChooseChargeIndex(index))
},
onDatePick = { show ->
onAction(RecurringAccountAction.RecurringAccountChargesAction.OnChargesDatePick(show))
},
onDateChange = { newDate ->
if (isSelectableDate(newDate)) {
onAction(RecurringAccountAction.RecurringAccountChargesAction.OnChargesDateChange(
DateHelper.getDateAsStringFromLong(newDate)))
}
},
amountError = if (state.chargeAmountError != null) stringResource(state.chargeAmountError) else null,
onAmountChange = { amount ->
isAmountDirty = true
onAction(RecurringAccountAction.RecurringAccountChargesAction.OnChargesAmountChange(amount))
},
)
}


@Composable
private fun ShowChargesDialog(
state: RecurringAccountState,
onAction: (RecurringAccountAction) -> Unit,
) {
var expandedIndex: Int? by rememberSaveable { mutableStateOf(-1) }
MifosBottomSheet(
onDismiss = {
onAction(RecurringAccountAction.RecurringAccountChargesAction.DismissDialog)
},
content = {
LazyColumn(
modifier = Modifier.fillMaxWidth().padding(DesignToken.padding.large),
verticalArrangement = Arrangement.spacedBy(DesignToken.padding.largeIncreased),
) {
item {
Text(
text = stringResource(Res.string.recurring_step_charges_view) + " " + stringResource(
Res.string.feature_recurring_deposit_step_charges
),
style = MifosTypography.titleMediumEmphasized,
)
}
itemsIndexed(items = state.addedCharges) { index, it ->
MifosActionsChargeListingComponent(
chargeTitle = it.name.toString(),
type = it.type.toString(),
date = it.date,
collectedOn = it.collectedOn,
amount = it.amount.toString(),
onActionClicked = { action ->
when (action) {
is Actions.Delete -> {
onAction(
RecurringAccountAction.RecurringAccountChargesAction.DeleteChargeFromSelectedCharges(
index
)
)
}

is Actions.Edit -> {
onAction(
RecurringAccountAction.RecurringAccountChargesAction.EditChargeDialog(
index
)
)
}

else -> {}
}
},

isExpanded = expandedIndex == it.id,
onExpandToggle = {
expandedIndex = if (expandedIndex == it.id) -1 else it.id
},
)


}
item {
MifosTwoButtonRow(
firstBtnText = stringResource(Res.string.feature_recurring_deposit_back),
secondBtnText = stringResource(Res.string.recurring_step_charges_add_new),
onFirstBtnClick = {
onAction(RecurringAccountAction.RecurringAccountChargesAction.DismissDialog)
},
onSecondBtnClick = {
onAction(RecurringAccountAction.RecurringAccountChargesAction.ShowAddChargeDialog)
},
)
}


}

}
)

}
Loading
Loading