diff --git a/feature/standing-instruction/.gitignore b/feature/standing-instruction/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/feature/standing-instruction/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/standing-instruction/build.gradle.kts b/feature/standing-instruction/build.gradle.kts new file mode 100644 index 000000000..6469c2a1c --- /dev/null +++ b/feature/standing-instruction/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) +} + +android { + namespace = "org.mifospay.feature.standing.instruction" +} + +dependencies { + implementation(projects.core.data) +} \ No newline at end of file diff --git a/feature/standing-instruction/consumer-rules.pro b/feature/standing-instruction/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/feature/standing-instruction/proguard-rules.pro b/feature/standing-instruction/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/feature/standing-instruction/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/standing-instruction/src/androidTest/java/org/mifospay/feature/standing/instruction/ExampleInstrumentedTest.kt b/feature/standing-instruction/src/androidTest/java/org/mifospay/feature/standing/instruction/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..8d857f5c3 --- /dev/null +++ b/feature/standing-instruction/src/androidTest/java/org/mifospay/feature/standing/instruction/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.mifospay.feature.standing.instruction + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.mifospay.feature.standing.instruction.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/standing-instruction/src/main/AndroidManifest.xml b/feature/standing-instruction/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/feature/standing-instruction/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/SIContent.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/SIContent.kt new file mode 100644 index 000000000..62944986c --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/SIContent.kt @@ -0,0 +1,65 @@ +package org.mifospay.feature.standing.instruction + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun SIContent( + fromClientName: String, + toClientName: String, + validTill: String, + amount: String +) { + Column(modifier = Modifier.padding(10.dp)) { + Text( + text = fromClientName, + color = Color.Black, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 20.dp) + ) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Text( + text = toClientName, + color = Color.Black, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 4.dp) + ) + Text( + text = amount, + color = Color.Black, + fontSize = 16.sp, + modifier = Modifier.padding(end = 8.dp, bottom = 8.dp) + ) + } + + Text( + text = validTill, + color = Color.Gray, + modifier = Modifier.padding(bottom = 4.dp) + ) + + Divider( + color = Color.Black, + thickness = 1.dp, + modifier = Modifier.padding(vertical = 8.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SIContentPreview() { + SIContent("From Client", "To Client", "Date", "Amount") +} \ No newline at end of file diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionScreen.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionScreen.kt new file mode 100644 index 000000000..4d795c78e --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionScreen.kt @@ -0,0 +1,162 @@ +package org.mifospay.feature.standing.instruction + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import com.mifospay.core.model.entity.client.Client +import com.mifospay.core.model.entity.client.Status +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.EmptyContentScreen + +@Composable +fun StandingInstructionsScreen( + viewModel: StandingInstructionViewModel = hiltViewModel(), + onNewSI: () -> Unit +) { + val standingInstructionsUiState by viewModel.standingInstructionsUiState.collectAsStateWithLifecycle() + StandingInstructionScreen( + standingInstructionsUiState = standingInstructionsUiState, + onNewSI = onNewSI + ) +} + +@Composable +fun StandingInstructionScreen( + standingInstructionsUiState: StandingInstructionsUiState, + onNewSI: () -> Unit +) = when (standingInstructionsUiState) { + StandingInstructionsUiState.Empty -> { + EmptyContentScreen( + modifier = Modifier, + title = stringResource(id = R.string.feature_standing_instruction_error_oops), + subTitle = stringResource(id = R.string.feature_standing_instruction_empty_standing_instructions), + iconTint = Color.Black, + iconImageVector = Icons.Rounded.Info + ) + } + + is StandingInstructionsUiState.Error -> { + EmptyContentScreen( + modifier = Modifier, + title = stringResource(id = R.string.feature_standing_instruction_error_oops), + subTitle = stringResource(id = R.string.feature_standing_instruction_error_fetching_si_list), + iconTint = Color.Black, + iconImageVector = Icons.Rounded.Info + ) + } + + StandingInstructionsUiState.Loading -> { + MifosLoadingWheel( + modifier = Modifier.fillMaxWidth(), + contentDesc = stringResource(R.string.feature_standing_instruction_loading) + ) + } + + is StandingInstructionsUiState.StandingInstructionList -> { + Scaffold( + modifier = Modifier, + floatingActionButton = { + FloatingActionButton( + onClick = { onNewSI.invoke() }, + ) { + Icon( + painter = rememberVectorPainter(MifosIcons.Add), + contentDescription = null, + tint = Color.Black + ) + } + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(standingInstructionsUiState.standingInstructionList) { items -> + SIContent( + fromClientName = items.fromClient.displayName.toString(), + toClientName = items.toClient.displayName.toString(), + validTill = items.validTill.toString(), + amount = items.amount.toString(), + ) + } + } + } + } + + } +} + +class StandingInstructionPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + StandingInstructionsUiState.Loading, + StandingInstructionsUiState.Empty, + StandingInstructionsUiState.Error("Error Screen"), + StandingInstructionsUiState.StandingInstructionList( + standingInstructionList = listOf( + StandingInstruction( + id = 1, + name = "Instruction 1", + fromClient = Client(displayName = "Alice"), + fromAccount = SavingAccount(), + toClient = Client(displayName = "Bob"), + toAccount = SavingAccount(), + status = Status(), + amount = 100.0, + validFrom = listOf(2022, 1, 1), + validTill = listOf(2024, 12, 31), + recurrenceInterval = 30, + recurrenceOnMonthDay = listOf(1) + ), + StandingInstruction( + id = 2, + name = "Instruction 2", + fromClient = Client(displayName = "Charlie"), + fromAccount = SavingAccount(), + toClient = Client(displayName = "Dave"), + toAccount = SavingAccount(), + status = Status(), + amount = 200.0, + validFrom = listOf(2022, 1, 1), + validTill = listOf(2024, 12, 31), + recurrenceInterval = 30, + recurrenceOnMonthDay = listOf(1) + ) + ) + ) + ) +} + +@Preview(showBackground = true) +@Composable +fun StandingInstructionsScreenPreview( + @PreviewParameter(StandingInstructionPreviewParameterProvider::class) standingInstructionsUiState: StandingInstructionsUiState +) { + StandingInstructionScreen( + standingInstructionsUiState = standingInstructionsUiState, + onNewSI = {} + ) +} \ No newline at end of file diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionViewModel.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionViewModel.kt new file mode 100644 index 000000000..5d9f6c3ca --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionViewModel.kt @@ -0,0 +1,59 @@ +package org.mifospay.feature.standing.instruction + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.standinginstruction.GetAllStandingInstructions +import org.mifospay.core.data.repository.local.LocalRepository +import javax.inject.Inject + +@HiltViewModel +class StandingInstructionViewModel @Inject constructor( + private val mUseCaseHandler: UseCaseHandler, + private val localRepository: LocalRepository, + private val getAllStandingInstructions: GetAllStandingInstructions +) : ViewModel() { + + private val _standingInstructionsUiState = + MutableStateFlow(StandingInstructionsUiState.Loading) + val standingInstructionsUiState: StateFlow = + _standingInstructionsUiState + + private fun getAllSI() { + val client = localRepository.clientDetails + _standingInstructionsUiState.value = StandingInstructionsUiState.Loading + mUseCaseHandler.execute(getAllStandingInstructions, + GetAllStandingInstructions.RequestValues(client.clientId), object : + UseCase.UseCaseCallback { + + override fun onSuccess(response: GetAllStandingInstructions.ResponseValue) { + if (response.standingInstructionsList.isEmpty()) { + _standingInstructionsUiState.value = StandingInstructionsUiState.Empty + } else { + _standingInstructionsUiState.value = + StandingInstructionsUiState.StandingInstructionList(response.standingInstructionsList) + } + } + + override fun onError(message: String) { + _standingInstructionsUiState.value = StandingInstructionsUiState.Error(message) + } + }) + } + + init { + getAllSI() + } +} + +sealed class StandingInstructionsUiState { + data object Loading : StandingInstructionsUiState() + data object Empty : StandingInstructionsUiState() + data class Error(val message: String) : StandingInstructionsUiState() + data class StandingInstructionList(val standingInstructionList: List) : + StandingInstructionsUiState() +} \ No newline at end of file diff --git a/feature/standing-instruction/src/main/res/values/strings.xml b/feature/standing-instruction/src/main/res/values/strings.xml new file mode 100644 index 000000000..970ad8ff6 --- /dev/null +++ b/feature/standing-instruction/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Oops! + No standing instructions to show + Couldn\'t fetch Standing Instructions. Please, try again. + Loading + \ No newline at end of file diff --git a/feature/standing-instruction/src/test/java/org/mifospay/feature/standing/instruction/ExampleUnitTest.kt b/feature/standing-instruction/src/test/java/org/mifospay/feature/standing/instruction/ExampleUnitTest.kt new file mode 100644 index 000000000..2527f068b --- /dev/null +++ b/feature/standing-instruction/src/test/java/org/mifospay/feature/standing/instruction/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package org.mifospay.feature.standing.instruction + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a5d6c6939..454b4fd51 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -48,4 +48,5 @@ include(":feature:invoices") include(":feature:invoices") include(":feature:settings") include(":feature:profile") +include(":feature:standing-instruction") include(":feature:payments")