Skip to content

Conversation

@Shreyash16b
Copy link

@Shreyash16b Shreyash16b commented Oct 6, 2025

Fixes - Client Add Charge

MIFOSAC-566.2.mp4
  • Changed ClientChargesViewModel to inherit from BaseViewModel instead of the plain ViewModel to align with the rest of the architecture.
  • Created the add charge form and the MifosBottomSheet to view charges

Remaining tasks:

  • View charges to be completed to show the previous charges

  • Charge functions like edit/delete

  • Run the static analysis check ./gradlew check or ci-prepush.sh to make sure you didn't break anything

  • If you have multiple commits please combine them into one commit by squashing them.

Summary by CodeRabbit

  • New Features
    • Redesigned charges interface with bottom sheet presentation
    • Added charge creation form with amount validation and charge selection dropdown
    • Added date picker controls for charge collection and due dates
    • Integrated pull-to-refresh functionality for charges list
    • Added status notifications for charge creation outcomes
    • Enabled direct charge addition from client profile screen

@Shreyash16b Shreyash16b marked this pull request as draft October 10, 2025 16:37
@Shreyash16b Shreyash16b marked this pull request as ready for review October 24, 2025 14:57
@therajanmaurya
Copy link
Member

therajanmaurya commented Oct 24, 2025

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Oct 24, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Oct 24, 2025

Walkthrough

This 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

Cohort / File(s) Summary
State & ViewModel Architecture
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesViewModel.kt
Refactored to extend BaseViewModel with unified state machine. Replaces separate UI state flows with ClientChargesState and ChargeDialogState. Adds comprehensive action handling (handleAction), data-loading flows (loadCharges, loadChargeTemplate), and write operations (createCharge). Centralizes form validation and dialog state management.
Removed Components
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientChargeDialog/ChargeDialogScreen.kt, ChargeDialogUiState.kt
Deleted ChargeDialogScreen composable and all related dialog UI components, validation functions, and preview infrastructure. Removed sealed ChargeDialogUiState and its variants (Loading, Error, AllChargesV2, ChargesCreatedSuccessfully). Charge creation logic now integrated into ClientChargesViewModel.
Main Screen Refactor
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.kt
Major restructuring to use new ClientChargesScaffold with state-driven rendering. Adds validation helpers (validateAmount, validateChargeName, validateDate) as public top-level functions. Introduces ClientChargesContent flow and expect declaration for ClientChargeContent. Implements date picker dialogs, MifosStatusDialog, and unified error/loading state handling.
Platform-Specific Implementations
feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesScreen.android.kt, feature/client/src/desktopMain/kotlin/.../ClientChargesScreen.desktop.kt, feature/client/src/nativeMain/kotlin/.../ClientChargesScreen.native.kt
Updated function signatures across all platforms: replaced onRetry: () -> Unit with onAction: (ClientChargesAction) -> Unit. Android implementation adds MifosBottomSheet presentation with charge list and action row; other implementations preserve existing behavior while adopting new callback contract.
Navigation Architecture
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientCharges/ClientChargesRoute.kt
New file introducing serializable ClientChargesRoute data class with clientId parameter. Adds clientChargesDestination NavGraphBuilder extension and navigateToClientChargesRoute NavController extension for route-based navigation.
Navigation Wiring
feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt, feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsNavigation.kt, feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientProfile/ClientProfileNavigation.kt, feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetailsProfile/ClientProfileDetailsScreen.kt
Replaced direct clientChargesRoute with destination-based approach. Added navigateToAddCharge: (Int) -> Unit callback parameter to profile-related destinations and screens. Updated call sites to use navigateToClientChargesRoute. Integrated charge navigation from profile details.
String Resources
feature/client/src/commonMain/composeResources/values/strings.xml
Added six new string resources: feature_client_choose_charge, feature_client_charge_title, feature_client_collected_on_date, feature_client_created_charge_success_title, feature_client_created_charge_failure_title, feature_client_created_charge_success_message. Supports charge selection, messaging, and success/failure feedback.

Sequence Diagram

sequenceDiagram
    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
Loading

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.

