Skip to content

Commit bb4e8b8

Browse files
authored
feature(savingsAccount) : charge details step impl (#2516)
1 parent e132de3 commit bb4e8b8

File tree

8 files changed

+482
-20
lines changed

8 files changed

+482
-20
lines changed

core/database/src/commonMain/kotlin/com/mifos/room/entities/templates/savings/SavingProductsTemplate.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package com.mifos.room.entities.templates.savings
1212
import com.mifos.core.model.objects.account.loan.Currency
1313
import com.mifos.core.model.objects.account.saving.FieldOfficerOptions
1414
import com.mifos.core.model.objects.commonfiles.InterestType
15+
import com.mifos.core.model.objects.template.client.ChargeOptions
1516
import com.mifos.core.model.objects.template.saving.AccountOptions
1617
import com.mifos.core.model.utils.IgnoredOnParcel
1718
import com.mifos.core.model.utils.Parcelable
@@ -82,4 +83,6 @@ class SavingProductsTemplate(
8283

8384
@IgnoredOnParcel
8485
val fieldOfficerOptions: List<FieldOfficerOptions>? = null,
86+
87+
val chargeOptions: List<ChargeOptions>? = null,
8588
) : Parcelable

core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBottomSheet.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ fun MifosBottomSheet(
5454
content: @Composable () -> Unit,
5555
) {
5656
val coroutineScope = rememberCoroutineScope()
57-
val modalSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
57+
val modalSheetState = rememberModalBottomSheetState(
58+
skipPartiallyExpanded = true,
59+
)
5860
var showBottomSheet by remember { mutableStateOf(true) }
5961

6062
fun dismissSheet() {

core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/AddChargeDialog.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ fun AddChargeBottomSheet(
6262
onChargeSelected: (Int, String) -> Unit,
6363
onDatePick: (Boolean) -> Unit,
6464
onDateChange: (Long) -> Unit,
65+
amountError: String? = null,
6566
onAmountChange: (String) -> Unit,
6667
) {
6768
MifosBottomSheet(
@@ -121,6 +122,8 @@ fun AddChargeBottomSheet(
121122
onValueChange = onAmountChange,
122123
label = stringResource(Res.string.amount),
123124
config = MifosTextFieldConfig(
125+
isError = amountError != null,
126+
errorText = amountError,
124127
keyboardOptions = KeyboardOptions(
125128
keyboardType = KeyboardType.Decimal,
126129
),
@@ -158,7 +161,7 @@ fun AddChargeBottomSheet(
158161
secondBtnText = confirmText,
159162
onFirstBtnClick = onDismiss,
160163
onSecondBtnClick = onConfirm,
161-
isSecondButtonEnabled = chargeAmount.isNotEmpty() && chargeType.isNotEmpty(),
164+
isSecondButtonEnabled = chargeAmount.isNotEmpty() && chargeType.isNotEmpty() && amountError.isNullOrEmpty(),
162165
)
163166
}
164167
},
@@ -187,6 +190,7 @@ private fun AddChargeBottomSheetPreview() {
187190
onChargeSelected = { _, _ -> },
188191
onDatePick = { },
189192
onDateChange = { },
193+
amountError = null,
190194
onAmountChange = {},
191195
)
192196
}

core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/TextFieldsValidator.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ object TextFieldsValidator {
4343
else -> null
4444
}
4545
}
46+
47+
fun doubleNumberValidator(input: String): StringResource? {
48+
return when {
49+
input.count { it == '.' } > 1 -> Res.string.error_invalid_number
50+
input.any { !it.isDigit() && it != '.' } -> Res.string.error_digits_only
51+
input.toDoubleOrNull() == null -> Res.string.error_invalid_number
52+
input == "0" || input == "0.0" || input.toDoubleOrNull() == 0.0 -> Res.string.error_number_zero
53+
else -> null
54+
}
55+
}
4656
}

feature/savings/src/commonMain/composeResources/values/feature_savings_strings.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@
131131
<string name="step_terms_apply_withdrawal_fee">Apply Withdrawal Fee for Transfers</string>
132132
<string name="step_terms_min_opening_balance">Minimum Opening Balance</string>
133133

134+
135+
<!-- Charges Page -->
136+
<string name="step_charges_add_new">Add New</string>
137+
<string name="step_charges_add">Add</string>
138+
<string name="step_charges_view">View</string>
139+
<string name="step_charges_active">Active</string>
140+
<string name="step_charges_edit_charge">Edit Charge</string>
141+
134142
</resources>
135143

136144

feature/savings/src/commonMain/kotlin/com/mifos/feature/savings/savingsAccountv2/SavingsAccountScreen.kt

Lines changed: 181 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,59 @@
1010
package com.mifos.feature.savings.savingsAccountv2
1111

1212
import androidclient.feature.savings.generated.resources.Res
13+
import androidclient.feature.savings.generated.resources.feature_savings_back
14+
import androidclient.feature.savings.generated.resources.feature_savings_cancel
1315
import androidclient.feature.savings.generated.resources.feature_savings_create_savings_account
1416
import androidclient.feature.savings.generated.resources.feature_savings_error_not_connected_internet
1517
import androidclient.feature.savings.generated.resources.step_charges
18+
import androidclient.feature.savings.generated.resources.step_charges_add
19+
import androidclient.feature.savings.generated.resources.step_charges_add_new
20+
import androidclient.feature.savings.generated.resources.step_charges_edit_charge
21+
import androidclient.feature.savings.generated.resources.step_charges_view
1622
import androidclient.feature.savings.generated.resources.step_details
1723
import androidclient.feature.savings.generated.resources.step_preview
1824
import androidclient.feature.savings.generated.resources.step_terms
25+
import androidx.compose.foundation.layout.Arrangement
1926
import androidx.compose.foundation.layout.Column
2027
import androidx.compose.foundation.layout.fillMaxSize
2128
import androidx.compose.foundation.layout.fillMaxWidth
2229
import androidx.compose.foundation.layout.padding
30+
import androidx.compose.foundation.lazy.LazyColumn
31+
import androidx.compose.foundation.lazy.itemsIndexed
32+
import androidx.compose.material3.ExperimentalMaterial3Api
33+
import androidx.compose.material3.Text
2334
import androidx.compose.runtime.Composable
35+
import androidx.compose.runtime.LaunchedEffect
2436
import androidx.compose.runtime.getValue
37+
import androidx.compose.ui.Alignment
2538
import androidx.compose.ui.Modifier
2639
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2740
import androidx.navigation.NavController
41+
import com.mifos.core.common.utils.DateHelper
42+
import com.mifos.core.designsystem.component.MifosBottomSheet
2843
import com.mifos.core.designsystem.component.MifosScaffold
2944
import com.mifos.core.designsystem.component.MifosSweetError
45+
import com.mifos.core.designsystem.theme.DesignToken
46+
import com.mifos.core.designsystem.theme.MifosTypography
47+
import com.mifos.core.ui.components.Actions
48+
import com.mifos.core.ui.components.AddChargeBottomSheet
49+
import com.mifos.core.ui.components.MifosActionsChargeListingComponent
3050
import com.mifos.core.ui.components.MifosBreadcrumbNavBar
3151
import com.mifos.core.ui.components.MifosProgressIndicator
3252
import com.mifos.core.ui.components.MifosProgressIndicatorOverlay
3353
import com.mifos.core.ui.components.MifosStepper
54+
import com.mifos.core.ui.components.MifosTwoButtonRow
3455
import com.mifos.core.ui.components.Step
3556
import com.mifos.core.ui.util.EventsEffect
57+
import com.mifos.core.ui.util.TextFieldsValidator.doubleNumberValidator
3658
import com.mifos.feature.savings.savingsAccountv2.pages.ChargesPage
3759
import com.mifos.feature.savings.savingsAccountv2.pages.DetailsPage
3860
import com.mifos.feature.savings.savingsAccountv2.pages.PreviewPage
3961
import com.mifos.feature.savings.savingsAccountv2.pages.TermsPage
4062
import org.jetbrains.compose.resources.stringResource
4163
import org.koin.compose.viewmodel.koinViewModel
64+
import kotlin.time.Clock
65+
import kotlin.time.ExperimentalTime
4266

4367
@Composable
4468
internal fun SavingsAccountScreen(
@@ -63,7 +87,7 @@ internal fun SavingsAccountScreen(
6387
)
6488

6589
SavingsAccountScaffold(
66-
modifier = modifier,
90+
modifier = modifier.fillMaxWidth(),
6791
state = state,
6892
onAction = { viewModel.trySendAction(it) },
6993
navController = navController,
@@ -91,9 +115,10 @@ private fun SavingsAccountScaffold(
91115
)
92116
},
93117
Step(stringResource(Res.string.step_charges)) {
94-
ChargesPage {
95-
onAction(SavingsAccountAction.NextStep)
96-
}
118+
ChargesPage(
119+
state = state,
120+
onAction = onAction,
121+
)
97122
},
98123
Step(stringResource(Res.string.step_preview)) {
99124
PreviewPage {
@@ -123,10 +148,11 @@ private fun SavingsAccountScaffold(
123148
onAction(SavingsAccountAction.OnStepChange(newIndex))
124149
},
125150
modifier = Modifier
126-
.fillMaxWidth(),
151+
.fillMaxWidth().align(Alignment.CenterHorizontally),
127152
)
128153
}
129154
}
155+
130156
is SavingsAccountState.ScreenState.NetworkError -> {
131157
MifosSweetError(
132158
message = stringResource(Res.string.feature_savings_error_not_connected_internet),
@@ -152,6 +178,156 @@ private fun NewSavingsAccountDialog(
152178
onclick = { onAction(SavingsAccountAction.Retry) },
153179
)
154180
}
181+
182+
is SavingsAccountState.DialogState.AddNewCharge -> AddNewChargeDialog(
183+
isEdit = state.dialogState.edit,
184+
state = state,
185+
onAction = onAction,
186+
index = state.dialogState.index,
187+
)
188+
189+
is SavingsAccountState.DialogState.ShowCharges -> ShowChargesDialog(
190+
state = state,
191+
onAction = onAction,
192+
)
193+
155194
null -> Unit
156195
}
157196
}
197+
198+
@OptIn(ExperimentalTime::class, ExperimentalMaterial3Api::class)
199+
@Composable
200+
private fun AddNewChargeDialog(
201+
isEdit: Boolean,
202+
index: Int = -1,
203+
state: SavingsAccountState,
204+
onAction: (SavingsAccountAction) -> Unit,
205+
) {
206+
LaunchedEffect(state.chargeAmount) {
207+
if (state.chargeAmount.isNotBlank()) {
208+
val amountError = doubleNumberValidator(state.chargeAmount)
209+
onAction(SavingsAccountAction.OnChargesAmountChangeError(amountError))
210+
} else {
211+
onAction(SavingsAccountAction.OnChargesAmountChangeError(null))
212+
}
213+
}
214+
fun isSelectableDate(utcTimeMillis: Long): Boolean {
215+
return utcTimeMillis >= Clock.System.now().toEpochMilliseconds().minus(86_400_000L)
216+
}
217+
AddChargeBottomSheet(
218+
title = if (isEdit) {
219+
stringResource(Res.string.step_charges_edit_charge)
220+
} else {
221+
stringResource(Res.string.step_charges_add_new) + " " + stringResource(Res.string.step_charges)
222+
},
223+
confirmText = if (isEdit) {
224+
stringResource(Res.string.step_charges_edit_charge)
225+
} else {
226+
stringResource(Res.string.step_charges_add)
227+
},
228+
dismissText = stringResource(Res.string.feature_savings_cancel),
229+
showDatePicker = state.showChargesDatePick,
230+
selectedChargeName = if (state.chooseChargeIndex == -1) {
231+
""
232+
} else {
233+
state.savingsProductTemplate?.chargeOptions?.getOrNull(state.chooseChargeIndex)?.name ?: ""
234+
},
235+
selectedDate = state.chargeDate,
236+
chargeAmount = state.chargeAmount,
237+
chargeType = if (state.chooseChargeIndex == -1) {
238+
""
239+
} else {
240+
state.savingsProductTemplate?.chargeOptions?.get(state.chooseChargeIndex)?.chargeCalculationType?.value
241+
?: ""
242+
},
243+
chargeCollectedOn = if (state.chooseChargeIndex == -1) {
244+
""
245+
} else {
246+
state.savingsProductTemplate?.chargeOptions?.getOrNull(state.chooseChargeIndex)?.chargeTimeType?.value ?: ""
247+
},
248+
chargeOptions = state.savingsProductTemplate?.chargeOptions?.map { it.name ?: "" } ?: emptyList(),
249+
onConfirm = {
250+
if (isEdit) {
251+
onAction(SavingsAccountAction.EditCharge(index))
252+
} else {
253+
onAction(SavingsAccountAction.AddChargeToList)
254+
}
255+
},
256+
onDismiss = { onAction(SavingsAccountAction.DismissDialog) },
257+
onChargeSelected = { index, _ ->
258+
onAction(SavingsAccountAction.OnChooseChargeIndexChange(index))
259+
},
260+
onDatePick = { show ->
261+
onAction(SavingsAccountAction.OnChargesDatePick(show))
262+
},
263+
onDateChange = { newDate ->
264+
if (isSelectableDate(newDate)) {
265+
onAction(SavingsAccountAction.OnChargesDateChange(DateHelper.getDateAsStringFromLong(newDate)))
266+
}
267+
},
268+
amountError = if (state.chargeAmountError != null) stringResource(state.chargeAmountError) else null,
269+
onAmountChange = { amount ->
270+
onAction(SavingsAccountAction.OnChargesAmountChange(amount))
271+
},
272+
)
273+
}
274+
275+
@Composable
276+
private fun ShowChargesDialog(
277+
state: SavingsAccountState,
278+
onAction: (SavingsAccountAction) -> Unit,
279+
) {
280+
MifosBottomSheet(
281+
onDismiss = {
282+
onAction(SavingsAccountAction.DismissDialog)
283+
},
284+
content = {
285+
LazyColumn(
286+
modifier = Modifier.fillMaxWidth().padding(DesignToken.padding.large),
287+
verticalArrangement = Arrangement.spacedBy(DesignToken.padding.largeIncreased),
288+
) {
289+
item {
290+
Text(
291+
text = stringResource(Res.string.step_charges_view) + " " + stringResource(Res.string.step_charges),
292+
style = MifosTypography.titleMediumEmphasized,
293+
)
294+
}
295+
itemsIndexed(items = state.addedCharges) { index, it ->
296+
MifosActionsChargeListingComponent(
297+
chargeTitle = it.name.toString(),
298+
type = it.type.toString(),
299+
date = it.date,
300+
collectedOn = it.collectedOn,
301+
amount = it.amount.toString(),
302+
onActionClicked = { action ->
303+
when (action) {
304+
is Actions.Delete -> {
305+
onAction(SavingsAccountAction.DeleteChargeFromSelectedCharges(index))
306+
}
307+
308+
is Actions.Edit -> {
309+
onAction(SavingsAccountAction.EditChargeDialog(index))
310+
}
311+
312+
else -> {}
313+
}
314+
},
315+
isExpandable = true,
316+
)
317+
}
318+
item {
319+
MifosTwoButtonRow(
320+
firstBtnText = stringResource(Res.string.feature_savings_back),
321+
secondBtnText = stringResource(Res.string.step_charges_add_new),
322+
onFirstBtnClick = {
323+
onAction(SavingsAccountAction.DismissDialog)
324+
},
325+
onSecondBtnClick = {
326+
onAction(SavingsAccountAction.ShowAddChargeDialog)
327+
},
328+
)
329+
}
330+
}
331+
},
332+
)
333+
}

0 commit comments

Comments
 (0)