-
Couldn't load subscription status.
- Fork 653
Client Add Charges #2518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Client Add Charges #2518
Conversation
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
WalkthroughThis pull request refactors the client charges UI and state management architecture. It replaces the traditional ViewModel pattern with a BaseViewModel-based approach, removes the standalone ChargeDialogScreen component, introduces unified ClientChargesState with centralized action handling, implements route-based navigation for charges, and updates all platform-specific implementations. Additionally, new string resources are added for charge workflow messaging. Changes
Sequence DiagramsequenceDiagram
participant User
participant Screen as ClientChargesScreen
participant ViewModel as ClientChargesViewModel
participant Repo as Repository
participant State as State Machine
User->>Screen: Navigate to Charges (clientId)
Screen->>ViewModel: Create with clientId
ViewModel->>State: Initialize state
ViewModel->>Repo: loadCharges()
Repo-->>ViewModel: chargesFlow (paging data)
ViewModel->>State: Update with chargesFlow
Note over Screen: Display charges in BottomSheet
User->>Screen: Tap "Add Charge"
Screen->>ViewModel: onAction(LoadChargeTemplate)
ViewModel->>Repo: getChargeTemplate()
Repo-->>ViewModel: ChargeTemplate
ViewModel->>State: Update DialogState to AllChargesV2
Note over Screen: Show charge creation dialog
User->>Screen: Fill form & submit
Screen->>ViewModel: onAction(CreateCharge(payload))
ViewModel->>Repo: createChargesUseCase(payload)
Repo-->>ViewModel: ResultStatus (Success/Error)
ViewModel->>State: Update DialogState to ShowStatusDialog
alt Success
ViewModel->>Repo: refreshChargesList()
ViewModel->>State: Close dialog, update charges
else Error
ViewModel->>State: Show error in dialog
end
Note over Screen: Reflect updated charges list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes This PR requires thorough review due to significant architectural refactoring. Key concerns include: (1) BaseViewModel integration and state-machine correctness across multi-step flows (template loading → charge creation → list refresh), (2) action handler completeness and event threading via viewModelScope, (3) navigation consistency across platform implementations, (4) removal of ChargeDialogScreen and integration of its logic into ViewModel, (5) BottomSheet presentation changes on Android platform, and (6) multiplatform expect/actual consistency for ClientChargeContent callback signature.
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (15)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesViewModel.kt (3)
288-296: Avoid Any? for chargesFlow; introduce a common typealias or UI model.Using Any? erodes type safety and invites runtime casts. Define a platform‑agnostic type for the paging flow in commonMain and keep the state strongly typed.
Example approach (sketch):
// commonMain expect class ChargeItem typealias ChargesPagingFlow = Flow<PagingData<ChargeItem>> data class ClientChargesState( /* ... */ - val chargesFlow: Any? = null, + val chargesFlow: ChargesPagingFlow? = null, /* ... */ )Then provide actuals:
- Android/desktop: actual typealias ChargeItem = com.mifos.room.entities.client.ChargesEntity
- Or map to a shared ChargeUiModel.
Also applies to: 190-231
295-296: Default dialog state as Loading may show a spinner on first render; add an Idle state.Unless UI gates on showAddCharges, users might see a loading indicator prematurely. Prefer an explicit Idle to represent “no dialog”.
- sealed class ChargeDialogState { + sealed class ChargeDialogState { + data object Idle : ChargeDialogState() data object Loading : ChargeDialogState() data class Error(val message: StringResource) : ChargeDialogState() data class AllChargesV2( val chargeTemplate: ChargeTemplate, val selectedChargeName: String, val selectedChargeId: Int, ) : ChargeDialogState() }And in state default:
- val chargeDialogState: ClientChargesViewModel.ChargeDialogState = ClientChargesViewModel.ChargeDialogState.Loading, + val chargeDialogState: ClientChargesViewModel.ChargeDialogState = ClientChargesViewModel.ChargeDialogState.Idle,Also applies to: 276-285
338-352: Action set overlaps (AddCharge vs OnShowAddCharge). Consolidate to one.Two actions trigger the same UI state (showAddCharges true, showCharges false). This increases surface without benefit.
- Drop OnShowAddCharge and use AddCharge everywhere.
- Or keep one and mark the other internal/private to the UI layer.
Also applies to: 82-121
feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.desktop.kt (1)
22-35: Unused onAction parameter on desktop; suppress to keep builds clean.The actual implementation doesn’t use onAction. Suppress the warning locally.
@Composable actual fun ClientChargeContent( pagingFlow: Flow<PagingData<ChargesEntity>>, - onAction: (ClientChargesAction) -> Unit, + onAction: (ClientChargesAction) -> Unit, ) { + @Suppress("UNUSED_PARAMETER") Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Text( text = "Not available in desktop yet", ) } }feature/client/src/nativeMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.native.kt (1)
25-26: UnusedonActionparameter in placeholder implementationSuppress or use an underscore name to avoid warnings until native is implemented.
Apply one of:
-actual fun ClientChargeContent( +@Suppress("UNUSED_PARAMETER") +actual fun ClientChargeContent( pagingFlow: Flow<PagingData<ChargesEntity>>, onAction: (ClientChargesAction) -> Unit, )or
- onAction: (ClientChargesAction) -> Unit, + _onAction: (ClientChargesAction) -> Unit,feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesRoute.kt (2)
17-21: Avoid sentinel default for critical route arg
clientIddefaulting to -1 hides mistakes. Prefer enforcing a valid id.-@Serializable -data class ClientChargesRoute( - val clientId: Int = -1, -) +@Serializable +data class ClientChargesRoute( + val clientId: Int, +)
34-42: Guard navigation with argument validationFail fast if an invalid id is passed.
fun NavController.navigateToClientChargesRoute( clientId: Int, ) { - this.navigate( + require(clientId > 0) { "clientId must be > 0" } + navigate( ClientChargesRoute( clientId = clientId, ), ) }Verified: ClientChargesViewModel reads clientId from savedStateHandle.toRoute().clientId.
feature/client/src/commonMain/composeResources/values/strings.xml (1)
99-102: Copy polish: clearer success/failure messagingTighten phrasing and keep UI voice consistent.
- <string name="feature_client_created_charge_success_title">Charge Added Successfully</string> - <string name="feature_client_created_charge_failure_title">Charge Creation Failed</string> - <string name="feature_client_created_charge_success_message">Client Charge created successfully. Please continue to go back to Profile Details</string> + <string name="feature_client_created_charge_success_title">Charge added successfully</string> + <string name="feature_client_created_charge_failure_title">Charge creation failed</string> + <string name="feature_client_created_charge_success_message">Client charge created. Tap Continue to return to Profile Details.</string>feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.android.kt (2)
102-106: Use generic action strings, not identifier-specific onesReplace identifier-specific strings with generic actions.
-Text( - text = stringResource(Res.string.client_identifier_btn_view), +Text( + text = stringResource(Res.string.action_view),
96-117: Don’t materialize the entire paging list; render with LazyColumn inside the sheetCreating
List(itemCount) { chargesPagingList[index] }defeats paging and can cause jank.- val chargesList = List(chargesPagingList.itemCount){index -> - chargesPagingList[index] - }.filterNotNull() - MifosBottomSheet(onDismiss = { + MifosBottomSheet(onDismiss = { onAction(ClientChargesAction.CloseShowChargesDialog) }, content = { Column( modifier = Modifier .fillMaxWidth() .padding(DesignToken.padding.large), verticalArrangement = Arrangement.spacedBy(DesignToken.padding.largeIncreased), ) { Text( - text = stringResource(Res.string.action_view), + text = stringResource(Res.string.action_view), style = MifosTypography.titleMediumEmphasized, ) - chargesList.forEach { it -> - MifosActionsChargeListingComponent( - chargeTitle = it.name.toString(), - type = it.chargeCalculationType?.value.toString(), - date = it.formattedDueDate, - collectedOn = it.formattedCollectedOnDate ?: it.formattedDueDate, - amount = it.amount.toString(), - onActionClicked = {}, - isExpandable = true - ) - } + LazyColumn(verticalArrangement = Arrangement.spacedBy(DesignToken.padding.medium)) { + items( + count = chargesPagingList.itemCount, + key = { index -> chargesPagingList[index]?.id ?: "index_$index" }, + ) { index -> + chargesPagingList[index]?.let { charge -> + MifosActionsChargeListingComponent( + chargeTitle = charge.name.orEmpty(), + type = charge.chargeCalculationType?.value.orEmpty(), + date = charge.formattedDueDate, + collectedOn = charge.formattedCollectedOnDate ?: charge.formattedDueDate, + amount = charge.amount.toString(), + onActionClicked = {}, + isExpandable = true + ) + } + } + when (chargesPagingList.loadState.append) { + is LoadState.Loading -> item { MifosPagingAppendProgress() } + is LoadState.Error -> item { + MifosSweetError( + message = stringResource(Res.string.feature_client_failed_to_load_more_charges) + ) { onAction(ClientChargesAction.OnRetry) } + } + else -> Unit + } + }feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.kt (4)
479-499: Amount field validation UX polishShow the trailing error icon only when invalid; you already compute
isAmountValid.- trailingIcon = { - if (!isAmountValid && state.amountTouched) { - Icon(imageVector = MifosIcons.Error, contentDescription = null) - } - }, + trailingIcon = { + if (!isAmountValid && state.amountTouched) { + Icon(imageVector = MifosIcons.Error, contentDescription = null) + } + },And prefer
state.amount.orEmpty()to avoid “null”:- value = state.amount ?: "", + value = state.amount.orEmpty(),
198-206: Count string not localizedCompose the count using resources (or plurals) instead of concatenation.
Add a string like
feature_client_charges_count= "%1$d Charges" and use:- text = state.addedCharges.size.toString() + " " + stringResource(Res.string.feature_client_charges), + text = stringResource(Res.string.feature_client_charges_count, state.addedCharges.size),
164-167: Avoid!!on error messageRender safely to prevent crashes if
erroris null.- MifosSweetError( - message = state.error!!, - ) + state.error?.let { MifosSweetError(message = it) }
219-231: Type-safety forchargesFlow— fix the unsafe cast
ClientChargesState.chargesFlowis currently typed asAny?, causing the unsafe cast inClientChargesScreen.kt. The review's suggestion is valid.
- Change
chargesFlow: Any? = nulltochargesFlow: Flow<PagingData<ChargesEntity>>? = nullinClientChargesState(ClientChargesViewModel.kt:290)- Simplify the
whenpattern inClientChargesScreen.ktto use a null-safe check insteadThis aligns with the proper typing already used in
ClientUpcomingChargesViewmodel.kt:130and eliminates the runtime type-checking and casting risks.feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt (1)
478-481: Deprecate old navigation API during type-safe migrationThe old
navigateClientChargesScreenfunction is unused internally while the new type-safenavigateToClientChargesRouteis actively used elsewhere (3 call sites). The suggested deprecation with replacement is appropriate for this migration.-fun NavController.navigateClientChargesScreen(clientId: Int) { - navigate(ClientScreens.ClientChargesScreen.argument(clientId)) -} +@Deprecated( + message = "Use typed route", + replaceWith = ReplaceWith("navigateToClientChargesRoute(clientId)"), +) +fun NavController.navigateClientChargesScreen(clientId: Int) = + navigateToClientChargesRoute(clientId)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.android.kt(2 hunks)feature/client/src/commonMain/composeResources/values/strings.xml(2 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientChargeDialog/ChargeDialogScreen.kt(0 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientChargeDialog/ChargeDialogUiState.kt(0 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesRoute.kt(1 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.kt(2 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesViewModel.kt(2 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsNavigation.kt(2 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsScreen.kt(2 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientProfile/ClientProfileNavigation.kt(1 hunks)feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt(6 hunks)feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.desktop.kt(1 hunks)feature/client/src/nativeMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.native.kt(1 hunks)
💤 Files with no reviewable changes (2)
- feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientChargeDialog/ChargeDialogUiState.kt
- feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientChargeDialog/ChargeDialogScreen.kt
🧰 Additional context used
🪛 GitHub Actions: PR Checks
feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.android.kt
[error] 1-1: Spotless Kotlin formatting violations detected. The build failed during :feature:client:spotlessKotlinCheck due to multiple formatting issues in ClientChargesScreen.android.kt (and other client charges-related files). Run './gradlew :feature:client:spotlessApply' to auto-fix formatting, or fix the violations manually. See log for specific import formatting and whitespace issues.
🔇 Additional comments (6)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsNavigation.kt (1)
22-35: No changes needed. The only call site already passes the parameter.The review assumes existing call sites may not pass
navigateToAddCharge, but verification shows the only invocation site in the codebase (ClientNavigation.kt:248) already passes all parameters includingnavigateToAddCharge = navController::navigateToClientChargesRoute(line 260). Therefore, the concern about downstream compile breaks does not apply.Likely an incorrect or invalid review comment.
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientProfile/ClientProfileNavigation.kt (1)
33-45: The review comment's premise is incorrect; the breaking change concern is not supported.The single call site at
ClientNavigation.kt:175already suppliesnavigateToAddCharge = navController::navigateToClientChargesRoute. No source-breaking change exists, and no call sites are missing the argument.However, a separate concern remains:
navigateToAddChargeis accepted byclientProfileDestinationbut cannot be forwarded toClientProfileScreen(which does not have this parameter in its signature atClientProfileScreen.kt:51). The parameter is unused in the current flow.To resolve this properly, either:
- Add
navigateToAddChargetoClientProfileScreen's parameters and forward it, or- Remove
navigateToAddChargefromclientProfileDestinationif it is not needed.Likely an incorrect or invalid review comment.
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsScreen.kt (1)
89-104: Navigation wiring verified: navigateToAddCharge is correctly supplied to the screen.The nav graph properly passes
navigateToAddChargeas a parameter toClientProfileDetailsScreenand the callback is invoked correctly with a safe fallback (state.client?.id?:-1). Code changes are good to proceed.Optionally, consider adding a guard to skip navigation when
id == -1to prevent dead routes, though this is not blocking.feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesViewModel.kt (1)
31-40: No changes needed - androidx.navigation.toRoute is correctly configured for multiplatform.The codebase is already using the correct multiplatform dependency:
org.jetbrains.androidx.navigation:navigation-compose(defined asjb-composeNavigationin the version catalog). This is provided through thecore.uimodule tofeature/clientincommonMain, ensuringtoRoute<>works across all targets (Android, iOS, Desktop, Web).feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt (1)
130-131: Good move to typed navigation for Client ChargesUsing
navigateToClientChargesRouteandclientChargesDestinationimproves type-safety.Also applies to: 146-149
feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.android.kt (1)
12-26: Verify Spotless violations by running checks locallyThe review comment lacks specific details about formatting violations. The code shown appears properly formatted: copyright header is present, imports are organized logically (androidclient resources → androidx → com.mifos), and follows the project's ktlint (v1.0.1) rules configured in
.editorconfig.Manually verify this by running:
./gradlew :feature:client:spotlessCheckIf violations exist, apply fixes with:
./gradlew :feature:client:spotlessApplyThe sandbox environment prevents automated verification, so you must confirm the actual Spotless status locally before merging.
| MifosActionsChargeListingComponent( | ||
| chargeTitle = it.name.toString(), | ||
| type = it.chargeCalculationType?.value.toString(), | ||
| date = it.formattedDueDate, | ||
| collectedOn = it.formattedDueDate, | ||
| amount = it.amount.toString(), | ||
| // menuList = , | ||
| onActionClicked = {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Wrong “collectedOn” value
Using formattedDueDate for collectedOn is misleading.
If ChargesEntity exposes a collected-on field, bind it; otherwise omit or clearly label as due date.
MifosActionsChargeListingComponent(
chargeTitle = it.name.toString(),
type = it.chargeCalculationType?.value.toString(),
date = it.formattedDueDate,
- collectedOn = it.formattedDueDate,
+ collectedOn = it.formattedCollectedOnDate ?: it.formattedDueDate,
amount = it.amount.toString(),Check available fields on ChargesEntity:
🏁 Script executed:
#!/bin/bash
rg -nP -C2 'data\s+class\s+ChargesEntity\b' && rg -nP -C2 'formattedCollectedOn|collectedOn'Length of output: 19343
Let me retrieve the complete ChargesEntity class definition directly.
---
🏁 Script executed:
```shell
#!/bin/bash
cat core/database/src/commonMain/kotlin/com/mifos/room/entities/client/ChargesEntity.kt
Length of output: 1803
Now let me verify the ChargeTimeTypeEntity structure to confirm the correct property path.
---
🏁 Script executed:
```shell
#!/bin/bash
rg -nP 'data\s+class\s+ChargeTimeTypeEntity|sealed\s+class\s+ChargeTimeTypeEntity' -A5 --type=kotlin
Length of output: 690
---
🏁 Script executed:
```shell
#!/bin/bash
cat core/database/src/commonMain/kotlin/com/mifos/room/entities/client/ChargeTimeTypeEntity.kt
Length of output: 936
Use chargeTimeType?.value for collectedOn, not formattedDueDate
The issue is valid: formattedDueDate is a due date, not a collected-on timestamp. However, the suggested fix referencing formattedCollectedOnDate is incorrect—this field doesn't exist in ChargesEntity.
The correct field to use is chargeTimeType?.value, which represents the charge collection time type and matches the pattern used in other screens (NewLoanAccountScreen.kt:467).
MifosActionsChargeListingComponent(
chargeTitle = it.name.toString(),
type = it.chargeCalculationType?.value.toString(),
date = it.formattedDueDate,
- collectedOn = it.formattedDueDate,
+ collectedOn = it.chargeTimeType?.value.toString(),
amount = it.amount.toString(),📝 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.
| MifosActionsChargeListingComponent( | |
| chargeTitle = it.name.toString(), | |
| type = it.chargeCalculationType?.value.toString(), | |
| date = it.formattedDueDate, | |
| collectedOn = it.formattedDueDate, | |
| amount = it.amount.toString(), | |
| // menuList = , | |
| onActionClicked = {}, | |
| MifosActionsChargeListingComponent( | |
| chargeTitle = it.name.toString(), | |
| type = it.chargeCalculationType?.value.toString(), | |
| date = it.formattedDueDate, | |
| collectedOn = it.chargeTimeType?.value.toString(), | |
| amount = it.amount.toString(), | |
| // menuList = , | |
| onActionClicked = {}, |
🤖 Prompt for AI Agents
In
feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.android.kt
around lines 107 to 114, the collectedOn prop is incorrectly set to
formattedDueDate; change it to use the charge collection type by passing
chargeTimeType?.value (matching the pattern used elsewhere) — ensure you convert
it to a safe string (e.g., using toString() or a null-safe fallback) so
collectedOn reflects the chargeTimeType value rather than the due date.
| firstBtnText = stringResource(Res.string.btn_back), | ||
| secondBtnText = stringResource(Res.string.client_identifier_btn_add), | ||
| onFirstBtnClick = { | ||
|
|
||
| is LoadState.Loading -> { | ||
| item { | ||
| MifosPagingAppendProgress() | ||
| } | ||
| } | ||
|
|
||
| is LoadState.NotLoading -> Unit | ||
| } | ||
|
|
||
| if ( | ||
| chargesPagingList.loadState.append is LoadState.NotLoading && | ||
| chargesPagingList.loadState.append.endOfPaginationReached && | ||
| chargesPagingList.itemCount > 0 | ||
| ) { | ||
| item { | ||
| Text( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(6.dp), | ||
| text = stringResource(Res.string.feature_client_no_more_charges_available), | ||
| style = MaterialTheme.typography.bodyMedium, | ||
| textAlign = TextAlign.Center, | ||
| }, | ||
| onSecondBtnClick = { | ||
| // onAction(NewLoanAccountAction.ShowAddChargeDialog) | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wire buttons: Back should close sheet; Add should open Add Charge
Currently both callbacks are no-ops.
MifosTwoButtonRow(
- firstBtnText = stringResource(Res.string.btn_back),
- secondBtnText = stringResource(Res.string.client_identifier_btn_add),
- onFirstBtnClick = {
-
- },
- onSecondBtnClick = {
-// onAction(NewLoanAccountAction.ShowAddChargeDialog)
- },
+ firstBtnText = stringResource(Res.string.btn_back),
+ secondBtnText = stringResource(Res.string.action_add),
+ onFirstBtnClick = { onAction(ClientChargesAction.CloseShowChargesDialog) },
+ onSecondBtnClick = { onAction(ClientChargesAction.OnShowAddCharge) },
)Also remove the unused NewLoanAccountAction import.
| <string name="feature_client_choose_charge">Choose Charge</string> | ||
| <string name="feature_client_charge_title">Charge Title</string> | ||
| <string name="feature_client_charge_name">Charge Name</string> | ||
| <string name="feature_client_charge_amount">Amount</string> | ||
| <string name="feature_client_due_date">Due Date</string> | ||
| <string name="feature_client_collected_on_date">Collected On</string> | ||
| <string name="feature_client_charge_invalid_amount_format">Invalid amount format</string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Terminology consistency: “Collected on” vs labels
Ensure the label used in UI matches this key (“Collected On”). Cross-check usages where the collected-on field uses the due-date label.
🏁 Script executed:
#!/bin/bash
rg -nP 'feature_client_collected_on_date|feature_client_due_date' --type=kt -C2Length of output: 26
I need to fix the search command since --type=kt is not recognized. Let me search across the codebase without the type restriction to find usages of these string keys.
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for usages of the two string resource keys across the codebase
rg -n 'feature_client_collected_on_date|feature_client_due_date' -C3
Length of output: 6893
Fix terminology inconsistency: collected-on field incorrectly uses due-date label
Line 505 in ClientChargesScreen.kt has a critical bug. The collected-on date picker field's label uses feature_client_due_date ("Due Date") instead of feature_client_collected_on_date ("Collected On"):
label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_due_date), // Wrong!Should be:
label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_collected_on_date),🤖 Prompt for AI Agents
In feature/client/src/commonMain/composeResources/values/strings.xml (lines
63–69) and ClientChargesScreen.kt (around line 505), the collected-on date
picker uses the wrong label resource (feature_client_due_date) causing the field
to display "Due Date" instead of "Collected On"; change the label reference in
ClientChargesScreen.kt at line ~505 to use
Res.string.feature_client_collected_on_date when state.collectedOn is non-null
so the picker shows the correct "Collected On" label.
| value = state.chargeName.toString(), | ||
| onValueChanged = { onAction(ClientChargesAction.OnChargeNameChange(it)) }, | ||
| label = stringResource(Res.string.feature_client_choose_charge), | ||
| readOnly = true, | ||
| onOptionSelected = { index, value -> | ||
| onAction( | ||
| ClientChargesAction.OnChargeIdChange( | ||
| chargeTemplate.chargeOptions[index].id ?: -1 | ||
| ) | ||
| ) | ||
| onAction(ClientChargesAction.OnChargeNameChange(value)) | ||
| onAction(ClientChargesAction.OnChargeNameTouched) | ||
| onAction(ClientChargesAction.OnShowAddCharge) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Avoid toString() on nullable state for text fields
toString() renders “null”. Prefer orEmpty() and explicit null handling.
- value = state.chargeName.toString(),
+ value = state.chargeName.orEmpty(),
@@
- onAction(ClientChargesAction.OnChargeNameChange(value))
+ onAction(ClientChargesAction.OnChargeNameChange(value ?: ""))Also update validation:
- val isChargeNameValid = validateChargeName(state.chargeName.toString())
+ val isChargeNameValid = validateChargeName(state.chargeName.orEmpty())Committable suggestion skipped: line range outside the PR's diff.
| MifosDatePickerTextField( | ||
| value = state.collectedOn?.let { | ||
| DateHelper.getDateAsStringFromLong(state.collectedOn!!) | ||
| } ?: stringResource(Res.string.feature_client_collected_on_date), | ||
| label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_due_date), | ||
| openDatePicker = { | ||
| onAction(ClientChargesAction.OnCollectedOnDatePick(true)) | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong label for “Collected On” picker
Label uses the due-date string. Use the collected-on resource.
MifosDatePickerTextField(
value = state.collectedOn?.let {
DateHelper.getDateAsStringFromLong(state.collectedOn!!)
- } ?: stringResource(Res.string.feature_client_collected_on_date),
- label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_due_date),
+ } ?: stringResource(Res.string.feature_client_collected_on_date),
+ label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_collected_on_date),📝 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.
| MifosDatePickerTextField( | |
| value = state.collectedOn?.let { | |
| DateHelper.getDateAsStringFromLong(state.collectedOn!!) | |
| } ?: stringResource(Res.string.feature_client_collected_on_date), | |
| label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_due_date), | |
| openDatePicker = { | |
| onAction(ClientChargesAction.OnCollectedOnDatePick(true)) | |
| }, | |
| ) | |
| MifosDatePickerTextField( | |
| value = state.collectedOn?.let { | |
| DateHelper.getDateAsStringFromLong(state.collectedOn!!) | |
| } ?: stringResource(Res.string.feature_client_collected_on_date), | |
| label = if (state.collectedOn == null) "" else stringResource(Res.string.feature_client_collected_on_date), | |
| openDatePicker = { | |
| onAction(ClientChargesAction.OnCollectedOnDatePick(true)) | |
| }, | |
| ) |
🤖 Prompt for AI Agents
In
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.kt
around lines 501 to 509, the MifosDatePickerTextField uses the due-date string
resource for its label when collectedOn is set; change the label to use the
collected-on resource instead. Replace the else branch to
stringResource(Res.string.feature_client_collected_on_date) so the label reads
the collected-on string (e.g., label = if (state.collectedOn == null) "" else
stringResource(Res.string.feature_client_collected_on_date)).
| onSecondBtnClick = { | ||
| val payload = ChargesPayload( | ||
| amount = state.amount, | ||
| dateFormat = DATE_FORMAT_LONG, | ||
| chargeId = state.chargeId, | ||
| dueDate = state.dueDate?.let { formatDate(it) }, | ||
| locale = locale | ||
| ) | ||
| onCreate(payload) | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Form requires “Collected On” but payload doesn’t include it
Either include collected-on in ChargesPayload or drop it from the validity gate.
Check payload fields:
If supported, extend payload:
val payload = ChargesPayload(
amount = state.amount,
dateFormat = DATE_FORMAT_LONG,
chargeId = state.chargeId,
dueDate = state.dueDate?.let { formatDate(it) },
+ collectedOn = state.collectedOn?.let { formatDate(it) },
locale = locale
)Otherwise relax validation:
- val isFormValid = isAmountValid && isChargeNameValid && validateDate(state.dueDate) && validateDate(state.collectedOn)
+ val isFormValid = isAmountValid && isChargeNameValid && validateDate(state.dueDate)Also applies to: 570-575
🏁 Script executed:
#!/bin/bash
rg -nP -C3 'data\s+class\s+ChargesPayload\b' && rg -nP -C2 '\bcollectedOn\b|\bdueDate\b|\bdateFormat\b|\blocale\b|\bchargeId\b|\bamount\b'Length of output: 210016
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# Check lines 570-575 in ClientChargesScreen.kt for context
sed -n '560,575p' feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.ktLength of output: 659
Add collectedOn field to ChargesPayload and include it in the payload construction
The form validates state.collectedOn at line 428, and the user can select it via the date picker (lines 502–507), but ChargesPayload lacks this field and the payload construction (lines 560–569) omits it. Either extend ChargesPayload to include collectedOn and pass it, or remove the validateDate(state.collectedOn) check from the validity gate.
var dueDate: String? = null,
var dateFormat: String? = null,
+var collectedOn: String? = null, val payload = ChargesPayload(
amount = state.amount,
dateFormat = DATE_FORMAT_LONG,
chargeId = state.chargeId,
dueDate = state.dueDate?.let { formatDate(it) },
+ collectedOn = state.collectedOn?.let { formatDate(it) },
locale = locale
)Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.kt
around lines 560 to 569, the payload creation omits the collectedOn field even
though the form validates state.collectedOn earlier; add a collectedOn parameter
to the ChargesPayload data class and include collectedOn =
state.collectedOn?.let { formatDate(it) } (or equivalent formatting) in the
payload construction so the selected date is sent; ensure the ChargesPayload
constructor/signature is updated wherever used to accept the new field and
update any callers/tests accordingly.
| private fun refreshChargesList() { | ||
| viewModelScope.launch { | ||
| mutableStateFlow.update { it.copy(isRefreshing = true) } | ||
| loadCharges() | ||
| mutableStateFlow.update { it.copy(isRefreshing = false) } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refresh flag clears too early; make loadCharges suspend and await it.
isRefreshing is set to false immediately after launching loadCharges, before data finishes. Convert loadCharges to a suspend function and call it from a single coroutine so refresh state reflects actual work.
- private fun loadCharges() {
- viewModelScope.launch {
- mutableStateFlow.update { it.copy(isLoading = true, error = null) }
+ private suspend fun loadCharges() {
+ mutableStateFlow.update { it.copy(isLoading = true, error = null) }
- try {
- val response = mutableStateFlow.value.clientId?.let { repository.getClientCharges(it) }
- mutableStateFlow.update {
- it.copy(
- isLoading = false,
- chargesFlow = response,
- error = null
- )
- }
- } catch (e: Exception) {
- mutableStateFlow.update {
- it.copy(
- isLoading = false,
- error = e.message ?: "Failed to load charges",
- chargesFlow = null
- )
- }
- }
- }
+ try {
+ val response = mutableStateFlow.value.clientId?.let { repository.getClientCharges(it) }
+ mutableStateFlow.update {
+ it.copy(isLoading = false, chargesFlow = response, error = null)
+ }
+ } catch (e: Exception) {
+ mutableStateFlow.update {
+ it.copy(isLoading = false, error = e.message ?: "Failed to load charges", chargesFlow = null)
+ }
+ }
}
- private fun refreshChargesList() {
- viewModelScope.launch {
- mutableStateFlow.update { it.copy(isRefreshing = true) }
- loadCharges()
- mutableStateFlow.update { it.copy(isRefreshing = false) }
- }
- }
+ private fun refreshChargesList() = viewModelScope.launch {
+ mutableStateFlow.update { it.copy(isRefreshing = true) }
+ loadCharges()
+ mutableStateFlow.update { it.copy(isRefreshing = false) }
+ }
- init {
- loadCharges()
- }
+ init {
+ viewModelScope.launch { loadCharges() }
+ }
- override fun handleAction(action: ClientChargesAction) {
+ override fun handleAction(action: ClientChargesAction) {
when (action) {
ClientChargesAction.NavigateBack -> sendEvent(ClientChargesEvent.NavigateBack)
- ClientChargesAction.Refresh -> refreshChargesList()
+ ClientChargesAction.Refresh -> refreshChargesList()
ClientChargesAction.LoadChargeTemplate -> loadChargeTemplate()
- ClientChargesAction.OnRetry -> loadCharges()
+ ClientChargesAction.OnRetry -> viewModelScope.launch { loadCharges() }Also applies to: 157-180, 42-45, 46-54
🤖 Prompt for AI Agents
In
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesViewModel.kt
around lines 182-187 (and also update usages around 157-180 and 42-54), the
refresh flag is cleared before loadCharges completes; change loadCharges to be a
suspend function, update its implementation accordingly, and call it directly
from the existing viewModelScope.launch coroutine (i.e., await it instead of
launching a separate coroutine), so mutableStateFlow.update {
it.copy(isRefreshing = false) } runs only after loadCharges returns; also update
all other callers to the new suspend signature and adjust imports/signatures as
needed.
Fixes - Client Add Charge
MIFOSAC-566.2.mp4
ClientChargesViewModelto inherit fromBaseViewModelinstead of the plainViewModelto align with the rest of the architecture.MifosBottomSheetto view chargesRemaining tasks:
View charges to be completed to show the previous charges
Charge functions like edit/delete
Run the static analysis check
./gradlew checkorci-prepush.shto make sure you didn't break anythingIf you have multiple commits please combine them into one commit by squashing them.
Summary by CodeRabbit