🐰 A charge of refactored delight!
From dialogs dispersed to state done right,
BottomSheets rise, old screens take flight,
BaseViewModel glows in the night—
Our warren's code hops ever so bright!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Client Add Charges" is concise, clear, and directly related to the main feature being implemented in this changeset. The summary of changes confirms that the PR adds charge creation functionality, including a new add-charge form and a MifosBottomSheet for viewing charges, alongside significant architectural updates to ClientChargesViewModel and navigation restructuring. The title accurately captures the primary user-facing feature from a developer's perspective—enabling the ability to add charges to a client—which aligns with the stated PR objective of fixing issue MIFOSAC-566. While the changeset includes substantial architectural refactoring (BaseViewModel migration, state management restructuring), the title appropriately focuses on the primary feature rather than implementation details.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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: Unused onAction parameter in placeholder implementation

Suppress 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

clientId defaulting 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 validation

Fail 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 messaging

Tighten 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 ones

Replace 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 sheet

Creating 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 polish

Show 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 localized

Compose 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 message

Render safely to prevent crashes if error is null.

- MifosSweetError(
-     message = state.error!!,
- )
+ state.error?.let { MifosSweetError(message = it) }

219-231: Type-safety for chargesFlow — fix the unsafe cast

ClientChargesState.chargesFlow is currently typed as Any?, causing the unsafe cast in ClientChargesScreen.kt. The review's suggestion is valid.

  • Change chargesFlow: Any? = null to chargesFlow: Flow<PagingData<ChargesEntity>>? = null in ClientChargesState (ClientChargesViewModel.kt:290)
  • Simplify the when pattern in ClientChargesScreen.kt to use a null-safe check instead

This aligns with the proper typing already used in ClientUpcomingChargesViewmodel.kt:130 and 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 migration

The old navigateClientChargesScreen function is unused internally while the new type-safe navigateToClientChargesRoute is 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

📥 Commits

Reviewing files that changed from the base of the PR and between efc0851 and 884c9c1.

📒 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 including navigateToAddCharge = 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:175 already supplies navigateToAddCharge = navController::navigateToClientChargesRoute. No source-breaking change exists, and no call sites are missing the argument.

However, a separate concern remains: navigateToAddCharge is accepted by clientProfileDestination but cannot be forwarded to ClientProfileScreen (which does not have this parameter in its signature at ClientProfileScreen.kt:51). The parameter is unused in the current flow.

To resolve this properly, either:

  • Add navigateToAddCharge to ClientProfileScreen's parameters and forward it, or
  • Remove navigateToAddCharge from clientProfileDestination if 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 navigateToAddCharge as a parameter to ClientProfileDetailsScreen and 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 == -1 to 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 as jb-composeNavigation in the version catalog). This is provided through the core.ui module to feature/client in commonMain, ensuring toRoute<> 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 Charges

Using navigateToClientChargesRoute and clientChargesDestination improves 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 locally

The 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:spotlessCheck

If violations exist, apply fixes with:

./gradlew :feature:client:spotlessApply

The sandbox environment prevents automated verification, so you must confirm the actual Spotless status locally before merging.

Comment on lines +107 to +114
MifosActionsChargeListingComponent(
chargeTitle = it.name.toString(),
type = it.chargeCalculationType?.value.toString(),
date = it.formattedDueDate,
collectedOn = it.formattedDueDate,
amount = it.amount.toString(),
// menuList = ,
onActionClicked = {},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.

Suggested change
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.

Comment on lines +119 to 127
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)
},
)
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

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.

Comment on lines +63 to 69
<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>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -C2

Length 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.

Comment on lines +446 to +458
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)
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

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.

Comment on lines +501 to +509
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))
},
)
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

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.

Suggested change
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)).

Comment on lines +560 to +569
onSecondBtnClick = {
val payload = ChargesPayload(
amount = state.amount,
dateFormat = DATE_FORMAT_LONG,
chargeId = state.chargeId,
dueDate = state.dueDate?.let { formatDate(it) },
locale = locale
)
onCreate(payload)
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.kt

Length 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.

Comment on lines +182 to +187
private fun refreshChargesList() {
viewModelScope.launch {
mutableStateFlow.update { it.copy(isRefreshing = true) }
loadCharges()
mutableStateFlow.update { it.copy(isRefreshing = false) }
}
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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants