diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt index a7853990462..eda22b83eea 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt @@ -42,6 +42,7 @@ import com.mifos.feature.loan.navigation.addLoanAccountScreen import com.mifos.feature.loan.navigation.groupLoanScreen import com.mifos.feature.loan.navigation.loanNavGraph import com.mifos.feature.loan.navigation.navigateToGroupLoanScreen +import com.mifos.feature.loan.navigation.navigateToLoanAccountScreen import com.mifos.feature.loan.navigation.navigateToLoanAccountSummaryScreen import com.mifos.feature.note.navigation.navigateToNoteScreen import com.mifos.feature.note.navigation.noteNavGraph @@ -171,7 +172,7 @@ internal fun FeatureNavHost( clientNavGraph( navController = appState.navController, paddingValues = padding, - addLoanAccount = appState.navController::navigateToLoanAccountSummaryScreen, + addLoanAccount = appState.navController::navigateToLoanAccountScreen, addSavingsAccount = { clientId -> appState.navController.navigateToAddSavingsAccount(0, clientId, false) }, diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientDetailsRepository.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientDetailsRepository.kt index 162a7d42c04..0aaf26bd46b 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientDetailsRepository.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientDetailsRepository.kt @@ -28,5 +28,5 @@ interface ClientDetailsRepository { suspend fun getClient(clientId: Int): ClientEntity - suspend fun getImage(clientId: Int): Flow> + fun getImage(clientId: Int): Flow> } diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientDetailsRepositoryImp.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientDetailsRepositoryImp.kt index c51558af3ff..da454c94b0d 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientDetailsRepositoryImp.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientDetailsRepositoryImp.kt @@ -40,7 +40,7 @@ class ClientDetailsRepositoryImp( return dataManagerClient.getClient(clientId) } - override suspend fun getImage(clientId: Int): Flow> { + override fun getImage(clientId: Int): Flow> { return dataManagerClient.getClientImage(clientId) } } diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UploadClientImageUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UploadClientImageUseCase.kt index 974401a7e89..85a636368d0 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UploadClientImageUseCase.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UploadClientImageUseCase.kt @@ -27,12 +27,13 @@ class UploadClientImageUseCase( ) { operator fun invoke(id: Int, image: MultiPartFormDataContent): Flow> = flow { + emit(DataState.Loading) + try { - emit(DataState.Loading) repository.uploadClientImage(id, image) - DataState.Success(getString(Res.string.core_domain_client_image_uploaded_successfully)) + emit(DataState.Success(getString(Res.string.core_domain_client_image_uploaded_successfully))) } catch (e: Exception) { - DataState.Error(e) + emit(DataState.Error(e)) } } } diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerClient.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerClient.kt index 50f4fdd0933..d9f188dd769 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerClient.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerClient.kt @@ -210,7 +210,7 @@ class DataManagerClient( return mBaseApiManager.clientsApi.uploadClientImage(clientId, file) } - suspend fun getClientImage(clientId: Int): Flow> { + fun getClientImage(clientId: Int): Flow> { return mBaseApiManager.clientsApi.getClientImage(clientId) .asDataStateFlow() .map { diff --git a/feature/center/build.gradle.kts b/feature/center/build.gradle.kts index 3022cc671ab..605147ae554 100644 --- a/feature/center/build.gradle.kts +++ b/feature/center/build.gradle.kts @@ -23,6 +23,7 @@ kotlin{ implementation(projects.core.domain) implementation(compose.material3) implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) implementation(compose.ui) implementation(libs.androidx.paging.common) implementation(libs.coil.kt.compose) diff --git a/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt b/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt index 5125df82edf..2915ca31c1d 100644 --- a/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt +++ b/feature/center/src/commonMain/kotlin/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt @@ -66,16 +66,19 @@ import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.component.MifosTextFieldDropdown import com.mifos.core.ui.components.MifosAlertDialog -import com.mifos.core.ui.util.DevicePreview import com.mifos.room.entities.center.CenterPayloadEntity import com.mifos.room.entities.organisation.OfficeEntity import kotlinx.datetime.Clock import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.PreviewParameter +import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider import org.koin.compose.viewmodel.koinViewModel @Composable internal fun CreateNewCenterScreen( + onBackPressed: () -> Unit, onCreateSuccess: () -> Unit, viewModel: CreateNewCenterViewModel = koinViewModel(), ) { @@ -94,6 +97,7 @@ internal fun CreateNewCenterScreen( viewModel.createNewCenter(it) }, onCreateSuccess = onCreateSuccess, + onBackPressed = onBackPressed, ) } @@ -103,13 +107,14 @@ internal fun CreateNewCenterScreen( onRetry: () -> Unit, createCenter: (CenterPayloadEntity) -> Unit, onCreateSuccess: () -> Unit, + onBackPressed: () -> Unit, ) { val snackbarHostState = remember { SnackbarHostState() } MifosScaffold( title = stringResource(Res.string.feature_center_create_new_center), snackbarHostState = snackbarHostState, - onBackPressed = {}, + onBackPressed = onBackPressed, ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { when (state) { @@ -306,46 +311,30 @@ private fun CreateNewCenterContent( } } -@DevicePreview -@Composable -private fun CreateNewCenterLoadingPreview() { - CreateNewCenterScreen( - state = CreateNewCenterUiState.Loading, - onRetry = {}, - createCenter = {}, - onCreateSuccess = {}, - ) -} +class CreateNewCenterUiStateProvider : PreviewParameterProvider { -@DevicePreview -@Composable -private fun CreateNewCenterErrorPreview() { - CreateNewCenterScreen( - state = CreateNewCenterUiState.Error(Res.string.feature_center_failed_to_load_offices), - onRetry = {}, - createCenter = {}, - onCreateSuccess = {}, + override val values = sequenceOf( + CreateNewCenterUiState.Loading, + CreateNewCenterUiState.Error(Res.string.feature_center_failed_to_load_offices), + CreateNewCenterUiState.Offices(sampleOfficeList), + CreateNewCenterUiState.CenterCreatedSuccessfully, ) } -@DevicePreview +@Preview @Composable -private fun CreateNewCenterOfficesPreview() { +private fun CreateNewCenterPreview( + @PreviewParameter(CreateNewCenterUiStateProvider::class) state: CreateNewCenterUiState, +) { CreateNewCenterScreen( - state = CreateNewCenterUiState.Offices(emptyList()), + state = state, onRetry = {}, createCenter = {}, onCreateSuccess = {}, + onBackPressed = {}, ) } -@DevicePreview -@Composable -private fun CreateNewCenterCenterCreatedSuccessfullyPreview() { - CreateNewCenterScreen( - state = CreateNewCenterUiState.CenterCreatedSuccessfully, - onRetry = {}, - createCenter = {}, - onCreateSuccess = {}, - ) +val sampleOfficeList = List(10) { + OfficeEntity(id = it) } diff --git a/feature/center/src/commonMain/kotlin/com/mifos/feature/center/navigation/CenterNavigation.kt b/feature/center/src/commonMain/kotlin/com/mifos/feature/center/navigation/CenterNavigation.kt index 6762076e205..8f9dfa4f7f5 100644 --- a/feature/center/src/commonMain/kotlin/com/mifos/feature/center/navigation/CenterNavigation.kt +++ b/feature/center/src/commonMain/kotlin/com/mifos/feature/center/navigation/CenterNavigation.kt @@ -49,6 +49,7 @@ fun NavGraphBuilder.centerNavGraph( loadClientsOfGroup = { }, ) createCenterScreenRoute( + onBackPressed = navController::popBackStack, onCreateSuccess = navController::popBackStack, ) } @@ -105,6 +106,7 @@ fun NavGraphBuilder.centerGroupListScreenRoute( } fun NavGraphBuilder.createCenterScreenRoute( + onBackPressed: () -> Unit, onCreateSuccess: () -> Unit, ) { composable( @@ -112,6 +114,7 @@ fun NavGraphBuilder.createCenterScreenRoute( ) { CreateNewCenterScreen( onCreateSuccess = onCreateSuccess, + onBackPressed = onBackPressed, ) } } diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.android.kt deleted file mode 100644 index 6b2b7a4099d..00000000000 --- a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.android.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.client.clientDetails - -import android.Manifest -import androidx.compose.runtime.Composable -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.rememberPermissionState -import com.mifos.feature.client.utils.PlatformCameraLauncher -import io.github.vinceglb.filekit.dialogs.compose.rememberCameraPickerLauncher - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -actual fun rememberPlatformCameraLauncher( - clientId: Int, - viewModel: ClientDetailsViewModel, -): PlatformCameraLauncher { - val permissionState = rememberPermissionState(Manifest.permission.CAMERA) - - val launcher = rememberCameraPickerLauncher { file -> - file?.let { viewModel.saveClientImage(clientId, it) } - } - - return PlatformCameraLauncher( - permissionState = permissionState, - launcher = launcher, - ) -} diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.android.kt deleted file mode 100644 index f8caa7eb9e3..00000000000 --- a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.android.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.client.createNewClient - -import android.Manifest -import android.telephony.PhoneNumberUtils -import androidx.compose.runtime.Composable -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.rememberPermissionState -import com.mifos.feature.client.utils.PlatformCameraLauncher -import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.dialogs.compose.rememberCameraPickerLauncher - -actual object PhoneNumberUtil { - actual fun isGlobalPhoneNumber(phoneNumber: String): Boolean { - return PhoneNumberUtils.isGlobalPhoneNumber(phoneNumber) - } -} - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -actual fun rememberPlatformCameraLauncher( - onImageCapturedPath: (PlatformFile?) -> Unit, -): PlatformCameraLauncher { - val permissionState = rememberPermissionState(Manifest.permission.CAMERA) - - val launcher = rememberCameraPickerLauncher { file -> - onImageCapturedPath(file) - } - - return PlatformCameraLauncher( - permissionState = permissionState, - launcher = launcher, - ) -} diff --git a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.native.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.android.kt similarity index 50% rename from feature/client/src/nativeMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.native.kt rename to feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.android.kt index cff5e70dafa..39ac52b9b09 100644 --- a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.native.kt +++ b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.android.kt @@ -7,15 +7,12 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.client.clientDetails +package com.mifos.feature.client.utils -import androidx.compose.runtime.Composable -import com.mifos.feature.client.utils.PlatformCameraLauncher +import android.telephony.PhoneNumberUtils -@Composable -actual fun rememberPlatformCameraLauncher( - clientId: Int, - viewModel: ClientDetailsViewModel, -): PlatformCameraLauncher { - TODO("Not yet implemented") +actual object PhoneNumberUtil { + actual fun isGlobalPhoneNumber(phoneNumber: String): Boolean { + return PhoneNumberUtils.isGlobalPhoneNumber(phoneNumber) + } } diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.android.kt index 28674616315..7e902bf1f20 100644 --- a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.android.kt +++ b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.android.kt @@ -9,10 +9,15 @@ */ package com.mifos.feature.client.utils +import android.Manifest +import androidx.compose.runtime.Composable import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.dialogs.compose.PhotoResultLauncher +import io.github.vinceglb.filekit.dialogs.compose.rememberCameraPickerLauncher @OptIn(ExperimentalPermissionsApi::class) actual class PlatformCameraLauncher @@ -28,3 +33,20 @@ internal constructor( } } } + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +actual fun rememberPlatformCameraLauncher( + onImageCapturedPath: (PlatformFile?) -> Unit, +): PlatformCameraLauncher { + val permissionState = rememberPermissionState(Manifest.permission.CAMERA) + + val launcher = rememberCameraPickerLauncher { file -> + onImageCapturedPath(file) + } + + return PlatformCameraLauncher( + permissionState = permissionState, + launcher = launcher, + ) +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.kt index 070fd617d73..bd77aa402d1 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.kt @@ -83,7 +83,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -104,7 +103,7 @@ import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.ui.components.MifosUserImage import com.mifos.core.ui.util.DevicePreview -import com.mifos.feature.client.utils.PlatformCameraLauncher +import com.mifos.feature.client.utils.rememberPlatformCameraLauncher import com.mifos.room.entities.accounts.loans.LoanAccountEntity import com.mifos.room.entities.accounts.savings.SavingAccountDepositTypeEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity @@ -157,8 +156,11 @@ internal fun ClientDetailsScreen( } val cameraLauncher = rememberPlatformCameraLauncher( - clientId, - clientDetailsViewModel, + onImageCapturedPath = { + file -> + showSelectImageDialog = false + clientDetailsViewModel.saveClientImage(clientId, file) + }, ) LaunchedEffect(key1 = true) { @@ -329,6 +331,9 @@ internal fun ClientDetailsScreen( padding = padding, loanAccountSelected = loanAccountSelected, savingsAccountSelected = savingsAccountSelected, + onClick = { + showSelectImageDialog = true + }, ) } } @@ -339,15 +344,14 @@ internal fun ClientDetailsScreen( private fun MifosClientDetailsScreen( loanAccountSelected: (Int) -> Unit, padding: PaddingValues, + onClick: () -> Unit, savingsAccountSelected: (Int, SavingAccountDepositTypeEntity) -> Unit, clientDetailsViewModel: ClientDetailsViewModel = koinViewModel(), ) { val client = clientDetailsViewModel.client.collectAsStateWithLifecycle().value - val scope = rememberCoroutineScope() val loanAccounts = clientDetailsViewModel.loanAccount.collectAsStateWithLifecycle().value val savingsAccounts = clientDetailsViewModel.savingsAccounts.collectAsStateWithLifecycle().value val profileImage = clientDetailsViewModel.profileImage.collectAsStateWithLifecycle() - var showSelectImageDialog by remember { mutableStateOf(false) } Column( modifier = Modifier @@ -362,7 +366,9 @@ private fun MifosClientDetailsScreen( ) { MifosUserImage( bitmap = profileImage.value, - modifier = Modifier.size(100.dp), + modifier = Modifier + .size(100.dp) + .clickable(onClick = onClick), username = client?.displayName ?: "", ) } @@ -858,12 +864,6 @@ private fun MifosClientDetailsText(icon: ImageVector, field: String, value: Stri } } -@Composable -expect fun rememberPlatformCameraLauncher( - clientId: Int, - viewModel: ClientDetailsViewModel, -): PlatformCameraLauncher - @DevicePreview @Composable private fun ClientDetailsScreenPreview() { diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsUiState.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsUiState.kt index 461be2df2a0..f5879f35c79 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsUiState.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsUiState.kt @@ -14,7 +14,7 @@ package com.mifos.feature.client.clientDetails */ sealed class ClientDetailsUiState { - data class ShowUploadImageSuccessfully(val response: String, val imagePath: String?) : + data class ShowUploadImageSuccessfully(val response: String) : ClientDetailsUiState() data object ShowClientImageDeletedSuccessfully : ClientDetailsUiState() diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt index b14a6c9ef00..cf62052d703 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt @@ -18,18 +18,12 @@ import com.mifos.core.data.repository.ClientDetailsRepository import com.mifos.core.domain.useCases.GetClientDetailsUseCase import com.mifos.core.domain.useCases.UploadClientImageUseCase import com.mifos.core.ui.util.imageToByteArray +import com.mifos.feature.client.utils.compressImage import com.mifos.feature.client.utils.createImageRequestBody import com.mifos.room.entities.accounts.loans.LoanAccountEntity import com.mifos.room.entities.accounts.savings.SavingsAccountEntity import com.mifos.room.entities.client.ClientEntity -import io.github.vinceglb.filekit.FileKit -import io.github.vinceglb.filekit.ImageFormat import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.absolutePath -import io.github.vinceglb.filekit.compressImage -import io.github.vinceglb.filekit.div -import io.github.vinceglb.filekit.filesDir -import io.github.vinceglb.filekit.write import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -86,8 +80,8 @@ class ClientDetailsViewModel( is DataState.Success -> { _clientDetailsUiState.value = ClientDetailsUiState.ShowUploadImageSuccessfully( result.data, - imageFile.absolutePath(), ) + getUserProfile() _showLoading.value = false } } @@ -101,6 +95,7 @@ class ClientDetailsViewModel( _clientDetailsUiState.value = ClientDetailsUiState.ShowClientImageDeletedSuccessfully + _profileImage.value = null _showLoading.value = false } catch (e: Exception) { _clientDetailsUiState.value = @@ -130,24 +125,17 @@ class ClientDetailsViewModel( } } - fun saveClientImage(clientId: Int, imageFile: PlatformFile) { + fun saveClientImage(clientId: Int, imageFile: PlatformFile?) { + imageFile ?: return viewModelScope.launch { - saveAutoClientImage(clientId, imageFile) - } - } - - suspend fun saveAutoClientImage(clientId: Int, imageFile: PlatformFile) { - try { - val bytes = FileKit.compressImage( - file = imageFile, - imageFormat = ImageFormat.PNG, - quality = 100, - ) - val outFile = FileKit.filesDir / "client_image_$clientId.png" - outFile.write(bytes) - uploadImage(clientId, outFile) - } catch (e: Exception) { - _clientDetailsUiState.value = ClientDetailsUiState.ShowError(e.message.toString()) + try { + _showLoading.value = true + val compressed = compressImage(imageFile, clientId) + uploadImage(clientId, compressed) + } catch (e: Exception) { + _showLoading.value = false + _clientDetailsUiState.value = ClientDetailsUiState.ShowError(e.message ?: "Unexpected error") + } } } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt index 37938b4a6a9..b926bd79525 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt @@ -111,7 +111,8 @@ import com.mifos.core.designsystem.component.MifosOutlinedTextField import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.component.MifosTextFieldDropdown -import com.mifos.feature.client.utils.PlatformCameraLauncher +import com.mifos.feature.client.utils.PhoneNumberUtil +import com.mifos.feature.client.utils.rememberPlatformCameraLauncher import com.mifos.room.entities.client.ClientPayloadEntity import com.mifos.room.entities.noncore.DataTableEntity import com.mifos.room.entities.organisation.OfficeEntity @@ -188,7 +189,7 @@ internal fun CreateNewClientScreen( MifosScaffold( title = stringResource(Res.string.feature_client_create_new_client), snackbarHostState = snackbarHostState, - onBackPressed = {}, + onBackPressed = navigateBack, ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { when (uiState) { @@ -687,7 +688,6 @@ private fun createClientPayload( dateOfBirth = formatDate(dateOfBirth), dateFormat = dateFormat, locale = locale, - ) // Optional fields @@ -990,15 +990,6 @@ private fun isMiddleNameValid( } } -internal expect object PhoneNumberUtil { - fun isGlobalPhoneNumber(phoneNumber: String): Boolean -} - -@Composable -expect fun rememberPlatformCameraLauncher( - onImageCapturedPath: (PlatformFile?) -> Unit, -): PlatformCameraLauncher - private class CreateNewClientScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientViewModel.kt index 9209212a81c..7bf5d11856d 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientViewModel.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.viewModelScope import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.MFErrorParser import com.mifos.core.data.repository.CreateNewClientRepository +import com.mifos.feature.client.utils.compressImage import com.mifos.feature.client.utils.createImageRequestBody import com.mifos.room.entities.client.ClientPayloadEntity import com.mifos.room.entities.organisation.OfficeEntity @@ -133,7 +134,8 @@ class CreateNewClientViewModel( viewModelScope.launch { try { - val requestFile = createImageRequestBody(selectedImage.value!!) + val compressedImage = compressImage(selectedImage.value!!, id) + val requestFile = createImageRequestBody(compressedImage) repository.uploadClientImage(id, requestFile) diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/CompressImage.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/CompressImage.kt new file mode 100644 index 00000000000..b2461480198 --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/CompressImage.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.feature.client.utils + +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.ImageFormat +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.compressImage +import io.github.vinceglb.filekit.div +import io.github.vinceglb.filekit.filesDir +import io.github.vinceglb.filekit.write + +suspend fun compressImage(imageFile: PlatformFile, clientId: Int): PlatformFile { + val bytes = FileKit.compressImage( + file = imageFile, + imageFormat = ImageFormat.PNG, + quality = 100, + maxHeight = 150, + ) + val outFile = FileKit.filesDir / "client_image_$clientId.png" + outFile.write(bytes) + return outFile +} diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.desktop.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.kt similarity index 50% rename from feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.desktop.kt rename to feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.kt index cff5e70dafa..7af24bbcd25 100644 --- a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsScreen.desktop.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.kt @@ -7,15 +7,8 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.client.clientDetails +package com.mifos.feature.client.utils -import androidx.compose.runtime.Composable -import com.mifos.feature.client.utils.PlatformCameraLauncher - -@Composable -actual fun rememberPlatformCameraLauncher( - clientId: Int, - viewModel: ClientDetailsViewModel, -): PlatformCameraLauncher { - TODO("Not yet implemented") +expect object PhoneNumberUtil { + fun isGlobalPhoneNumber(phoneNumber: String): Boolean } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.kt index 406f5ba45e1..a6ea9454cf0 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.kt @@ -9,6 +9,14 @@ */ package com.mifos.feature.client.utils +import androidx.compose.runtime.Composable +import io.github.vinceglb.filekit.PlatformFile + expect class PlatformCameraLauncher { fun launch() } + +@Composable +expect fun rememberPlatformCameraLauncher( + onImageCapturedPath: (PlatformFile?) -> Unit, +): PlatformCameraLauncher diff --git a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.native.kt b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.desktop.kt similarity index 62% rename from feature/client/src/nativeMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.native.kt rename to feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.desktop.kt index 0fa6a387e97..de60bb6f643 100644 --- a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.native.kt +++ b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.desktop.kt @@ -7,11 +7,7 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.client.createNewClient - -import androidx.compose.runtime.Composable -import com.mifos.feature.client.utils.PlatformCameraLauncher -import io.github.vinceglb.filekit.PlatformFile +package com.mifos.feature.client.utils actual object PhoneNumberUtil { actual fun isGlobalPhoneNumber(phoneNumber: String): Boolean { @@ -19,8 +15,3 @@ actual object PhoneNumberUtil { return phoneNumber.isNotBlank() && phoneNumber.all { it.isDigit() || it == '+' } } } - -@Composable -actual fun rememberPlatformCameraLauncher(onImageCapturedPath: (PlatformFile?) -> Unit): PlatformCameraLauncher { - TODO("Not yet implemented") -} diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.desktop.kt b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.desktop.kt index d810cf3eef4..9030ed48c43 100644 --- a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.desktop.kt +++ b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.desktop.kt @@ -9,7 +9,15 @@ */ package com.mifos.feature.client.utils +import androidx.compose.runtime.Composable +import io.github.vinceglb.filekit.PlatformFile + actual class PlatformCameraLauncher { actual fun launch() { } } + +@Composable +actual fun rememberPlatformCameraLauncher(onImageCapturedPath: (PlatformFile?) -> Unit): PlatformCameraLauncher { + TODO("Not yet implemented") +} diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.desktop.kt b/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.native.kt similarity index 62% rename from feature/client/src/desktopMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.desktop.kt rename to feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.native.kt index 0fa6a387e97..de60bb6f643 100644 --- a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientScreen.desktop.kt +++ b/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PhoneNumberUtil.native.kt @@ -7,11 +7,7 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.client.createNewClient - -import androidx.compose.runtime.Composable -import com.mifos.feature.client.utils.PlatformCameraLauncher -import io.github.vinceglb.filekit.PlatformFile +package com.mifos.feature.client.utils actual object PhoneNumberUtil { actual fun isGlobalPhoneNumber(phoneNumber: String): Boolean { @@ -19,8 +15,3 @@ actual object PhoneNumberUtil { return phoneNumber.isNotBlank() && phoneNumber.all { it.isDigit() || it == '+' } } } - -@Composable -actual fun rememberPlatformCameraLauncher(onImageCapturedPath: (PlatformFile?) -> Unit): PlatformCameraLauncher { - TODO("Not yet implemented") -} diff --git a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.native.kt b/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.native.kt index d810cf3eef4..9030ed48c43 100644 --- a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.native.kt +++ b/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.native.kt @@ -9,7 +9,15 @@ */ package com.mifos.feature.client.utils +import androidx.compose.runtime.Composable +import io.github.vinceglb.filekit.PlatformFile + actual class PlatformCameraLauncher { actual fun launch() { } } + +@Composable +actual fun rememberPlatformCameraLauncher(onImageCapturedPath: (PlatformFile?) -> Unit): PlatformCameraLauncher { + TODO("Not yet implemented") +} diff --git a/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt index 3c59a51c7b3..af036c702c5 100644 --- a/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt @@ -94,6 +94,7 @@ import org.koin.compose.viewmodel.koinViewModel internal fun CreateNewGroupScreen( viewModel: CreateNewGroupViewModel = koinViewModel(), onGroupCreated: (group: SaveResponse?, userStatus: Boolean) -> Unit, + onBackPressed: () -> Unit, ) { val uiState by viewModel.createNewGroupUiState.collectAsStateWithLifecycle() val userStatus by viewModel.userStatus.collectAsStateWithLifecycle() @@ -110,6 +111,7 @@ internal fun CreateNewGroupScreen( }, onGroupCreated = { onGroupCreated(it, userStatus) }, getResponse = { viewModel.getResponse() }, + onBackPressed = onBackPressed, ) } @@ -118,6 +120,7 @@ internal fun CreateNewGroupScreen( internal fun CreateNewGroupScreen( uiState: CreateNewGroupUiState, onRetry: () -> Unit, + onBackPressed: () -> Unit, invokeGroupCreation: (GroupPayloadEntity) -> Unit, onGroupCreated: (group: SaveResponse?) -> Unit, modifier: Modifier = Modifier, @@ -128,7 +131,7 @@ internal fun CreateNewGroupScreen( MifosScaffold( modifier = modifier, title = stringResource(Res.string.feature_groups_create_new_group), - onBackPressed = {}, + onBackPressed = onBackPressed, snackbarHostState = snackbarHostState, ) { paddingValues -> Box( @@ -441,5 +444,6 @@ private fun PreviewCreateNewGroupScreen( onGroupCreated = { _ -> }, getResponse = { "" }, + onBackPressed = {}, ) } diff --git a/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt index ec66ef8fbf4..089819a2f94 100644 --- a/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt +++ b/feature/groups/src/commonMain/kotlin/com/mifos/feature/groups/navigation/GroupNavGraph.kt @@ -69,6 +69,7 @@ fun NavGraphBuilder.groupNavGraph( groups?.groupId?.let { navController.navigateToGroupDetailsScreen(it) } } }, + onBackPressed = navController::popBackStack, ) } } @@ -121,11 +122,13 @@ fun NavGraphBuilder.groupDetailsRoute( } fun NavGraphBuilder.addNewGroupRoute( + onBackPressed: () -> Unit, onGroupCreated: (group: SaveResponse?, userStatus: Boolean) -> Unit, ) { composable(route = GroupScreen.CreateNewGroupScreen.route) { CreateNewGroupScreen( onGroupCreated = onGroupCreated, + onBackPressed = onBackPressed, ) } }