diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/di/RepositoryModule.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/di/RepositoryModule.kt index 1a487c8fc1c..2c736a9db05 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/di/RepositoryModule.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/di/RepositoryModule.kt @@ -19,7 +19,6 @@ import com.mifos.core.data.repository.CheckerInboxTasksRepository import com.mifos.core.data.repository.ClientChargeRepository import com.mifos.core.data.repository.ClientDetailsEditRepository import com.mifos.core.data.repository.ClientDetailsRepository -import com.mifos.core.data.repository.ClientIdentifierDialogRepository import com.mifos.core.data.repository.ClientIdentifiersRepository import com.mifos.core.data.repository.ClientListRepository import com.mifos.core.data.repository.CreateNewCenterRepository @@ -29,7 +28,7 @@ import com.mifos.core.data.repository.DataTableDataRepository import com.mifos.core.data.repository.DataTableListRepository import com.mifos.core.data.repository.DataTableRepository import com.mifos.core.data.repository.DataTableRowDialogRepository -import com.mifos.core.data.repository.DocumentDialogRepository +import com.mifos.core.data.repository.DocumentCreateUpdateRepository import com.mifos.core.data.repository.DocumentListRepository import com.mifos.core.data.repository.GenerateCollectionSheetRepository import com.mifos.core.data.repository.GroupDetailsRepository @@ -81,7 +80,6 @@ import com.mifos.core.data.repositoryImp.CheckerInboxTasksRepositoryImp import com.mifos.core.data.repositoryImp.ClientChargeRepositoryImp import com.mifos.core.data.repositoryImp.ClientDetailsEditRepositoryImpl import com.mifos.core.data.repositoryImp.ClientDetailsRepositoryImp -import com.mifos.core.data.repositoryImp.ClientIdentifierDialogRepositoryImp import com.mifos.core.data.repositoryImp.ClientIdentifiersRepositoryImp import com.mifos.core.data.repositoryImp.ClientListRepositoryImp import com.mifos.core.data.repositoryImp.CreateNewCenterRepositoryImp @@ -91,7 +89,7 @@ import com.mifos.core.data.repositoryImp.DataTableDataRepositoryImp import com.mifos.core.data.repositoryImp.DataTableListRepositoryImp import com.mifos.core.data.repositoryImp.DataTableRepositoryImp import com.mifos.core.data.repositoryImp.DataTableRowDialogRepositoryImp -import com.mifos.core.data.repositoryImp.DocumentDialogRepositoryImp +import com.mifos.core.data.repositoryImp.DocumentCreateUpdateRepositoryImp import com.mifos.core.data.repositoryImp.DocumentListRepositoryImp import com.mifos.core.data.repositoryImp.GenerateCollectionSheetRepositoryImp import com.mifos.core.data.repositoryImp.GroupDetailsRepositoryImp @@ -151,7 +149,6 @@ val RepositoryModule = module { singleOf(::ClientDetailsRepositoryImp) bind ClientDetailsRepository::class singleOf(::ClientListRepositoryImp) bind ClientListRepository::class singleOf(::ClientChargeRepositoryImp) bind ClientChargeRepository::class - singleOf(::ClientIdentifierDialogRepositoryImp) bind ClientIdentifierDialogRepository::class singleOf(::ClientIdentifiersRepositoryImp) bind ClientIdentifiersRepository::class singleOf(::CreateNewClientRepositoryImp) bind CreateNewClientRepository::class singleOf(::ClientDetailsEditRepositoryImpl) bind ClientDetailsEditRepository::class @@ -207,7 +204,7 @@ val RepositoryModule = module { singleOf(::DataTableListRepositoryImp) bind DataTableListRepository::class singleOf(::DataTableRepositoryImp) bind DataTableRepository::class singleOf(::DataTableRowDialogRepositoryImp) bind DataTableRowDialogRepository::class - singleOf(::DocumentDialogRepositoryImp) bind DocumentDialogRepository::class + singleOf(::DocumentCreateUpdateRepositoryImp) bind DocumentCreateUpdateRepository::class singleOf(::DocumentListRepositoryImp) bind DocumentListRepository::class singleOf(::IndividualCollectionSheetDetailsRepositoryImp) bind IndividualCollectionSheetDetailsRepository::class singleOf(::NewIndividualCollectionSheetRepositoryImp) bind NewIndividualCollectionSheetRepository::class diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifierDialogRepository.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifierDialogRepository.kt deleted file mode 100644 index b81e8eb8e13..00000000000 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifierDialogRepository.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2024 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.core.data.repository - -import com.mifos.core.model.objects.noncoreobjects.IdentifierCreationResponse -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate - -/** - * Created by Aditya Gupta on 16/08/23. - */ -interface ClientIdentifierDialogRepository { - - suspend fun getClientIdentifierTemplate(clientId: Int): IdentifierTemplate - - suspend fun createClientIdentifier( - clientId: Int, - identifierPayload: IdentifierPayload, - ): IdentifierCreationResponse -} diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifiersRepository.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifiersRepository.kt index 4acbde2387a..b260a3283d6 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifiersRepository.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/ClientIdentifiersRepository.kt @@ -11,18 +11,33 @@ package com.mifos.core.data.repository import com.mifos.core.common.utils.DataState import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.network.model.DeleteClientsClientIdIdentifiersIdentifierIdResponse +import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate +import com.mifos.core.network.GenericResponse +import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow /** - * Created by Aditya Gupta on 08/08/23. + * Created by Arin Yadav on 12/09/2025. */ interface ClientIdentifiersRepository { - fun getClientIdentifiers(clientId: Int): Flow>> + fun getClientListIdentifiers(clientId: Long): Flow>> - suspend fun deleteClientIdentifier( - clientId: Int, - identifierId: Int, - ): DeleteClientsClientIdIdentifiersIdentifierIdResponse + fun getClientIdentifiers(clientId: Long, identifierId: Long): Flow> + + fun getClientIdentifierTemplate(clientId: Long): Flow> + + suspend fun deleteClientIdentifier(clientId: Long, identifierId: Long): GenericResponse + + suspend fun createClientIdentifier( + clientId: Long, + identifierPayload: IdentifierPayload, + ): HttpResponse + + suspend fun updateClientIdentifier( + clientId: Long, + identifierId: Long, + identifierPayload: IdentifierPayload, + ): GenericResponse } diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/DocumentDialogRepository.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/DocumentCreateUpdateRepository.kt similarity index 95% rename from core/data/src/commonMain/kotlin/com/mifos/core/data/repository/DocumentDialogRepository.kt rename to core/data/src/commonMain/kotlin/com/mifos/core/data/repository/DocumentCreateUpdateRepository.kt index dca682ffb4d..0319dde6958 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/DocumentDialogRepository.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/repository/DocumentCreateUpdateRepository.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.Flow /** * Created by Aditya Gupta on 16/08/23. */ -interface DocumentDialogRepository { +interface DocumentCreateUpdateRepository { suspend fun createDocument( entityType: String, diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifierDialogRepositoryImp.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifierDialogRepositoryImp.kt deleted file mode 100644 index 045eed9ce6c..00000000000 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifierDialogRepositoryImp.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2024 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.core.data.repositoryImp - -import com.mifos.core.data.repository.ClientIdentifierDialogRepository -import com.mifos.core.model.objects.noncoreobjects.IdentifierCreationResponse -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate -import com.mifos.core.network.datamanager.DataManagerClient - -/** - * Created by Aditya Gupta on 16/08/23. - */ -class ClientIdentifierDialogRepositoryImp( - private val dataManagerClient: DataManagerClient, -) : ClientIdentifierDialogRepository { - - override suspend fun getClientIdentifierTemplate(clientId: Int): IdentifierTemplate { - return dataManagerClient.getClientIdentifierTemplate(clientId) - } - - override suspend fun createClientIdentifier( - clientId: Int, - identifierPayload: IdentifierPayload, - ): IdentifierCreationResponse { - return dataManagerClient.createClientIdentifier(clientId, identifierPayload) - } -} diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifiersRepositoryImp.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifiersRepositoryImp.kt index f6d605a875b..184ef3e0f40 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifiersRepositoryImp.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/ClientIdentifiersRepositoryImp.kt @@ -13,25 +13,48 @@ import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.asDataStateFlow import com.mifos.core.data.repository.ClientIdentifiersRepository import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.network.datamanager.DataManagerClient -import com.mifos.core.network.model.DeleteClientsClientIdIdentifiersIdentifierIdResponse +import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate +import com.mifos.core.network.GenericResponse +import com.mifos.core.network.datamanager.DataManagerIdentifiers +import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow /** - * Created by Aditya Gupta on 08/08/23. + * Created by Arin Yadav on 12/09/2025. */ class ClientIdentifiersRepositoryImp( - private val dataManagerClient: DataManagerClient, + private val dataManagerIdentifiers: DataManagerIdentifiers, ) : ClientIdentifiersRepository { - override fun getClientIdentifiers(clientId: Int): Flow>> { - return dataManagerClient.getClientIdentifiers(clientId).asDataStateFlow() + override fun getClientListIdentifiers(clientId: Long): Flow>> { + return dataManagerIdentifiers.getClientListIdentifiers(clientId).asDataStateFlow() } - override suspend fun deleteClientIdentifier( - clientId: Int, - identifierId: Int, - ): DeleteClientsClientIdIdentifiersIdentifierIdResponse { - return dataManagerClient.deleteClientIdentifier(clientId, identifierId) + override fun getClientIdentifiers(clientId: Long, identifierId: Long): Flow> { + return dataManagerIdentifiers.getClientIdentifiers(clientId, identifierId).asDataStateFlow() + } + + override fun getClientIdentifierTemplate(clientId: Long): Flow> { + return dataManagerIdentifiers.getClientIdentifierTemplate(clientId).asDataStateFlow() + } + + override suspend fun deleteClientIdentifier(clientId: Long, identifierId: Long): GenericResponse { + return dataManagerIdentifiers.deleteClientIdentifier(clientId, identifierId) + } + + override suspend fun createClientIdentifier( + clientId: Long, + identifierPayload: IdentifierPayload, + ): HttpResponse { + return dataManagerIdentifiers.createClientIdentifier(clientId, identifierPayload) + } + + override suspend fun updateClientIdentifier( + clientId: Long, + identifierId: Long, + identifierPayload: IdentifierPayload, + ): GenericResponse { + return dataManagerIdentifiers.updateClientIdentifier(clientId, identifierId, identifierPayload) } } diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/DocumentDialogRepositoryImp.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/DocumentCreateUpdateRepositoryImp.kt similarity index 91% rename from core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/DocumentDialogRepositoryImp.kt rename to core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/DocumentCreateUpdateRepositoryImp.kt index 7701c09632f..aec566c5acd 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/DocumentDialogRepositoryImp.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/repositoryImp/DocumentCreateUpdateRepositoryImp.kt @@ -11,7 +11,7 @@ package com.mifos.core.data.repositoryImp import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.asDataStateFlow -import com.mifos.core.data.repository.DocumentDialogRepository +import com.mifos.core.data.repository.DocumentCreateUpdateRepository import com.mifos.core.network.GenericResponse import com.mifos.core.network.datamanager.DataManagerDocument import io.ktor.client.request.forms.MultiPartFormDataContent @@ -21,9 +21,9 @@ import kotlinx.coroutines.flow.flow /** * Created by Aditya Gupta on 16/08/23. */ -class DocumentDialogRepositoryImp( +class DocumentCreateUpdateRepositoryImp( private val dataManagerDocument: DataManagerDocument, -) : DocumentDialogRepository { +) : DocumentCreateUpdateRepository { override suspend fun createDocument( entityType: String, diff --git a/core/data/src/commonMain/kotlin/com/mifos/core/data/util/ErrorHandling.kt b/core/data/src/commonMain/kotlin/com/mifos/core/data/util/ErrorHandling.kt index da41f92f9e4..3fd9512a154 100644 --- a/core/data/src/commonMain/kotlin/com/mifos/core/data/util/ErrorHandling.kt +++ b/core/data/src/commonMain/kotlin/com/mifos/core/data/util/ErrorHandling.kt @@ -24,8 +24,13 @@ suspend fun extractErrorMessage(response: HttpResponse): String { val errorResponse = json.decodeFromString(responseText) errorResponse.errors.firstOrNull()?.defaultUserMessage ?: errorResponse.defaultUserMessage - ?: "Unknown error" + ?: Error.MSG_NOT_FOUND } catch (e: Exception) { - "Failed to parse error response" + Error.FAILED_TO_PARSE_ERROR_RESPONSE } } + +data object Error { + const val MSG_NOT_FOUND = "Message Not Found" + const val FAILED_TO_PARSE_ERROR_RESPONSE = "Failed to parse error response" +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt index a80da488744..d4f95d92002 100644 --- a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt @@ -212,8 +212,14 @@ fun MifosTextButton( modifier = modifier.height(48.dp), enabled = enabled, colors = ButtonDefaults.textButtonColors( - contentColor = AppColors.customWhite, containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = .12f, + ), + disabledContentColor = MaterialTheme.colorScheme.onSurface.copy( + .5f, + ), ), content = content, shape = DesignToken.shapes.medium, diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt index 3c038e2f6e6..22e4836a9fa 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/di/UseCaseModule.kt @@ -43,7 +43,6 @@ import com.mifos.core.domain.useCases.GetCentersInOfficeUseCase import com.mifos.core.domain.useCases.GetCheckerInboxBadgesUseCase import com.mifos.core.domain.useCases.GetCheckerTasksUseCase import com.mifos.core.domain.useCases.GetClientDetailsUseCase -import com.mifos.core.domain.useCases.GetClientIdentifierTemplateUseCase import com.mifos.core.domain.useCases.GetClientPinpointLocationsUseCase import com.mifos.core.domain.useCases.GetClientSavingsAccountTemplateByProductUseCase import com.mifos.core.domain.useCases.GetClientTemplateUseCase @@ -78,6 +77,7 @@ import com.mifos.core.domain.useCases.SaveIndividualCollectionSheetUseCase import com.mifos.core.domain.useCases.ServerConfigValidatorUseCase import com.mifos.core.domain.useCases.SubmitCollectionSheetUseCase import com.mifos.core.domain.useCases.SubmitProductiveSheetUseCase +import com.mifos.core.domain.useCases.UpdateClientIdentifierUseCase import com.mifos.core.domain.useCases.UpdateClientPinpointUseCase import com.mifos.core.domain.useCases.UpdateNoteUseCase import com.mifos.core.domain.useCases.UpdateSignatureUseCase @@ -125,7 +125,7 @@ val UseCaseModule = module { factoryOf(::GetCheckerInboxBadgesUseCase) factoryOf(::GetCheckerTasksUseCase) factoryOf(::GetClientDetailsUseCase) - factoryOf(::GetClientIdentifierTemplateUseCase) + factoryOf(::UpdateClientIdentifierUseCase) factoryOf(::GetClientPinpointLocationsUseCase) factoryOf(::GetClientSavingsAccountTemplateByProductUseCase) factoryOf(::GetDataTableInfoUseCase) diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateClientIdentifierUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateClientIdentifierUseCase.kt index c9075941ef0..0a58fcef2f0 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateClientIdentifierUseCase.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/CreateClientIdentifierUseCase.kt @@ -11,20 +11,19 @@ package com.mifos.core.domain.useCases import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.asDataStateFlow -import com.mifos.core.data.repository.ClientIdentifierDialogRepository -import com.mifos.core.model.objects.noncoreobjects.IdentifierCreationResponse +import com.mifos.core.data.repository.ClientIdentifiersRepository import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class CreateClientIdentifierUseCase( - private val repository: ClientIdentifierDialogRepository, + private val repository: ClientIdentifiersRepository, ) { - operator fun invoke( - clientId: Int, + clientId: Long, identifierPayload: IdentifierPayload, - ): Flow> = flow { + ): Flow> = flow { emit(repository.createClientIdentifier(clientId, identifierPayload)) }.asDataStateFlow() } diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteIdentifierUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteIdentifierUseCase.kt index 5d25146b076..9518a2d52da 100644 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteIdentifierUseCase.kt +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteIdentifierUseCase.kt @@ -12,7 +12,7 @@ package com.mifos.core.domain.useCases import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.asDataStateFlow import com.mifos.core.data.repository.ClientIdentifiersRepository -import com.mifos.core.network.model.DeleteClientsClientIdIdentifiersIdentifierIdResponse +import com.mifos.core.network.GenericResponse import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -21,9 +21,9 @@ class DeleteIdentifierUseCase( ) { operator fun invoke( - clientId: Int, - identifierId: Int, - ): Flow> = flow { + clientId: Long, + identifierId: Long, + ): Flow> = flow { emit(repository.deleteClientIdentifier(clientId = clientId, identifierId = identifierId)) }.asDataStateFlow() } diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientIdentifierTemplateUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientIdentifierTemplateUseCase.kt deleted file mode 100644 index e6c806d3b89..00000000000 --- a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/GetClientIdentifierTemplateUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 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.core.domain.useCases - -import com.mifos.core.common.utils.DataState -import com.mifos.core.common.utils.asDataStateFlow -import com.mifos.core.data.repository.ClientIdentifierDialogRepository -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -class GetClientIdentifierTemplateUseCase( - private val repository: ClientIdentifierDialogRepository, -) { - - operator fun invoke(clientId: Int): Flow> = flow { - emit(repository.getClientIdentifierTemplate(clientId)) - }.asDataStateFlow() -} diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateClientIdentifierUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateClientIdentifierUseCase.kt new file mode 100644 index 00000000000..734dd5f15b1 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateClientIdentifierUseCase.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.core.domain.useCases + +import com.mifos.core.common.utils.DataState +import com.mifos.core.common.utils.asDataStateFlow +import com.mifos.core.data.repository.ClientIdentifiersRepository +import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import com.mifos.core.network.GenericResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class UpdateClientIdentifierUseCase( + private val repository: ClientIdentifiersRepository, +) { + + operator fun invoke( + clientId: Long, + identifierId: Long, + identifierPayload: IdentifierPayload, + ): Flow> = flow { + emit( + repository.updateClientIdentifier( + clientId = clientId, + identifierId = identifierId, + identifierPayload = identifierPayload, + ), + ) + }.asDataStateFlow() +} diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/error/MifosError.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/error/MifosError.kt index eac46822114..111735bca92 100644 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/error/MifosError.kt +++ b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/error/MifosError.kt @@ -16,11 +16,11 @@ import kotlinx.serialization.Serializable @Parcelize @Serializable data class MifosError( - var developerMessage: String = "", + var developerMessage: String? = null, - var httpStatusCode: String = "", + var httpStatusCode: String? = null, - var defaultUserMessage: String = "", + var defaultUserMessage: String? = null, var userMessageGlobalisationCode: String? = null, diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/Identifier.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/Identifier.kt index 8b5a45b821d..5dd8b1908da 100644 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/Identifier.kt +++ b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/Identifier.kt @@ -11,11 +11,13 @@ package com.mifos.core.model.objects.noncoreobjects import com.mifos.core.model.utils.Parcelable import com.mifos.core.model.utils.Parcelize +import kotlinx.serialization.Serializable /** * Created by ishankhanna on 03/07/14. */ @Parcelize +@Serializable data class Identifier( var id: Int? = null, diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierCreationResponse.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierCreationResponse.kt deleted file mode 100644 index b5f96295708..00000000000 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierCreationResponse.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 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.core.model.objects.noncoreobjects - -import com.mifos.core.model.utils.Parcelable -import com.mifos.core.model.utils.Parcelize -import kotlinx.serialization.Serializable - -/** - * Created by Tarun on 07-08-17. - */ - -@Serializable -@Parcelize -data class IdentifierCreationResponse( - var clientId: Int = 0, - - var officeId: Int = 0, - - var resourceId: Int = 0, -) : Parcelable diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierTemplate.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierTemplate.kt index 6d9247ddddb..eec3a595eec 100644 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierTemplate.kt +++ b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/noncoreobjects/IdentifierTemplate.kt @@ -19,5 +19,8 @@ import kotlinx.serialization.Serializable @Parcelize @Serializable class IdentifierTemplate( + + @Serializable var allowedDocumentTypes: List? = emptyList(), + ) : Parcelable diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/apis/ClientIdentifierApi.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/apis/ClientIdentifierApi.kt index 4f706e15552..bb13aab7ee5 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/apis/ClientIdentifierApi.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/apis/ClientIdentifierApi.kt @@ -9,52 +9,109 @@ */ package com.mifos.core.network.apis -import com.mifos.core.network.model.DeleteClientsClientIdIdentifiersIdentifierIdResponse -import com.mifos.core.network.model.GetClientsClientIdIdentifiersResponse -import com.mifos.core.network.model.GetClientsClientIdIdentifiersTemplateResponse +import com.mifos.core.model.objects.noncoreobjects.Identifier +import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate +import com.mifos.core.network.GenericResponse +import com.mifos.room.basemodel.APIEndPoint +import de.jensklingenberg.ktorfit.http.Body import de.jensklingenberg.ktorfit.http.DELETE import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.POST +import de.jensklingenberg.ktorfit.http.PUT import de.jensklingenberg.ktorfit.http.Path +import io.ktor.client.statement.HttpResponse import kotlinx.coroutines.flow.Flow interface ClientIdentifierApi { + /** - * List all Identifiers for a Client - * Example Requests: clients/1/identifiers clients/1/identifiers?fields=documentKey,documentType,description - * Responses: - * - 200: OK + * Fetches the list of identifiers for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers * - * @param clientId clientId - * @return [kotlin.collections.List] + * @param clientId The unique ID of the client. + * @return [Flow] emitting a list of [Identifier]s for the specified client. */ - @GET("clients/{clientId}/identifiers") - fun retrieveAllClientIdentifiers(@Path("clientId") clientId: Long): Flow> + @GET(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS) + fun getClientListIdentifiers(@Path("clientId") clientId: Long): Flow> /** - * Retrieve Client Identifier Details Template - * This is a convenience resource useful for building maintenance user interface screens for client applications. The template data returned consists of any or all of: Field Defaults Allowed description Lists Example Request: clients/1/identifiers/template - * Responses: - * - 200: OK + * Retrieves a specific client identifier. * - * @param clientId clientId - * @return [GetClientsClientIdIdentifiersTemplateResponse] + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/{identifierId} + * + * @param clientId The unique ID of the client. + * @param identifierId The unique ID of the identifier. + * @return [Flow] emitting the [Identifier] object. */ - @GET("clients/{clientId}/identifiers/template") - suspend fun newClientIdentifierDetails(@Path("clientId") clientId: Long): GetClientsClientIdIdentifiersTemplateResponse + @GET(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS + "/{identifierId}") + fun getClientIdentifiers( + @Path("clientId") clientId: Long, + @Path("identifierId") identifierId: Long, + ): Flow /** - * Delete a Client Identifier - * Deletes a Client Identifier - * Responses: - * - 200: OK - * - * @param clientId clientId - * @param identifierId identifierId - * @return [DeleteClientsClientIdIdentifiersIdentifierIdResponse] + * Fetches the client identifier template for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/template + * + * @param clientId The unique ID of the client. + * @return [Flow] emitting the [IdentifierTemplate] for the specified client. */ - @DELETE("clients/{clientId}/identifiers/{identifierId}") + @GET(APIEndPoint.CLIENTS + "/{clientId}/identifiers/template") + fun getClientIdentifierTemplate(@Path("clientId") clientId: Long): Flow + + /** + * Deletes a client identifier for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/{identifierId} + * + * @param clientId The unique ID of the client. + * @param identifierId The unique ID of the identifier to be deleted. + * @return [GenericResponse] indicating the result of the delete operation. + */ + @DELETE(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS + "/{identifierId}") suspend fun deleteClientIdentifier( @Path("clientId") clientId: Long, @Path("identifierId") identifierId: Long, - ): DeleteClientsClientIdIdentifiersIdentifierIdResponse + ): GenericResponse + + /** + * Creates a new identifier for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers + * + * @param clientId The unique ID of the client. + * @param identifierPayload The payload containing identifier details. + * @return [GenericResponse] indicating the result of the create operation. + */ + @POST(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS) + suspend fun createClientIdentifier( + @Path("clientId") clientId: Long, + @Body identifierPayload: IdentifierPayload, + ): HttpResponse + + /** + * Updates an existing client identifier for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/{identifierId} + * + * @param clientId The unique ID of the client. + * @param identifierId The unique ID of the identifier to be updated. + * @param identifierPayload The updated payload for the identifier. + * @return [GenericResponse] indicating the result of the update operation. + */ + @PUT(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS + "/{identifierId}") + suspend fun updateClientIdentifier( + @Path("clientId") clientId: Long, + @Path("identifierId") identifierId: Long, + @Body identifierPayload: IdentifierPayload, + ): GenericResponse } 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 bcbb159f2da..d3b1ece556b 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 @@ -22,18 +22,11 @@ import com.mifos.core.model.objects.clients.ClientCloseRequest import com.mifos.core.model.objects.clients.CollateralPayload import com.mifos.core.model.objects.clients.ProposeTransferRequest import com.mifos.core.model.objects.clients.UpdateSavingsAccountRequest -import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.model.objects.noncoreobjects.IdentifierCreationResponse -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate import com.mifos.core.network.BaseApiManager import com.mifos.core.network.mappers.clients.GetClientResponseMapper import com.mifos.core.network.mappers.clients.GetClientsClientIdAccountMapper -import com.mifos.core.network.mappers.clients.GetIdentifiersTemplateMapper -import com.mifos.core.network.mappers.clients.IdentifierMapper import com.mifos.core.network.model.ClientCloseTemplateResponse import com.mifos.core.network.model.CollateralItem -import com.mifos.core.network.model.DeleteClientsClientIdIdentifiersIdentifierIdResponse import com.mifos.core.network.model.PinpointLocationActionResponse import com.mifos.core.network.model.PostClientAddressRequest import com.mifos.core.network.model.PostClientAddressResponse @@ -351,61 +344,6 @@ class DataManagerClient( clientDatabaseHelper.updateDatabaseClientPayload(clientPayload) } - /** - * This Method is for fetching the Client identifier from the REST API. - * - * @param clientId Client Id - * @return List - */ - fun getClientIdentifiers(clientId: Int): Flow> { - return mBaseApiManager.clientIdentifiersApi.retrieveAllClientIdentifiers(clientId.toLong()) - .map { responseList -> - responseList.map(IdentifierMapper::mapFromEntity) - } - } - - /** - * This Method is, for creating the Client Identifier. - * - * @param clientId Client Id - * @param identifierPayload IdentifierPayload - * @return IdentifierCreationResponse - */ - suspend fun createClientIdentifier( - clientId: Int, - identifierPayload: IdentifierPayload, - ): IdentifierCreationResponse { - return mBaseApiManager.clientService.createClientIdentifier(clientId, identifierPayload) - } - - /** - * This Method is, for fetching the Client Identifier Template. - * - * @param clientId Client Id - * @return IdentifierTemplate - */ - suspend fun getClientIdentifierTemplate(clientId: Int): IdentifierTemplate { - return mBaseApiManager.clientIdentifiersApi.newClientIdentifierDetails(clientId.toLong()) - .let(GetIdentifiersTemplateMapper::mapFromEntity) - } - - /** - * This Method is for deleting the Client Identifier. - * - * @param clientId Client Id - * @param identifierId Identifier Id - * @return GenericResponse - */ - suspend fun deleteClientIdentifier( - clientId: Int, - identifierId: Int, - ): DeleteClientsClientIdIdentifiersIdentifierIdResponse { - return mBaseApiManager.clientIdentifiersApi.deleteClientIdentifier( - clientId.toLong(), - identifierId.toLong(), - ) - } - /** * This Method is fetching the Client Pinpoint location from the DataTable * "client_pinpoint_location" diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerIdentifiers.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerIdentifiers.kt new file mode 100644 index 00000000000..8cbb1744962 --- /dev/null +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/datamanager/DataManagerIdentifiers.kt @@ -0,0 +1,116 @@ +/* + * 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.core.network.datamanager + +import com.mifos.core.model.objects.noncoreobjects.Identifier +import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate +import com.mifos.core.network.BaseApiManager +import com.mifos.core.network.GenericResponse +import io.ktor.client.statement.HttpResponse +import kotlinx.coroutines.flow.Flow + +/** + * Created by Arin Yadav on 12/09/25. + */ +class DataManagerIdentifiers( + val mBaseApiManager: BaseApiManager, +) { + + /** + * Fetches the list of identifiers for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers + * + * @param clientId The unique ID of the client. + * @return [Flow] emitting a list of [Identifier]s for the specified client. + */ + fun getClientListIdentifiers(clientId: Long): Flow> { + return mBaseApiManager.clientIdentifiersApi.getClientListIdentifiers(clientId) + } + + /** + * Retrieves a specific client identifier. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/{identifierId} + * + * @param clientId The unique ID of the client. + * @param identifierId The unique ID of the identifier. + * @return [Flow] emitting the [Identifier] object. + */ + fun getClientIdentifiers(clientId: Long, identifierId: Long): Flow { + return mBaseApiManager.clientIdentifiersApi.getClientIdentifiers(clientId, identifierId) + } + + /** + * Fetches the client identifier template for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/template + * + * @param clientId The unique ID of the client. + * @return [Flow] emitting the [IdentifierTemplate] for the specified client. + */ + fun getClientIdentifierTemplate(clientId: Long): Flow { + return mBaseApiManager.clientIdentifiersApi.getClientIdentifierTemplate(clientId) + } + + /** + * Deletes a client identifier for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/{identifierId} + * + * @param clientId The unique ID of the client. + * @param identifierId The unique ID of the identifier to be deleted. + * @return [GenericResponse] indicating the result of the delete operation. + */ + suspend fun deleteClientIdentifier(clientId: Long, identifierId: Long): GenericResponse { + return mBaseApiManager.clientIdentifiersApi.deleteClientIdentifier(clientId, identifierId) + } + + /** + * Creates a new identifier for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers + * + * @param clientId The unique ID of the client. + * @param identifierPayload The payload containing identifier details. + * @return [GenericResponse] indicating the result of the create operation. + */ + suspend fun createClientIdentifier( + clientId: Long, + identifierPayload: IdentifierPayload, + ): HttpResponse { + return mBaseApiManager.clientIdentifiersApi.createClientIdentifier(clientId, identifierPayload) + } + + /** + * Updates an existing client identifier for a given client. + * + * REST END POINT: + * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/{identifierId} + * + * @param clientId The unique ID of the client. + * @param identifierId The unique ID of the identifier to be updated. + * @param identifierPayload The updated payload for the identifier. + * @return [GenericResponse] indicating the result of the update operation. + */ + suspend fun updateClientIdentifier( + clientId: Long, + identifierId: Long, + identifierPayload: IdentifierPayload, + ): GenericResponse { + return mBaseApiManager.clientIdentifiersApi.updateClientIdentifier(clientId, identifierId, identifierPayload) + } +} diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/di/DataMangerModule.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/di/DataMangerModule.kt index e3466bfee22..b3c48a79dfa 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/di/DataMangerModule.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/di/DataMangerModule.kt @@ -20,6 +20,7 @@ import com.mifos.core.network.datamanager.DataManagerCollectionSheet import com.mifos.core.network.datamanager.DataManagerDataTable import com.mifos.core.network.datamanager.DataManagerDocument import com.mifos.core.network.datamanager.DataManagerGroups +import com.mifos.core.network.datamanager.DataManagerIdentifiers import com.mifos.core.network.datamanager.DataManagerLoan import com.mifos.core.network.datamanager.DataManagerNote import com.mifos.core.network.datamanager.DataManagerOffices @@ -49,4 +50,5 @@ val DataManagerModule = module { single { DataManagerSearch(get()) } single { DataManagerStaff(get(), get(), get()) } single { DataManagerSurveys(get(), get(), get()) } + single { DataManagerIdentifiers(get()) } } diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetIdentifiersTemplateMapper.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetIdentifiersTemplateMapper.kt deleted file mode 100644 index 3abb5d6c68e..00000000000 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/GetIdentifiersTemplateMapper.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2024 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.core.network.mappers.clients - -import com.mifos.core.model.objects.noncoreobjects.DocumentType -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate -import com.mifos.core.network.data.AbstractMapper -import com.mifos.core.network.model.GetClientsAllowedDocumentTypes -import com.mifos.core.network.model.GetClientsClientIdIdentifiersTemplateResponse - -/** - * Created by Aditya Gupta on 30/08/23. - */ - -object GetIdentifiersTemplateMapper : - AbstractMapper() { - - override fun mapFromEntity(entity: GetClientsClientIdIdentifiersTemplateResponse): IdentifierTemplate { - return IdentifierTemplate().apply { - allowedDocumentTypes = entity.allowedDocumentTypes?.map { - DocumentType().apply { - id = it.id?.toInt() - name = it.name - position = it.position - } - } - } - } - - override fun mapToEntity(domainModel: IdentifierTemplate): GetClientsClientIdIdentifiersTemplateResponse { - return GetClientsClientIdIdentifiersTemplateResponse( - allowedDocumentTypes = domainModel.allowedDocumentTypes?.map { - GetClientsAllowedDocumentTypes( - id = it.id?.toLong(), - name = it.name, - position = it.position, - ) - }?.toSet(), - ) - } -} diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/IdentifierMapper.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/IdentifierMapper.kt deleted file mode 100644 index 7e16840a3d9..00000000000 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/mappers/clients/IdentifierMapper.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2024 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.core.network.mappers.clients - -import com.mifos.core.model.objects.noncoreobjects.DocumentType -import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.network.data.AbstractMapper -import com.mifos.core.network.model.GetClientsClientIdIdentifiersResponse -import com.mifos.core.network.model.GetClientsDocumentType - -/** - * Created by Aditya Gupta on 30/08/23. - */ -object IdentifierMapper : AbstractMapper() { - override fun mapFromEntity(entity: GetClientsClientIdIdentifiersResponse): Identifier { - return Identifier( - id = entity.id?.toInt(), - clientId = entity.clientId?.toInt(), - documentKey = entity.documentKey, - description = entity.description, - documentType = entity.documentType?.let { - DocumentType( - id = it.id?.toInt(), - name = it.name, - ) - }, - status = entity.status, - ) - } - - override fun mapToEntity(domainModel: Identifier): GetClientsClientIdIdentifiersResponse { - return GetClientsClientIdIdentifiersResponse( - id = domainModel.id?.toLong(), - clientId = domainModel.clientId?.toLong(), - documentKey = domainModel.documentKey, - description = domainModel.description, - documentType = domainModel.documentType?.let { - GetClientsDocumentType( - id = it.id?.toLong(), - name = it.name, - ) - }, - status = domainModel.status, - ) - } -} diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/DeleteClientsClientIdIdentifiersIdentifierIdResponse.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/model/DeleteClientsClientIdIdentifiersIdentifierIdResponse.kt deleted file mode 100644 index 136769087e1..00000000000 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/DeleteClientsClientIdIdentifiersIdentifierIdResponse.kt +++ /dev/null @@ -1,31 +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.core.network.model - -import kotlinx.serialization.Serializable - -/** - * DeleteClientsClientIdIdentifiersIdentifierIdResponse - * - * @param clientId - * @param officeId - * @param resourceId - */ - -@Serializable -data class DeleteClientsClientIdIdentifiersIdentifierIdResponse( - - val clientId: Long? = null, - - val officeId: Long? = null, - - val resourceId: Long? = null, - -) diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsClientIdIdentifiersResponse.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsClientIdIdentifiersResponse.kt deleted file mode 100644 index 9886e0ad0da..00000000000 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsClientIdIdentifiersResponse.kt +++ /dev/null @@ -1,38 +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.core.network.model - -import kotlinx.serialization.Serializable - -/** - * GetClientsClientIdIdentifiersResponse - * - * @param clientId - * @param description - * @param documentKey - * @param documentType - * @param id - */ - -@Serializable -data class GetClientsClientIdIdentifiersResponse( - - val clientId: Long? = null, - - val description: String? = null, - - val documentKey: String? = null, - - val documentType: GetClientsDocumentType? = null, - - val id: Long? = null, - - val status: String? = null, -) diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/services/ClientService.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/services/ClientService.kt index 744236d6a8c..80732cef587 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/services/ClientService.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/services/ClientService.kt @@ -19,10 +19,6 @@ import com.mifos.core.model.objects.clients.ClientCloseRequest import com.mifos.core.model.objects.clients.CollateralPayload import com.mifos.core.model.objects.clients.ProposeTransferRequest import com.mifos.core.model.objects.clients.UpdateSavingsAccountRequest -import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.model.objects.noncoreobjects.IdentifierCreationResponse -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate import com.mifos.core.network.GenericResponse import com.mifos.core.network.model.ClientCloseTemplateResponse import com.mifos.core.network.model.CollateralItem @@ -104,59 +100,6 @@ interface ClientService { @GET(APIEndPoint.CLIENTS + "/{clientId}/accounts") fun getClientAccounts(@Path("clientId") clientId: Int): Flow - /** - * This Service is for fetching the List of Identifiers. - * REST END POINT: - * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers - * - * @param clientId Client Id - * @return List - */ - @GET(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS) - fun getClientIdentifiers(@Path("clientId") clientId: Int): Flow> - - /** - * This Service is for Creating the Client Identifier. - * REST END POINT: - * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers - * - * @param clientId Client Id - * @param identifierPayload IdentifierPayload - * @return IdentifierCreationResponse - */ - @POST(APIEndPoint.CLIENTS + "/{clientId}/identifiers") - suspend fun createClientIdentifier( - @Path("clientId") clientId: Int, - @Body identifierPayload: IdentifierPayload, - ): IdentifierCreationResponse - - /** - * This Service is for the Fetching the Client Identifier Template. - * REST END POINT: - * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/template - * - * @param clientId Client Id - * @return IdentifierTemplate - */ - @GET(APIEndPoint.CLIENTS + "/{clientId}/identifiers/template") - fun getClientIdentifierTemplate(@Path("clientId") clientId: Int): Flow - - /** - * This Service for Deleting the Client Identifier. - * REST END POINT: - * https://demo.openmf.org/fineract-provider/api/v1/clients/{clientId}/identifiers/ - * {identifierId} - * - * @param clientId Client Id - * @param identifierId Identifier Id - * @return GenericResponse - */ - @DELETE(APIEndPoint.CLIENTS + "/{clientId}/" + APIEndPoint.IDENTIFIERS + "/{identifierId}") - fun deleteClientIdentifier( - @Path("clientId") clientId: Int, - @Path("identifierId") identifierId: Int, - ): Flow - /** * This is the service for fetching the client pinpoint locations from the dataTable * "client_pinpoint_location". This DataTable entries are diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosProgressIndicator.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosProgressIndicator.kt index 11118146a3f..93f9f2b6020 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosProgressIndicator.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosProgressIndicator.kt @@ -52,7 +52,8 @@ fun MifosProgressIndicator( ) Box( - modifier = modifier, + modifier = modifier + .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center, ) { Image( diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosRowWithTextAndButton.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosRowWithTextAndButton.kt index eb502411759..17bd0150eaf 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosRowWithTextAndButton.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosRowWithTextAndButton.kt @@ -9,19 +9,27 @@ */ package com.mifos.core.ui.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import com.mifos.core.designsystem.component.MifosOutlinedButton import com.mifos.core.designsystem.theme.DesignToken import com.mifos.core.designsystem.theme.MifosTheme -import com.mifos.core.designsystem.theme.MifosTypography import org.jetbrains.compose.ui.tooling.preview.Preview @Composable @@ -32,33 +40,58 @@ fun MifosRowWithTextAndButton( text: String, modifier: Modifier = Modifier, ) { - MifosListingComponentOutline( - modifier = modifier, - content = { - Row( - Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = text, - style = MifosTypography.bodyLarge, - modifier = Modifier.weight(1f), - ) - Spacer(Modifier.width(DesignToken.padding.extraSmall)) - MifosOutlinedButton( - onClick = { onBtnClick() }, - text = { - Text( - text = btnText, - color = MaterialTheme.colorScheme.primary, - style = MifosTypography.labelMediumEmphasized, - ) - }, - enabled = btnEnabled, - ) - } - }, - ) + Row( + modifier = modifier + .fillMaxWidth() + .clip(shape = DesignToken.shapes.medium) + .border( + 1.dp, + color = MaterialTheme.colorScheme.secondaryContainer, + shape = DesignToken.shapes.medium, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + fontFamily = FontFamily.SansSerif, + modifier = Modifier.padding( + start = DesignToken.padding.large, + top = DesignToken.padding.large, + bottom = DesignToken.padding.large, + ) + .weight(.6f), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + MifosOutlinedButton( + onClick = { + onBtnClick() + }, + colors = ButtonDefaults.outlinedButtonColors( + containerColor = MaterialTheme.colorScheme.onPrimary, + contentColor = MaterialTheme.colorScheme.primary, + ), + shape = DesignToken.shapes.small, + border = BorderStroke( + 1.dp, + color = MaterialTheme.colorScheme.secondaryContainer, + ), + modifier = Modifier + .padding(end = DesignToken.padding.large) + .height(DesignToken.sizes.iconExtraLarge) + .wrapContentWidth(), + enabled = btnEnabled, + ) { + Text( + text = btnText, + style = MaterialTheme.typography.labelLarge, + fontFamily = FontFamily.SansSerif, + ) + } + } } @Composable diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosTwoButtonRow.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosTwoButtonRow.kt index b775ec3bf07..7b5ad4a9eab 100644 --- a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosTwoButtonRow.kt +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosTwoButtonRow.kt @@ -12,6 +12,7 @@ package com.mifos.core.ui.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon @@ -19,6 +20,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.mifos.core.designsystem.component.MifosOutlinedButton import com.mifos.core.designsystem.component.MifosTextButton import com.mifos.core.designsystem.icon.MifosIcons @@ -33,6 +35,7 @@ fun MifosTwoButtonRow( secondBtnText: String, onFirstBtnClick: () -> Unit, onSecondBtnClick: () -> Unit, + isButtonIconVisible: Boolean = true, isFirstButtonEnabled: Boolean = true, isSecondButtonEnabled: Boolean = true, modifier: Modifier = Modifier, @@ -43,12 +46,14 @@ fun MifosTwoButtonRow( onFirstBtnClick() }, leadingIcon = { - Icon( - imageVector = MifosIcons.ChevronLeft, - contentDescription = null, - modifier = Modifier.size(DesignToken.sizes.iconAverage), - tint = MaterialTheme.colorScheme.primary, - ) + if (isButtonIconVisible) { + Icon( + imageVector = MifosIcons.ChevronLeft, + contentDescription = null, + modifier = Modifier.size(DesignToken.sizes.iconAverage), + tint = MaterialTheme.colorScheme.primary, + ) + } }, text = { Text( @@ -57,7 +62,7 @@ fun MifosTwoButtonRow( style = MifosTypography.labelLarge, ) }, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).height(40.dp), enabled = isFirstButtonEnabled, ) Spacer(Modifier.padding(DesignToken.padding.small)) @@ -66,11 +71,13 @@ fun MifosTwoButtonRow( onSecondBtnClick() }, leadingIcon = { - Icon( - imageVector = MifosIcons.Check, - contentDescription = null, - modifier = Modifier.size(DesignToken.sizes.iconAverage), - ) + if (isButtonIconVisible) { + Icon( + imageVector = MifosIcons.Check, + contentDescription = null, + modifier = Modifier.size(DesignToken.sizes.iconAverage), + ) + } }, text = { Text( @@ -78,7 +85,7 @@ fun MifosTwoButtonRow( style = MifosTypography.labelLarge, ) }, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).height(40.dp), enabled = isSecondButtonEnabled, ) } diff --git a/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosViewPdf.kt b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosViewPdf.kt new file mode 100644 index 00000000000..c97610306a8 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosViewPdf.kt @@ -0,0 +1,66 @@ +/* + * 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.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImage +import com.mifos.core.designsystem.theme.DesignToken + +@Composable +fun MifosViewPdf( + bitmaps: List, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + itemsIndexed(bitmaps) { index, bmp -> + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.onPrimary), + ) { + AsyncImage( + model = bmp, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + ) + + Text( + text = "Page ${index + 1} of ${bitmaps.size}", + fontSize = 5.sp, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = DesignToken.padding.medium, bottom = DesignToken.padding.small), + ) + } + + if (bitmaps.size != 1) { + HorizontalDivider() + } + } + } +} diff --git a/feature/client/build.gradle.kts b/feature/client/build.gradle.kts index 91f6088782a..cc05154ff6a 100644 --- a/feature/client/build.gradle.kts +++ b/feature/client/build.gradle.kts @@ -55,6 +55,11 @@ kotlin { implementation(libs.play.services.location) implementation(libs.kotlinx.coroutines.play.services) } + + + desktopMain.dependencies { + implementation(libs.pdfbox) + } } } diff --git a/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PdfViewer.android.kt b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PdfViewer.android.kt new file mode 100644 index 00000000000..324e92f5b2d --- /dev/null +++ b/feature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/PdfViewer.android.kt @@ -0,0 +1,56 @@ +/* + * 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 android.graphics.Bitmap +import android.graphics.pdf.PdfRenderer +import android.os.ParcelFileDescriptor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.graphics.createBitmap +import com.mifos.core.ui.components.MifosViewPdf +import java.io.File +import java.io.FileOutputStream + +@Composable +actual fun PdfPreview(pdfBytes: ByteArray, modifier: Modifier) { + val context = LocalContext.current + val bitmaps = remember { mutableStateListOf() } + + LaunchedEffect(pdfBytes) { + bitmaps.clear() + + val tempFile = File.createTempFile("temp", ".pdf", context.cacheDir) + FileOutputStream(tempFile).use { it.write(pdfBytes) } + + val renderer = PdfRenderer( + ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_ONLY), + ) + + for (i in 0 until renderer.pageCount) { + val page = renderer.openPage(i) + + val scale = 3f + val bmp = createBitmap((page.width * scale).toInt(), (page.height * scale).toInt()) + + page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + page.close() + + bitmaps.add(bmp) + } + renderer.close() + } + + MifosViewPdf(bitmaps) +} diff --git a/feature/client/src/commonMain/composeResources/values/strings.xml b/feature/client/src/commonMain/composeResources/values/strings.xml index b396eb4d8bc..8fca3f4cb18 100644 --- a/feature/client/src/commonMain/composeResources/values/strings.xml +++ b/feature/client/src/commonMain/composeResources/values/strings.xml @@ -95,18 +95,6 @@ Failed To Create Charge No Charges Found - Failed To Load Identifiers - Failed To Create Identifier - Identifier Created Successfully - Create Identifier - Document Type - Status - Unique Id - Description - Required Field - Submit - Active - Failed To Add Signature Signature Should Not Be Empty Signature @@ -479,7 +467,6 @@ Error Retry Successfully deleted client identifier with id - Choose Application Type @@ -504,13 +491,34 @@ Draw More + Share Accounts An error occurred Failed to fetch share accounts Try again + Failed to get upcoming charges No more charges available! Charges Overview + + Add Identifiers + Document Type* + Status* + Document Key* + Description + Document Name* + Create New + Upload New + Next + Back + Add + View + Submit + No File Selected + Document not found + Update + Update Document + \ No newline at end of file diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/DocumentSelectAndUploadRepositoryImpl.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/DocumentSelectAndUploadRepositoryImpl.kt index ca23b259976..4df82bf8dee 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/DocumentSelectAndUploadRepositoryImpl.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/DocumentSelectAndUploadRepositoryImpl.kt @@ -15,7 +15,7 @@ import androidclient.feature.client.generated.resources.error_document_not_found import androidclient.feature.client.generated.resources.error_failed_to_get_document_type import com.mifos.core.common.utils.DataState import com.mifos.core.common.utils.FileKitUtil -import com.mifos.core.data.repository.DocumentDialogRepository +import com.mifos.core.data.repository.DocumentCreateUpdateRepository import com.mifos.core.data.repository.DocumentListRepository import com.mifos.feature.client.utils.createDocumentRequestBody import io.github.vinceglb.filekit.PlatformFile @@ -29,7 +29,7 @@ import org.jetbrains.compose.resources.getString class DocumentSelectAndUploadRepositoryImpl( private val documentsRepository: DocumentListRepository, - private val documentDialogRepository: DocumentDialogRepository, + private val documentDialogRepository: DocumentCreateUpdateRepository, ) : DocumentSelectAndUploadRepository { override val entityDocumentStateMutableStateFlow = MutableStateFlow(EntityDocumentState()) diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientAddDocuments/ClientAddDocumentsScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientAddDocuments/ClientAddDocumentsScreen.kt index 53fb3052eaa..81e06f2a417 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientAddDocuments/ClientAddDocumentsScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientAddDocuments/ClientAddDocumentsScreen.kt @@ -29,12 +29,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -44,9 +42,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController @@ -60,6 +56,7 @@ import com.mifos.core.designsystem.theme.MifosTypography import com.mifos.core.ui.components.MifosBreadcrumbNavBar import com.mifos.core.ui.components.MifosFilePickerBottomSheet import com.mifos.core.ui.components.MifosProgressIndicator +import com.mifos.core.ui.components.MifosRowWithTextAndButton import com.mifos.core.ui.util.EventsEffect import com.mifos.feature.client.EntityDocumentState import org.jetbrains.compose.resources.stringResource @@ -179,9 +176,25 @@ private fun ClientAddDocumentScaffold( .padding(bottom = DesignToken.padding.small), ) - AddViewFileAndFileNameRow( - state = state, - onAction = onAction, + MifosRowWithTextAndButton( + text = if (state.step == EntityDocumentState.Step.ADD) { + stringResource(Res.string.no_file_selected) + } else { + state.pickedDocumentName + }, + onBtnClick = { + if (state.step == EntityDocumentState.Step.ADD) { + onAction(ClientAddDocumentScreenAction.AddNewDocument) + } else { + onAction(ClientAddDocumentScreenAction.ViewDocument) + } + }, + btnText = if (state.step == EntityDocumentState.Step.ADD) { + stringResource(Res.string.action_add) + } else { + stringResource(Res.string.action_view) + }, + ) Spacer(Modifier.height(DesignToken.spacing.largeIncreased)) @@ -290,73 +303,3 @@ private fun ClientAddDocumentScaffold( } } } - -@Composable -private fun AddViewFileAndFileNameRow( - state: ClientAddDocumentScreenState, - onAction: (ClientAddDocumentScreenAction) -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(shape = DesignToken.shapes.medium) - .border( - 1.dp, - color = MaterialTheme.colorScheme.secondaryContainer, - shape = DesignToken.shapes.medium, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = if (state.step == EntityDocumentState.Step.ADD) { - stringResource(Res.string.no_file_selected) - } else { - state.pickedDocumentName - }, - style = MaterialTheme.typography.bodyLarge, - fontFamily = FontFamily.SansSerif, - modifier = Modifier.padding( - start = DesignToken.padding.large, - top = DesignToken.padding.large, - bottom = DesignToken.padding.large, - ) - .weight(.6f), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - - MifosOutlinedButton( - onClick = { - if (state.step == EntityDocumentState.Step.ADD) { - onAction(ClientAddDocumentScreenAction.AddNewDocument) - } else { - onAction(ClientAddDocumentScreenAction.ViewDocument) - } - }, - colors = ButtonDefaults.outlinedButtonColors( - containerColor = MaterialTheme.colorScheme.onPrimary, - contentColor = MaterialTheme.colorScheme.primary, - ), - shape = DesignToken.shapes.small, - border = BorderStroke( - 1.dp, - color = MaterialTheme.colorScheme.secondaryContainer, - ), - modifier = Modifier - .padding(end = DesignToken.padding.large) - .height(DesignToken.sizes.iconExtraLarge) - .wrapContentWidth(), - ) { - Text( - text = if (state.step == EntityDocumentState.Step.ADD) { - stringResource(Res.string.action_add) - } else { - stringResource(Res.string.action_view) - }, - style = MaterialTheme.typography.labelLarge, - fontFamily = FontFamily.SansSerif, - ) - } - } -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersScreen.kt deleted file mode 100644 index 65ce3e9d77e..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersScreen.kt +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright 2024 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 - */ -@file:OptIn(ExperimentalMaterial3Api::class) - -package com.mifos.feature.client.clientIdentifiers - -import androidclient.feature.client.generated.resources.Res -import androidclient.feature.client.generated.resources.feature_client_description -import androidclient.feature.client.generated.resources.feature_client_documents -import androidclient.feature.client.generated.resources.feature_client_failed_to_load_client_identifiers -import androidclient.feature.client.generated.resources.feature_client_id -import androidclient.feature.client.generated.resources.feature_client_identifier_deleted_successfully -import androidclient.feature.client.generated.resources.feature_client_identifiers -import androidclient.feature.client.generated.resources.feature_client_remove -import androidclient.feature.client.generated.resources.feature_client_there_is_no_identifier_to_show -import androidclient.feature.client.generated.resources.feature_client_type -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mifos.core.designsystem.component.MifosMenuDropDownItem -import com.mifos.core.designsystem.component.MifosScaffold -import com.mifos.core.designsystem.component.MifosSweetError -import com.mifos.core.designsystem.icon.MifosIcons -import com.mifos.core.designsystem.theme.identifierTextStyleDark -import com.mifos.core.designsystem.theme.identifierTextStyleLight -import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.core.ui.components.MifosEmptyUi -import com.mifos.core.ui.components.MifosProgressIndicator -import com.mifos.core.ui.util.DevicePreview -import com.mifos.feature.client.clientIdentifiersDialog.ClientIdentifierDialogUiState -import com.mifos.feature.client.clientIdentifiersDialog.ClientIdentifiersDialogScreen -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import org.jetbrains.compose.resources.getString -import org.jetbrains.compose.resources.stringResource -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 ClientIdentifiersScreen( - onBackPressed: () -> Unit, - onDocumentClicked: (Int) -> Unit, - viewModel: ClientIdentifiersViewModel = koinViewModel(), -) { - val clientIdentifiersUiState by viewModel.clientIdentifiersUiState.collectAsStateWithLifecycle() - val clientIdentifiersDialogUiState by viewModel.clientIdentifierDialogUiState.collectAsStateWithLifecycle() - val refreshState by viewModel.isRefreshing.collectAsStateWithLifecycle() - val showCreateDialog by viewModel.showCreateDialog.collectAsStateWithLifecycle() - - ClientIdentifiersScreen( - state = clientIdentifiersUiState, - dialogState = clientIdentifiersDialogUiState, - showCreateDialog = showCreateDialog, - onShowDialog = viewModel::showCreateIdentifierDialog, - onHideDialog = viewModel::hideCreateIdentifierDialog, - onBackPressed = onBackPressed, - onDeleteIdentifier = { identifierId -> - viewModel.deleteIdentifier(identifierId) - }, - onCreateIdentifier = { identifierPayload -> - viewModel.createClientIdentifier(identifierPayload) - }, - refreshState = refreshState, - onRefresh = viewModel::refreshIdentifiersList, - onRetry = viewModel::loadIdentifiers, - onDocumentClicked = onDocumentClicked, - events = viewModel.events, - ) -} - -@Composable -internal fun ClientIdentifiersScreen( - state: ClientIdentifiersUiState, - dialogState: ClientIdentifierDialogUiState, - showCreateDialog: Boolean, - onShowDialog: () -> Unit, - onHideDialog: () -> Unit, - onBackPressed: () -> Unit, - onDeleteIdentifier: (Int) -> Unit, - onCreateIdentifier: (IdentifierPayload) -> Unit, - refreshState: Boolean, - onRefresh: () -> Unit, - onRetry: () -> Unit, - onDocumentClicked: (Int) -> Unit, - events: Flow, -) { - val snackbarHostState = remember { SnackbarHostState() } - val pullToRefreshState = rememberPullToRefreshState() - - LaunchedEffect(events) { - events.collect { event -> - when (event) { - is ClientIdentifiersViewModel.ClientIdentifiersEvent.ShowMessage -> { - snackbarHostState.showSnackbar(message = getString(event.message)) - } - } - } - } - - if (showCreateDialog) { - ClientIdentifiersDialogScreen( - state = dialogState, - onDismiss = { - onHideDialog() - }, - onRetry = onRetry, - onCreateIdentifier = onCreateIdentifier, - ) - } - - MifosScaffold( - title = stringResource(Res.string.feature_client_identifiers), - onBackPressed = onBackPressed, - actions = { - IconButton( - onClick = { - onShowDialog() - }, - ) { - Icon( - imageVector = MifosIcons.Add, - contentDescription = null, - ) - } - }, - snackbarHostState = snackbarHostState, - ) { paddingValues -> - Column(modifier = Modifier.padding(paddingValues)) { - PullToRefreshBox( - state = pullToRefreshState, - onRefresh = onRefresh, - isRefreshing = refreshState, - ) { - when (state) { - is ClientIdentifiersUiState.ClientIdentifiers -> { - when (state.identifiers.isEmpty()) { - true -> { - MifosEmptyUi( - text = stringResource(Res.string.feature_client_there_is_no_identifier_to_show), - icon = MifosIcons.FileTask, - ) - } - - false -> ClientIdentifiersContent( - identifiers = state.identifiers, - onDeleteIdentifier = onDeleteIdentifier, - onDocumentClicked = onDocumentClicked, - ) - } - } - - is ClientIdentifiersUiState.Error -> MifosSweetError( - message = stringResource( - state.message, - ), - ) { - onRetry() - } - - is ClientIdentifiersUiState.IdentifierDeletedSuccessfully -> { - } - - is ClientIdentifiersUiState.Loading -> MifosProgressIndicator() - } - } - } - } -} - -@Composable -private fun ClientIdentifiersContent( - identifiers: List, - onDeleteIdentifier: (Int) -> Unit, - onDocumentClicked: (Int) -> Unit, -) { - LazyColumn { - items(identifiers) { identifier -> - ClientIdentifiersItem( - identifier = identifier, - onDeleteIdentifier = onDeleteIdentifier, - onDocumentClicked = onDocumentClicked, - ) - } - } -} - -@Composable -private fun ClientIdentifiersItem( - identifier: Identifier, - onDeleteIdentifier: (Int) -> Unit, - onDocumentClicked: (Int) -> Unit, -) { - var showMenu by remember { mutableStateOf(false) } - - ElevatedCard( - modifier = Modifier.padding(8.dp), - elevation = CardDefaults.elevatedCardElevation(0.dp), - colors = CardDefaults.elevatedCardColors(MaterialTheme.colorScheme.surfaceVariant), - onClick = {}, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Canvas( - modifier = Modifier - .width(16.dp) - .height(94.dp), - ) { - drawRect( - // TODO use lightGreen color - color = Color.Green, - size = Size(size.width, size.height), - ) - } - Column( - modifier = Modifier - .weight(3f), - ) { - MifosIdentifierDetailsText( - field = stringResource(Res.string.feature_client_id), - value = identifier.id.toString(), - ) - MifosIdentifierDetailsText( - field = stringResource(Res.string.feature_client_type), - value = identifier.documentType?.name ?: "-", - ) - MifosIdentifierDetailsText( - field = stringResource(Res.string.feature_client_description), - value = identifier.description ?: "-", - ) - } - IconButton(modifier = Modifier.weight(.5f), onClick = { showMenu = showMenu.not() }) { - Icon(imageVector = MifosIcons.MoreVert, contentDescription = null) - DropdownMenu( - modifier = Modifier.background(MaterialTheme.colorScheme.background), - expanded = showMenu, - onDismissRequest = { showMenu = false }, - ) { - MifosMenuDropDownItem( - option = stringResource(Res.string.feature_client_remove), - onClick = { - identifier.id?.let { onDeleteIdentifier(it) } - showMenu = false - }, - ) - MifosMenuDropDownItem( - option = stringResource(Res.string.feature_client_documents), - onClick = { - identifier.id?.let { onDocumentClicked(it) } - showMenu = false - }, - ) - } - } - } - } -} - -@Composable -private fun MifosIdentifierDetailsText(field: String, value: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - text = field, - style = identifierTextStyleDark, - ) - Text( - modifier = Modifier.weight(1f), - text = value, - style = identifierTextStyleLight, - ) - } -} - -private class ClientIdentifiersUiStateProvider : - PreviewParameterProvider { - - override val values: Sequence - get() = sequenceOf( - ClientIdentifiersUiState.Loading, - ClientIdentifiersUiState.Error(Res.string.feature_client_failed_to_load_client_identifiers), - ClientIdentifiersUiState.IdentifierDeletedSuccessfully(Res.string.feature_client_identifier_deleted_successfully), - ClientIdentifiersUiState.ClientIdentifiers(sampleClientIdentifiers), - ) -} - -@DevicePreview -@Composable -private fun ClientIdentifiersScreenPreview( - @PreviewParameter(ClientIdentifiersUiStateProvider::class) state: ClientIdentifiersUiState, -) { - ClientIdentifiersScreen( - state = state, - dialogState = ClientIdentifierDialogUiState.Loading, - onBackPressed = {}, - onDeleteIdentifier = {}, - refreshState = true, - onRefresh = {}, - onRetry = {}, - onDocumentClicked = {}, - onShowDialog = {}, - onCreateIdentifier = {}, - showCreateDialog = false, - onHideDialog = {}, - events = emptyFlow(), - ) -} - -val sampleClientIdentifiers = List(10) { - Identifier(id = it, description = "description $it") -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersUiState.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersUiState.kt deleted file mode 100644 index 25ef967e76e..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersUiState.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2024 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.clientIdentifiers - -import com.mifos.core.model.objects.noncoreobjects.Identifier -import org.jetbrains.compose.resources.StringResource - -/** - * Created by Aditya Gupta on 08/08/23. - */ -sealed class ClientIdentifiersUiState { - - data object Loading : ClientIdentifiersUiState() - - data class Error(val message: StringResource) : ClientIdentifiersUiState() - - data class ClientIdentifiers(val identifiers: List) : ClientIdentifiersUiState() - - data class IdentifierDeletedSuccessfully(val message: StringResource) : ClientIdentifiersUiState() -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersViewModel.kt deleted file mode 100644 index 567dd59a0ec..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiers/ClientIdentifiersViewModel.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2024 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.clientIdentifiers - -import androidclient.feature.client.generated.resources.Res -import androidclient.feature.client.generated.resources.feature_client_failed_to_create_identifier -import androidclient.feature.client.generated.resources.feature_client_failed_to_delete_identifier -import androidclient.feature.client.generated.resources.feature_client_failed_to_load_client_identifiers -import androidclient.feature.client.generated.resources.feature_client_failed_to_load_identifiers -import androidclient.feature.client.generated.resources.feature_client_identifier_created_successfully -import androidclient.feature.client.generated.resources.feature_client_identifier_deleted_successfully -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mifos.core.common.utils.Constants -import com.mifos.core.common.utils.DataState -import com.mifos.core.data.repository.ClientIdentifiersRepository -import com.mifos.core.domain.useCases.CreateClientIdentifierUseCase -import com.mifos.core.domain.useCases.DeleteIdentifierUseCase -import com.mifos.core.domain.useCases.GetClientIdentifierTemplateUseCase -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.feature.client.clientIdentifiersDialog.ClientIdentifierDialogUiState -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.StringResource -class ClientIdentifiersViewModel( - private val clientIdentifiersRepository: ClientIdentifiersRepository, - private val deleteIdentifierUseCase: DeleteIdentifierUseCase, - private val getClientIdentifierTemplateUseCase: GetClientIdentifierTemplateUseCase, - private val createClientIdentifierUseCase: CreateClientIdentifierUseCase, - private val savedStateHandle: SavedStateHandle, -) : ViewModel() { - - val clientId = savedStateHandle.getStateFlow(key = Constants.CLIENT_ID, initialValue = 0) - - private val _showCreateDialog = MutableStateFlow(false) - val showCreateDialog = _showCreateDialog.asStateFlow() - - private val _events = MutableSharedFlow(extraBufferCapacity = 1) - val events = _events.asSharedFlow() - private val _clientIdentifiersUiState = - MutableStateFlow(ClientIdentifiersUiState.Loading) - val clientIdentifiersUiState = _clientIdentifiersUiState.asStateFlow() - - private val _clientIdentifierDialogUiState = - MutableStateFlow(ClientIdentifierDialogUiState.Loading) - val clientIdentifierDialogUiState = _clientIdentifierDialogUiState.asStateFlow() - - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing = _isRefreshing.asStateFlow() - - init { - loadIdentifiers() - } - - fun showCreateIdentifierDialog() { - loadClientIdentifierTemplate() - _showCreateDialog.value = true - } - - fun hideCreateIdentifierDialog() { - _showCreateDialog.value = false - } - - fun refreshIdentifiersList() = viewModelScope.launch { - _isRefreshing.value = true - clientIdentifiersRepository.getClientIdentifiers(clientId.value).collect { result -> - when (result) { - is DataState.Error -> { - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.Error(Res.string.feature_client_failed_to_load_client_identifiers) - _isRefreshing.value = false - } - is DataState.Loading -> { - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.Loading - } - is DataState.Success -> { - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.ClientIdentifiers(result.data) - _isRefreshing.value = false - } - } - } - } - - fun loadIdentifiers() = viewModelScope.launch { - clientIdentifiersRepository.getClientIdentifiers(clientId.value).collect { result -> - when (result) { - is DataState.Error -> - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.Error(Res.string.feature_client_failed_to_load_client_identifiers) - - is DataState.Loading -> - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.Loading - - is DataState.Success -> - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.ClientIdentifiers(result.data) - } - } - } - - fun deleteIdentifier(identifierId: Int) = viewModelScope.launch { - deleteIdentifierUseCase(clientId.value, identifierId).collect { result -> - when (result) { - is DataState.Error -> - _clientIdentifiersUiState.value = - ClientIdentifiersUiState.Error(Res.string.feature_client_failed_to_delete_identifier) - - is DataState.Loading -> { - } - - is DataState.Success -> { - _events.tryEmit( - ClientIdentifiersEvent.ShowMessage( - Res.string.feature_client_identifier_deleted_successfully, - ), - ) - loadIdentifiers() - } - } - } - } - - fun loadClientIdentifierTemplate() = viewModelScope.launch { - getClientIdentifierTemplateUseCase(clientId.value).collect { result -> - when (result) { - is DataState.Error -> - _clientIdentifierDialogUiState.value = - ClientIdentifierDialogUiState.Error(Res.string.feature_client_failed_to_load_identifiers) - - is DataState.Loading -> - _clientIdentifierDialogUiState.value = - ClientIdentifierDialogUiState.Loading - - is DataState.Success -> - _clientIdentifierDialogUiState.value = - ClientIdentifierDialogUiState.ClientIdentifierTemplate( - result.data, - ) - } - } - } - - fun createClientIdentifier(identifierPayload: IdentifierPayload) = - viewModelScope.launch { - hideCreateIdentifierDialog() - _clientIdentifiersUiState.value = ClientIdentifiersUiState.Loading - createClientIdentifierUseCase(clientId.value, identifierPayload).collect { result -> - when (result) { - is DataState.Error -> { - _events.tryEmit( - ClientIdentifiersEvent.ShowMessage( - Res.string.feature_client_failed_to_create_identifier, - ), - ) - loadIdentifiers() - } - - is DataState.Loading -> { - } - - is DataState.Success -> { - _events.tryEmit( - ClientIdentifiersEvent.ShowMessage( - Res.string.feature_client_identifier_created_successfully, - ), - ) - loadIdentifiers() - } - } - } - } - sealed interface ClientIdentifiersEvent { - data class ShowMessage(val message: StringResource) : ClientIdentifiersEvent - } -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt new file mode 100644 index 00000000000..2f71fc18a5d --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt @@ -0,0 +1,648 @@ +/* + * Copyright 2024 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.clientIdentifiersAddUpdate + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.mifos.core.common.utils.Constants +import com.mifos.core.common.utils.DataState +import com.mifos.core.common.utils.FileKitUtil +import com.mifos.core.data.repository.ClientIdentifiersRepository +import com.mifos.core.data.repository.DocumentCreateUpdateRepository +import com.mifos.core.data.util.Error +import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.data.util.extractErrorMessage +import com.mifos.core.domain.useCases.CreateClientIdentifierUseCase +import com.mifos.core.domain.useCases.DownloadDocumentUseCase +import com.mifos.core.domain.useCases.GetDocumentsListUseCase +import com.mifos.core.model.objects.noncoreobjects.DocumentType +import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload +import com.mifos.core.ui.components.Status +import com.mifos.core.ui.util.BaseViewModel +import com.mifos.core.ui.util.multipartRequestBody +import com.mifos.feature.client.utils.toPlatformFile +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.extension +import io.github.vinceglb.filekit.name +import io.github.vinceglb.filekit.readBytes +import io.ktor.client.statement.readRawBytes +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +class ClientIdentifiersAddUpdateViewModel( + private val clientIdentifiersRepository: ClientIdentifiersRepository, + private val createClientIdentifierUseCase: CreateClientIdentifierUseCase, + private val downloadDocumentUseCase: DownloadDocumentUseCase, + private val getDocumentListUseCase: GetDocumentsListUseCase, + private val repository: DocumentCreateUpdateRepository, + savedStateHandle: SavedStateHandle, + private val networkMonitor: NetworkMonitor, +) : BaseViewModel( + initialState = ClientIdentifiersAddUpdateState(), +) { + private val route = savedStateHandle.toRoute() + + init { + mutableStateFlow.update { + it.copy( + clientId = route.clientId, + feature = route.feature, + ) + } + if (route.feature == Feature.ADD_IDENTIFIER) { + getIdentifiersOptionsAndObserveNetwork() + } else { + viewModelScope.launch { + getDocumentId() + } + } + } + + private fun getIdentifiersOptionsAndObserveNetwork() { + viewModelScope.launch { + observeNetwork() + + getIdentifiersTemplate() + } + } + + private fun observeNetwork() { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + mutableStateFlow.update { it.copy(networkConnection = isConnected) } + } + } + } + + private suspend fun createClientIdentifier(identifierPayload: IdentifierPayload) { + createClientIdentifierUseCase(route.clientId.toLong(), identifierPayload) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + isOverlayLoading = true, + dialogState = ClientIdentifiersAddUpdateState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + val error = extractErrorMessage(dataState.data) + + if (error == Error.MSG_NOT_FOUND) { + mutableStateFlow.update { + it.copy( + dialogState = null, + isOverlayLoading = false, + feature = Feature.ADD_UPDATE_DOCUMENT, + ) + } + } else { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + error.replace("unique", "document").replace("under", " under"), + ), + handleServerResponse = true, + ) + } + } + } + } + } + } + + private suspend fun getIdentifiersTemplate() { + clientIdentifiersRepository.getClientIdentifierTemplate(clientId = route.clientId.toLong()) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + identifierTemplate = dataState.data.allowedDocumentTypes, + ) + } + } + } + } + } + + private suspend fun getDocument(extension: String?) { + state.documentId?.let { documentId -> + downloadDocumentUseCase( + Constants.ENTITY_TYPE_CLIENT_IDENTIFIERS, + route.clientId, + documentId, + ) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + documentImageFile = dataState.data.readRawBytes(), + fileExtension = extension, + ) + } + } + } + } + } + } + + /** + * Retrieves the document ID for the given key. + * + * - If the document ID is `null`, it means the document does not exist. + * In this case, we display an error message along with a "Create Document" option, + * allowing the user to create a new document. + * - If the document ID is found, it will be used to update the state accordingly. + */ + private suspend fun getDocumentId() { + getDocumentListUseCase(Constants.ENTITY_TYPE_CLIENT_IDENTIFIERS, route.clientId) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + val data = + dataState.data.firstOrNull { it.description == route.uniqueKeyForHandleDocument } + + if (data?.id == null) { + // Handle missing document + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + "Document not found", + ), + documentNotFount = true, + ) + } + } else { + // Update state with valid document details + mutableStateFlow.update { + it.copy( + dialogState = null, + documentId = data.id, + imageFileName = data.fileName, + documentName = data.fileName, + previewButtonHandle = if (state.feature == Feature.ADD_UPDATE_DOCUMENT) PreviewButtonHandle.Submit else PreviewButtonHandle.Hide, + ) + } + + val extension = data.type?.substringAfterLast("/", "") + getDocument(extension) + } + } + } + } + } + + private suspend fun createDocument(file: PlatformFile, name: String?, uniqueKeyForHandleDocument: String) { + repository.createDocument( + Constants.ENTITY_TYPE_CLIENT_IDENTIFIERS, + route.clientId, + multipartRequestBody( + file = file, + name = name, + description = uniqueKeyForHandleDocument, + ), + ).collect { state -> + when (state) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error(state.message), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + isOverlayLoading = true, + dialogState = ClientIdentifiersAddUpdateState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + isOverlayLoading = false, + dialogState = null, + ) + } + + sendEvent(ClientIdentifiersAddUpdateEvent.NavigateBackWithUpdatedList) + } + } + } + } + + private suspend fun updateDocument(file: PlatformFile, name: String?, uniqueKeyForHandleDocument: String?) { + state.documentId?.let { documentId -> + repository.updateDocument( + entityType = Constants.CLIENTS, + entityId = route.clientId, + documentId = documentId, + file = multipartRequestBody( + file = file, + name = name, + description = uniqueKeyForHandleDocument, + ), + ).collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + ) + } + + sendEvent(ClientIdentifiersAddUpdateEvent.NavigateBackWithUpdatedList) + } + } + } + } + } + + override fun handleAction(action: ClientIdentifiersAddUpdateAction) { + when (action) { + ClientIdentifiersAddUpdateAction.CloseDialog -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + ) + } + } + + ClientIdentifiersAddUpdateAction.NavigateBack -> { + sendEvent(ClientIdentifiersAddUpdateEvent.NavigateBack) + } + + is ClientIdentifiersAddUpdateAction.OnDocumentTypeChange -> { + mutableStateFlow.update { + it.copy( + documentType = state.identifierTemplate?.get(action.index)?.name, + documentTypeId = state.identifierTemplate?.get(action.index)?.id, + ) + } + } + + is ClientIdentifiersAddUpdateAction.OnStatusChange -> { + mutableStateFlow.update { + it.copy( + status = state.statusList[action.index], + ) + } + } + + is ClientIdentifiersAddUpdateAction.OnDescriptionChange -> { + mutableStateFlow.update { + it.copy( + description = action.value, + ) + } + } + + is ClientIdentifiersAddUpdateAction.OnDocumentKeyChange -> { + mutableStateFlow.update { + it.copy( + documentKey = action.value, + ) + } + } + + ClientIdentifiersAddUpdateAction.OnCreateClientIdentifier -> { + viewModelScope.launch { + createClientIdentifier( + IdentifierPayload( + documentKey = state.documentKey, + documentTypeId = state.documentTypeId, + status = state.status, + description = state.description, + ), + ) + } + } + + ClientIdentifiersAddUpdateAction.OnRetry -> { + getIdentifiersOptionsAndObserveNetwork() + } + + is ClientIdentifiersAddUpdateAction.OnDocumentNameChange -> { + mutableStateFlow.update { + it.copy( + documentName = action.value, + ) + } + } + + ClientIdentifiersAddUpdateAction.OnShowBottomSheet -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.ShowBottomSheet, + ) + } + } + + ClientIdentifiersAddUpdateAction.OnSelectFile -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + ) + } + + viewModelScope.launch { + FileKitUtil.pickFile().collect { dataState -> + when (dataState) { + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + documentImageFile = dataState.data?.readBytes(), + imageFileName = dataState.data?.name, + fileExtension = dataState.data?.extension, + dialogState = null, + feature = Feature.VIEW_DOCUMENT, + previewButtonHandle = PreviewButtonHandle.Submit, + ) + } + } + + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> {} + } + } + } + } + + ClientIdentifiersAddUpdateAction.OnSelectImage -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + ) + } + + viewModelScope.launch { + FileKitUtil.pickImage().collect { dataState -> + when (dataState) { + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + documentImageFile = dataState.data?.readBytes(), + imageFileName = dataState.data?.name, + fileExtension = dataState.data?.extension, + dialogState = null, + feature = Feature.VIEW_DOCUMENT, + previewButtonHandle = PreviewButtonHandle.Submit, + ) + } + } + + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersAddUpdateState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> {} + } + } + } + } + + ClientIdentifiersAddUpdateAction.OnCreateDocument -> { + viewModelScope.launch { + val fileName = state.imageFileName.orEmpty() + val file = state.documentImageFile?.toPlatformFile(fileName) ?: return@launch + + /** + * A unique key generated for document operations (update/delete). + * + * This key is built by concatenating: + * - the document type name + * - the document key + * - the current status + * + * It ensures each document can be uniquely identified and handled + * even if multiple documents share the same type or key. + */ + val uniqueKey = state.documentType + state.documentKey + state.status + + if (state.documentId == null) { + createDocument( + file = file, + name = state.documentName, + uniqueKeyForHandleDocument = route.uniqueKeyForHandleDocument ?: uniqueKey, + ) + } else { + updateDocument( + file = file, + name = state.documentName, + uniqueKeyForHandleDocument = route.uniqueKeyForHandleDocument, + ) + } + } + } + + ClientIdentifiersAddUpdateAction.OnClosePreview -> { + mutableStateFlow.update { + it.copy( + feature = Feature.ADD_UPDATE_DOCUMENT, + ) + } + } + + ClientIdentifiersAddUpdateAction.OnOpenPreview -> { + mutableStateFlow.update { + it.copy( + feature = Feature.VIEW_DOCUMENT, + previewButtonHandle = PreviewButtonHandle.UploadNew, + ) + } + } + + ClientIdentifiersAddUpdateAction.OnNotFoundDocument -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + feature = Feature.ADD_UPDATE_DOCUMENT, + documentKey = "", + ) + } + } + } + } +} + +data class ClientIdentifiersAddUpdateState( + val identifierTemplate: List? = emptyList(), + val statusList: List = listOf(Status.Inactive.name, Status.Active.name), + val status: String? = null, + val documentKey: String? = null, + val description: String? = null, + val documentTypeId: Int? = null, + val documentType: String? = null, + val documentName: String? = null, + val imageFileName: String? = null, + val documentImageFile: ByteArray? = null, + val documentId: Int? = null, + val fileExtension: String? = null, + val dialogState: DialogState? = null, + val clientId: Int = -1, + val isOverlayLoading: Boolean = false, + val handleServerResponse: Boolean = false, + val documentKeyForUpdate: String? = null, + val feature: Feature = Feature.ADD_IDENTIFIER, + val documentNotFount: Boolean = false, + val previewButtonHandle: PreviewButtonHandle = PreviewButtonHandle.Submit, + val networkConnection: Boolean = true, +) { + sealed interface DialogState { + data class Error(val message: String) : DialogState + data object Loading : DialogState + data object ShowBottomSheet : DialogState + } +} + +sealed interface ClientIdentifiersAddUpdateEvent { + data object NavigateBack : ClientIdentifiersAddUpdateEvent + data object NavigateBackWithUpdatedList : ClientIdentifiersAddUpdateEvent +} + +sealed interface ClientIdentifiersAddUpdateAction { + data object CloseDialog : ClientIdentifiersAddUpdateAction + data object NavigateBack : ClientIdentifiersAddUpdateAction + data object OnRetry : ClientIdentifiersAddUpdateAction + data class OnDocumentTypeChange(val index: Int) : ClientIdentifiersAddUpdateAction + data class OnStatusChange(val index: Int) : ClientIdentifiersAddUpdateAction + data class OnDocumentKeyChange(val value: String) : ClientIdentifiersAddUpdateAction + data class OnDescriptionChange(val value: String) : ClientIdentifiersAddUpdateAction + data class OnDocumentNameChange(val value: String) : ClientIdentifiersAddUpdateAction + data object OnCreateClientIdentifier : ClientIdentifiersAddUpdateAction + data object OnCreateDocument : ClientIdentifiersAddUpdateAction + data object OnShowBottomSheet : ClientIdentifiersAddUpdateAction + data object OnSelectImage : ClientIdentifiersAddUpdateAction + data object OnSelectFile : ClientIdentifiersAddUpdateAction + data object OnClosePreview : ClientIdentifiersAddUpdateAction + data object OnOpenPreview : ClientIdentifiersAddUpdateAction + data object OnNotFoundDocument : ClientIdentifiersAddUpdateAction +} + +@Serializable +enum class Feature { + ADD_IDENTIFIER, + ADD_UPDATE_DOCUMENT, + VIEW_DOCUMENT, +} + +enum class PreviewButtonHandle { + Hide, + Submit, + UploadNew, +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateRoute.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateRoute.kt new file mode 100644 index 00000000000..a10a995415c --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateRoute.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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.clientIdentifiersAddUpdate + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import kotlinx.serialization.Serializable + +@Serializable +data class ClientIdentitiesAddUpdateRoute( + val clientId: Int, + val feature: Feature, + val uniqueKeyForHandleDocument: String?, +) + +fun NavGraphBuilder.clientIdentifiersAddUpdateDestination( + onBackPressed: () -> Unit, + onUpdatedListBack: (Int) -> Unit, + navController: NavController, +) { + composable { + ClientIdentifiersAddUpdateScreen( + onBackPressed = onBackPressed, + onUpdatedListBack = onUpdatedListBack, + navController = navController, + ) + } +} + +fun NavController.onNavigateToClientIdentifiersAddUpdateScreen( + clientId: Int, + feature: Feature, + uniqueKeyForHandleDocument: String?, +) { + this.navigate( + ClientIdentitiesAddUpdateRoute( + clientId, + feature, + uniqueKeyForHandleDocument, + ), + ) +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateScreen.kt new file mode 100644 index 00000000000..4e5e0ffa53b --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateScreen.kt @@ -0,0 +1,423 @@ +/* + * Copyright 2024 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 + */ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.mifos.feature.client.clientIdentifiersAddUpdate + +import androidclient.feature.client.generated.resources.Res +import androidclient.feature.client.generated.resources.add_document_title +import androidclient.feature.client.generated.resources.client_identifier_btn_add +import androidclient.feature.client.generated.resources.client_identifier_btn_back +import androidclient.feature.client.generated.resources.client_identifier_btn_create_new +import androidclient.feature.client.generated.resources.client_identifier_btn_next +import androidclient.feature.client.generated.resources.client_identifier_btn_submit +import androidclient.feature.client.generated.resources.client_identifier_btn_update +import androidclient.feature.client.generated.resources.client_identifier_btn_upload_new +import androidclient.feature.client.generated.resources.client_identifier_btn_view +import androidclient.feature.client.generated.resources.client_identifier_description +import androidclient.feature.client.generated.resources.client_identifier_document_key +import androidclient.feature.client.generated.resources.client_identifier_document_name +import androidclient.feature.client.generated.resources.client_identifier_document_type +import androidclient.feature.client.generated.resources.client_identifier_no_file_selected +import androidclient.feature.client.generated.resources.client_identifier_status +import androidclient.feature.client.generated.resources.client_identifier_title +import androidclient.feature.client.generated.resources.client_identifiers_error_text +import androidclient.feature.client.generated.resources.client_update_document_title +import androidclient.feature.client.generated.resources.feature_client_cancel +import androidclient.feature.client.generated.resources.feature_client_dialog_action_ok +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import coil3.compose.AsyncImage +import com.mifos.core.designsystem.component.MifosCard +import com.mifos.core.designsystem.component.MifosOutlinedTextField +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.theme.DesignToken +import com.mifos.core.designsystem.theme.MifosTypography +import com.mifos.core.ui.components.MifosAlertDialog +import com.mifos.core.ui.components.MifosBreadcrumbNavBar +import com.mifos.core.ui.components.MifosErrorComponent +import com.mifos.core.ui.components.MifosFilePickerBottomSheet +import com.mifos.core.ui.components.MifosProgressIndicator +import com.mifos.core.ui.components.MifosProgressIndicatorOverlay +import com.mifos.core.ui.components.MifosRowWithTextAndButton +import com.mifos.core.ui.components.MifosTwoButtonRow +import com.mifos.core.ui.util.EventsEffect +import com.mifos.feature.client.utils.PdfPreview +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun ClientIdentifiersAddUpdateScreen( + onBackPressed: () -> Unit, + onUpdatedListBack: (Int) -> Unit, + navController: NavController, + viewModel: ClientIdentifiersAddUpdateViewModel = koinViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + ClientIdentifiersAddUpdateEvent.NavigateBack -> onBackPressed() + ClientIdentifiersAddUpdateEvent.NavigateBackWithUpdatedList -> onUpdatedListBack(state.clientId) + } + } + + ClientIdentifiersAddUpdateScaffold( + state = state, + navController = navController, + onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + ) + + ClientIdentifiersAddUpdateDialog( + state = state, + onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + ) +} + +@Composable +private fun ClientIdentifiersAddUpdateDialog( + state: ClientIdentifiersAddUpdateState, + onAction: (ClientIdentifiersAddUpdateAction) -> Unit, +) { + when (state.dialogState) { + is ClientIdentifiersAddUpdateState.DialogState.Error -> { + if (state.documentNotFount || state.handleServerResponse) { + MifosAlertDialog( + dialogTitle = when { + state.documentNotFount -> stringResource(Res.string.client_identifiers_error_text) + else -> null + }, + dialogText = state.dialogState.message, + confirmationText = when { + state.documentNotFount -> stringResource(Res.string.client_identifier_btn_create_new) + else -> stringResource(Res.string.feature_client_dialog_action_ok) + }, + dismissText = when { + state.documentNotFount -> stringResource(Res.string.feature_client_cancel) + else -> null + }, + onDismissRequest = { + if (state.documentNotFount) { + onAction(ClientIdentifiersAddUpdateAction.NavigateBack) + } + }, + onConfirmation = { + if (state.documentNotFount) { + onAction(ClientIdentifiersAddUpdateAction.OnNotFoundDocument) + } else { + onAction(ClientIdentifiersAddUpdateAction.CloseDialog) + } + }, + ) + } else { + MifosErrorComponent( + modifier = Modifier.background(MaterialTheme.colorScheme.background), + message = state.dialogState.message, + isRetryEnabled = true, + onRetry = { + onAction(ClientIdentifiersAddUpdateAction.OnRetry) + }, + ) + } + } + + ClientIdentifiersAddUpdateState.DialogState.Loading -> { + if (state.isOverlayLoading) { + MifosProgressIndicatorOverlay() + } else { + MifosProgressIndicator() + } + } + + ClientIdentifiersAddUpdateState.DialogState.ShowBottomSheet -> { + MifosFilePickerBottomSheet( + onDismiss = { + onAction(ClientIdentifiersAddUpdateAction.CloseDialog) + }, + onGalleryClick = { + onAction(ClientIdentifiersAddUpdateAction.OnSelectImage) + }, + onFilesClick = { + onAction(ClientIdentifiersAddUpdateAction.OnSelectFile) + }, + onMoreClick = { + // implement further + }, + ) + } + + null -> Unit + } +} + +@Composable +internal fun ClientIdentifiersAddUpdateScaffold( + state: ClientIdentifiersAddUpdateState, + navController: NavController, + modifier: Modifier = Modifier, + onAction: (ClientIdentifiersAddUpdateAction) -> Unit, +) { + MifosScaffold( + title = "", + onBackPressed = { + onAction(ClientIdentifiersAddUpdateAction.NavigateBack) + }, + modifier = modifier.background(MaterialTheme.colorScheme.onPrimary), + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues).fillMaxSize(), + ) { + if (state.feature != Feature.VIEW_DOCUMENT) { + MifosBreadcrumbNavBar(navController) + } + + Column( + modifier = modifier.fillMaxSize().padding( + horizontal = DesignToken.padding.large, + ), + ) { + if (state.feature != Feature.VIEW_DOCUMENT) { + Text( + text = when { + state.feature == Feature.VIEW_DOCUMENT -> stringResource(Res.string.client_identifier_title) + + state.documentKey == null -> stringResource(Res.string.client_update_document_title) + + else -> stringResource(Res.string.add_document_title) + }, + style = MifosTypography.titleMedium, + ) + } + + Spacer(Modifier.height(DesignToken.spacing.large)) + + when (state.feature) { + Feature.ADD_IDENTIFIER -> { + ClientIdentifiersAddIdentifier( + state = state, + onAction = onAction, + ) + } + + Feature.ADD_UPDATE_DOCUMENT -> { + ClientIdentifiersAddUpdateDocument( + state = state, + onAction = onAction, + ) + } + + Feature.VIEW_DOCUMENT -> { + ClientIdentifiersDocumentPreview( + state = state, + onAction = onAction, + ) + } + } + } + } + } +} + +@Composable +private fun ClientIdentifiersAddIdentifier( + state: ClientIdentifiersAddUpdateState, + onAction: (ClientIdentifiersAddUpdateAction) -> Unit, +) { + MifosTextFieldDropdown( + value = state.documentType ?: "", + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(ClientIdentifiersAddUpdateAction.OnDocumentTypeChange(index)) + }, + options = state.identifierTemplate?.map { + it.name ?: "" + } ?: emptyList(), + label = stringResource(Res.string.client_identifier_document_type), + ) + + MifosTextFieldDropdown( + value = state.status ?: "", + onValueChanged = {}, + onOptionSelected = { index, value -> + onAction(ClientIdentifiersAddUpdateAction.OnStatusChange(index)) + }, + options = state.statusList.map { + it + }, + label = stringResource(Res.string.client_identifier_status), + ) + + MifosOutlinedTextField( + value = state.documentKey ?: "", + onValueChange = { + onAction(ClientIdentifiersAddUpdateAction.OnDocumentKeyChange(it)) + }, + label = stringResource(Res.string.client_identifier_document_key), + ) + + Spacer(Modifier.height(DesignToken.padding.large)) + + MifosOutlinedTextField( + value = state.description ?: "", + onValueChange = { + onAction(ClientIdentifiersAddUpdateAction.OnDescriptionChange(it)) + }, + label = stringResource(Res.string.client_identifier_description), + ) + + Spacer(Modifier.height(DesignToken.spacing.largeIncreased)) + + MifosTwoButtonRow( + firstBtnText = stringResource(Res.string.client_identifier_btn_back), + secondBtnText = stringResource(Res.string.client_identifier_btn_next), + onFirstBtnClick = { + onAction(ClientIdentifiersAddUpdateAction.NavigateBack) + }, + onSecondBtnClick = { + onAction(ClientIdentifiersAddUpdateAction.OnCreateClientIdentifier) + }, + isSecondButtonEnabled = !state.documentKey.isNullOrEmpty() && !state.documentType.isNullOrEmpty() && !state.status.isNullOrEmpty(), + ) +} + +@Composable +private fun ClientIdentifiersAddUpdateDocument( + state: ClientIdentifiersAddUpdateState, + onAction: (ClientIdentifiersAddUpdateAction) -> Unit, +) { + MifosOutlinedTextField( + value = state.documentName ?: "", + onValueChange = { + onAction(ClientIdentifiersAddUpdateAction.OnDocumentNameChange(it)) + }, + label = stringResource(Res.string.client_identifier_document_name), + ) + + Spacer(Modifier.height(DesignToken.padding.large)) + + MifosRowWithTextAndButton( + text = state.imageFileName ?: stringResource(Res.string.client_identifier_no_file_selected), + onBtnClick = { + if (state.imageFileName == null) { + onAction(ClientIdentifiersAddUpdateAction.OnShowBottomSheet) + } else { + onAction(ClientIdentifiersAddUpdateAction.OnOpenPreview) + } + }, + btnText = if (state.imageFileName == null) { + stringResource(Res.string.client_identifier_btn_add) + } else { + stringResource( + Res.string.client_identifier_btn_view, + ) + }, + ) + + Spacer(Modifier.height(DesignToken.spacing.largeIncreased)) + + MifosTwoButtonRow( + firstBtnText = stringResource(Res.string.client_identifier_btn_back), + secondBtnText = when { + state.documentKey == null -> stringResource(Res.string.client_identifier_btn_update) + else -> stringResource(Res.string.client_identifier_btn_submit) + }, + onFirstBtnClick = { + onAction(ClientIdentifiersAddUpdateAction.NavigateBack) + }, + onSecondBtnClick = { + onAction(ClientIdentifiersAddUpdateAction.OnCreateDocument) + }, + isSecondButtonEnabled = !state.imageFileName.isNullOrEmpty() && !state.documentName.isNullOrEmpty(), + ) +} + +@Composable +private fun ClientIdentifiersDocumentPreview( + state: ClientIdentifiersAddUpdateState, + modifier: Modifier = Modifier, + onAction: (ClientIdentifiersAddUpdateAction) -> Unit, +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + MifosCard( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.onPrimary), + elevation = 0.dp, + borderStroke = BorderStroke(1.dp, MaterialTheme.colorScheme.secondaryContainer), + ) { + Box( + modifier = Modifier.aspectRatio(0.707f, true), + contentAlignment = Alignment.Center, + ) { + if (state.fileExtension == "pdf") { + PdfPreview(state.documentImageFile!!, Modifier.matchParentSize()) + } else { + AsyncImage( + model = state.documentImageFile, + contentDescription = null, + modifier = Modifier.fillMaxSize().align(Alignment.Center), + ) + } + } + } + + Spacer(Modifier.height(DesignToken.spacing.largeIncreased)) + + MifosTwoButtonRow( + firstBtnText = stringResource(Res.string.client_identifier_btn_back), + secondBtnText = if (state.previewButtonHandle == PreviewButtonHandle.UploadNew) { + stringResource( + Res.string.client_identifier_btn_upload_new, + ) + } else { + stringResource( + Res.string.client_identifier_btn_submit, + ) + }, + onFirstBtnClick = { + if (state.previewButtonHandle == PreviewButtonHandle.Hide) { + onAction(ClientIdentifiersAddUpdateAction.NavigateBack) + } else { + onAction(ClientIdentifiersAddUpdateAction.OnClosePreview) + } + }, + onSecondBtnClick = { + if (state.previewButtonHandle == PreviewButtonHandle.UploadNew) { + onAction(ClientIdentifiersAddUpdateAction.OnShowBottomSheet) + } else { + onAction(ClientIdentifiersAddUpdateAction.OnClosePreview) + } + }, + isSecondButtonEnabled = state.previewButtonHandle != PreviewButtonHandle.Hide, + isButtonIconVisible = false, + ) + } +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersDialog/ClientIdentifierDialogUiState.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersDialog/ClientIdentifierDialogUiState.kt deleted file mode 100644 index ec533ff0ac7..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersDialog/ClientIdentifierDialogUiState.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 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.clientIdentifiersDialog - -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate -import org.jetbrains.compose.resources.StringResource - -/** - * Created by Aditya Gupta on 16/08/23. - */ -sealed class ClientIdentifierDialogUiState { - - data object Loading : ClientIdentifierDialogUiState() - - data class Error(val message: StringResource) : ClientIdentifierDialogUiState() - - data class ClientIdentifierTemplate(val identifierTemplate: IdentifierTemplate) : - ClientIdentifierDialogUiState() - - data object IdentifierCreatedSuccessfully : ClientIdentifierDialogUiState() -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersDialog/ClientIdentifiersDialogScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersDialog/ClientIdentifiersDialogScreen.kt deleted file mode 100644 index 208db76b9db..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersDialog/ClientIdentifiersDialogScreen.kt +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2024 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.clientIdentifiersDialog - -import androidclient.feature.client.generated.resources.Res -import androidclient.feature.client.generated.resources.feature_client_create_identifier_dialog -import androidclient.feature.client.generated.resources.feature_client_failed_to_load_client_identifiers -import androidclient.feature.client.generated.resources.feature_client_identifier_description -import androidclient.feature.client.generated.resources.feature_client_identifier_document_type -import androidclient.feature.client.generated.resources.feature_client_identifier_isActive -import androidclient.feature.client.generated.resources.feature_client_identifier_message_field_required -import androidclient.feature.client.generated.resources.feature_client_identifier_submit -import androidclient.feature.client.generated.resources.feature_client_identifier_unique_id -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import com.mifos.core.designsystem.component.MifosOutlinedTextField -import com.mifos.core.designsystem.component.MifosSweetError -import com.mifos.core.designsystem.component.MifosTextFieldDropdown -import com.mifos.core.designsystem.icon.MifosIcons -import com.mifos.core.model.objects.noncoreobjects.IdentifierPayload -import com.mifos.core.model.objects.noncoreobjects.IdentifierTemplate -import com.mifos.core.ui.components.MifosProgressIndicator -import com.mifos.core.ui.util.DevicePreview -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.PreviewParameter -import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider - -@Composable -internal fun ClientIdentifiersDialogScreen( - state: ClientIdentifierDialogUiState, - onDismiss: () -> Unit, - onRetry: () -> Unit, - onCreateIdentifier: (IdentifierPayload) -> Unit, -) { - Dialog( - onDismissRequest = { onDismiss() }, - ) { - Surface( - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surface, - ) { - Box( - modifier = Modifier - .height(425.dp) - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface), - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier.padding(20.dp) - .verticalScroll(rememberScrollState()), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(Res.string.feature_client_create_identifier_dialog), - fontSize = MaterialTheme.typography.titleLarge.fontSize, - ) - IconButton(onClick = { onDismiss() }) { - Icon( - imageVector = MifosIcons.Close, - contentDescription = "", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .width(30.dp) - .height(30.dp), - ) - } - } - when (state) { - is ClientIdentifierDialogUiState.ClientIdentifierTemplate -> { - ClientIdentifiersContent( - clientIdentifierTemplate = state.identifierTemplate, - onCreate = onCreateIdentifier, - ) - } - - is ClientIdentifierDialogUiState.Error -> MifosSweetError( - message = stringResource(state.message), - ) { - onRetry() - } - - is ClientIdentifierDialogUiState.IdentifierCreatedSuccessfully -> { - } - - is ClientIdentifierDialogUiState.Loading -> MifosProgressIndicator() - } - } - } - } - } -} - -@Composable -private fun ClientIdentifiersContent( - clientIdentifierTemplate: IdentifierTemplate, - onCreate: (IdentifierPayload) -> Unit, -) { - var documentType by rememberSaveable { - mutableStateOf( - clientIdentifierTemplate.allowedDocumentTypes?.get( - 0, - )?.name ?: "", - ) - } - var documentTypeId by rememberSaveable { - mutableStateOf( - clientIdentifierTemplate.allowedDocumentTypes?.get( - 0, - )?.id, - ) - } - var uniqueId by rememberSaveable { mutableStateOf("") } - var uniqueIdError by rememberSaveable { mutableStateOf(false) } - var description by rememberSaveable { mutableStateOf("") } - var descriptionError by rememberSaveable { mutableStateOf(false) } - var isActive by rememberSaveable { mutableStateOf(false) } - - fun validateInput(): Boolean { - var temp = true - if (uniqueId.isEmpty()) { - uniqueIdError = true - temp = false - } - if (description.isEmpty()) { - descriptionError = true - temp = false - } - return temp - } - Column { - MifosTextFieldDropdown( - value = documentType, - onValueChanged = { - documentType = it - }, - onOptionSelected = { index, value -> - documentType = value - documentTypeId = clientIdentifierTemplate.allowedDocumentTypes?.get(index)?.id - }, - label = stringResource(Res.string.feature_client_identifier_document_type), - options = clientIdentifierTemplate.allowedDocumentTypes?.map { it.name.toString() } - ?: emptyList(), - readOnly = true, - ) - - MifosOutlinedTextField( - value = uniqueId, - onValueChange = { - uniqueId = it - uniqueIdError = false - }, - label = stringResource(Res.string.feature_client_identifier_unique_id), - error = if (uniqueIdError) stringResource(Res.string.feature_client_identifier_message_field_required) else null, - trailingIcon = { - if (uniqueIdError) { - Icon( - imageVector = MifosIcons.Error, - contentDescription = null, - ) - } - }, - ) - - MifosOutlinedTextField( - value = description, - onValueChange = { - description = it - descriptionError = false - }, - label = stringResource(Res.string.feature_client_identifier_description), - error = if (descriptionError) stringResource(Res.string.feature_client_identifier_message_field_required) else null, - trailingIcon = { - if (descriptionError) { - Icon( - imageVector = MifosIcons.Error, - contentDescription = null, - ) - } - }, - ) - - Row( - modifier = Modifier.padding(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Checkbox( - checked = isActive, - onCheckedChange = { - isActive = it - }, - ) - Text(text = stringResource(Res.string.feature_client_identifier_isActive)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - if (validateInput()) { - val payload = IdentifierPayload( - documentTypeId = documentTypeId, - documentKey = uniqueId, - status = if (isActive) "Active" else "InActive", - description = description, - ) - onCreate(payload) - } - }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - ) { - Text(text = stringResource(Res.string.feature_client_identifier_submit)) - } - } -} - -private class ClientIdentifiersDialogUiStatePreview : - PreviewParameterProvider { - - override val values: Sequence - get() = sequenceOf( - ClientIdentifierDialogUiState.Loading, - ClientIdentifierDialogUiState.Error(Res.string.feature_client_failed_to_load_client_identifiers), - ClientIdentifierDialogUiState.IdentifierCreatedSuccessfully, - ) -} - -@DevicePreview -@Composable -private fun ClientIdentifiersDialogScreenPreview( - @PreviewParameter(ClientIdentifiersDialogUiStatePreview::class) state: ClientIdentifierDialogUiState, -) { - ClientIdentifiersDialogScreen( - state = state, - onDismiss = {}, - onRetry = {}, - onCreateIdentifier = {}, - ) -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListRoute.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListRoute.kt new file mode 100644 index 00000000000..3a5f54f1c51 --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListRoute.kt @@ -0,0 +1,50 @@ +/* + * 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.clientIdentifiersList + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.mifos.feature.client.clientIdentifiersAddUpdate.Feature +import kotlinx.serialization.Serializable + +@Serializable +data class ClientIdentifiersListRoute( + val clientId: Int = -1, +) + +fun NavGraphBuilder.clientIdentifiersListDestination( + addNewClientIdentity: (Int, Feature, String?) -> Unit, + onBackPress: () -> Unit, + navController: NavController, +) { + composable { + ClientIdentifiersListScreen( + addNewClientIdentity = addNewClientIdentity, + onBackPress = onBackPress, + navController = navController, + ) + } +} + +fun NavController.navigateToClientIdentifiersListScreen( + clientId: Int, +) { + this.navigate(ClientIdentifiersListRoute(clientId = clientId)) +} + +fun NavController.navigateBackToUpdateClientIdentifiersListScreen( + clientId: Int, +) { + this.navigate(ClientIdentifiersListRoute(clientId = clientId)) { + popUpTo(ClientIdentifiersListRoute(clientId = clientId)) { inclusive = true } + launchSingleTop = true + } +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListScreen.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListScreen.kt similarity index 71% rename from feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListScreen.kt rename to feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListScreen.kt index deb9cbee8a3..adbbd354429 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListScreen.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListScreen.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.client.clientIdentitiesList +package com.mifos.feature.client.clientIdentifiersList import androidclient.feature.client.generated.resources.Res import androidclient.feature.client.generated.resources.add_icon @@ -39,8 +39,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import com.mifos.core.designsystem.component.LoadingDialogState -import com.mifos.core.designsystem.component.MifosLoadingDialog import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.theme.DesignToken import com.mifos.core.designsystem.theme.MifosTypography @@ -50,49 +48,55 @@ import com.mifos.core.ui.components.MifosActionsIdentifierListingComponent import com.mifos.core.ui.components.MifosAlertDialog import com.mifos.core.ui.components.MifosBreadcrumbNavBar import com.mifos.core.ui.components.MifosEmptyCard +import com.mifos.core.ui.components.MifosProgressIndicator +import com.mifos.core.ui.components.MifosProgressIndicatorOverlay import com.mifos.core.ui.util.EventsEffect +import com.mifos.feature.client.clientIdentifiersAddUpdate.Feature import com.mifos.feature.client.utils.getClientIdentifierStatus import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Composable -internal fun ClientIdentitiesListScreenRoute( - addNewClientIdentity: (Int) -> Unit, +internal fun ClientIdentifiersListScreen( + addNewClientIdentity: (Int, Feature, String?) -> Unit, + onBackPress: () -> Unit, navController: NavController, - viewModel: ClientIdentitiesListViewModel = koinViewModel(), + viewModel: ClientIdentifiersListViewModel = koinViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() EventsEffect(viewModel.eventFlow) { event -> when (event) { - is ClientIdentitiesListEvent.AddNewClientIdentity -> addNewClientIdentity(event.id) - ClientIdentitiesListEvent.ViewDocument -> {} + is ClientIdentifiersListEvent.AddNewClientIdentity -> addNewClientIdentity(event.id, event.feature, event.uniqueKeyForHandleDocument) + ClientIdentifiersListEvent.NavigateBack -> onBackPress() } } - ClientIdentitiesListScreen( + ClientIdentifiersListScreen( state = state, onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, navController = navController, ) - ClientIdentitiesDialog( + ClientIdentifiersDialog( state = state, onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, ) } @Composable -internal fun ClientIdentitiesListScreen( - state: ClientIdentitiesListState, +internal fun ClientIdentifiersListScreen( + state: ClientIdentifiersListState, navController: NavController, - onAction: (ClientIdentitiesListAction) -> Unit, + onAction: (ClientIdentifiersListAction) -> Unit, ) { val emptyMessage = stringResource(Res.string.client_identifiers_not_available) MifosScaffold( - onBackPressed = {}, + onBackPressed = { + onAction(ClientIdentifiersListAction.NavigateBack) + }, title = "Client Identities", ) { paddingValues -> Column( @@ -118,12 +122,28 @@ internal fun ClientIdentitiesListScreen( } else { LazyColumn { item { - state.clientIdentitiesList.forEachIndexed { index, item -> + state.clientIdentitiesList.reversed().forEachIndexed { index, item -> + + val status = getClientIdentifierStatus(item.status) + + /** + * A unique key generated for document operations (update/delete). + * + * This key is built by concatenating: + * - the document type name + * - the document key + * - the current status + * + * It ensures each document can be uniquely identified and handled + * even if multiple documents share the same type or key. + */ + val uniqueKeyForHandleDocument = item.documentType?.name + item.documentKey + status + MifosActionsIdentifierListingComponent( type = item.documentType?.name ?: emptyMessage, id = if (item.id != null) item.id.toString() else emptyMessage, key = item.documentKey ?: emptyMessage, - status = getClientIdentifierStatus(item.status), + status = status, description = item.description ?: emptyMessage, // TODO check what is identifyDocuments, couldnot find in the api identifyDocuments = item.documentType?.name ?: emptyMessage, @@ -135,16 +155,17 @@ internal fun ClientIdentitiesListScreen( onActionClicked = { actions -> when (actions) { is Actions.ViewDocument -> onAction.invoke( - ClientIdentitiesListAction.ViewDocument, + ClientIdentifiersListAction.ViewDocument(uniqueKeyForHandleDocument), ) is Actions.UploadAgain -> onAction.invoke( - ClientIdentitiesListAction.UploadAgain, + ClientIdentifiersListAction.UploadAgain(uniqueKeyForHandleDocument), ) is Actions.DeleteDocument -> onAction.invoke( - ClientIdentitiesListAction.DeleteDocument( + ClientIdentifiersListAction.DeleteDocument( item.id ?: -1, + uniqueKeyForHandleDocument, ), ) @@ -153,7 +174,7 @@ internal fun ClientIdentitiesListScreen( }, onClick = { onAction.invoke( - ClientIdentitiesListAction.ToggleShowMenu( + ClientIdentifiersListAction.ToggleShowMenu( index, ), ) @@ -174,7 +195,7 @@ internal fun ClientIdentitiesListScreen( @Composable private fun ClientIdentifiersHeader( totalItem: String, - onAction: (ClientIdentitiesListAction) -> Unit, + onAction: (ClientIdentifiersListAction) -> Unit, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -195,7 +216,7 @@ private fun ClientIdentifiersHeader( Icon( modifier = Modifier.onClick { - onAction.invoke(ClientIdentitiesListAction.ToggleSearch) + onAction.invoke(ClientIdentifiersListAction.ToggleSearch) }, painter = painterResource(Res.drawable.search), contentDescription = null, @@ -205,7 +226,7 @@ private fun ClientIdentifiersHeader( Icon( modifier = Modifier.onClick { - onAction.invoke(ClientIdentitiesListAction.AddNewClientIdentity) + onAction.invoke(ClientIdentifiersListAction.AddNewClientIdentity) }, painter = painterResource(Res.drawable.add_icon), contentDescription = null, @@ -215,40 +236,46 @@ private fun ClientIdentifiersHeader( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ClientIdentitiesDialog( - state: ClientIdentitiesListState, - onAction: (ClientIdentitiesListAction) -> Unit, +private fun ClientIdentifiersDialog( + state: ClientIdentifiersListState, + onAction: (ClientIdentifiersListAction) -> Unit, ) { when (state.dialogState) { - is ClientIdentitiesListState.DialogState.Error -> { + is ClientIdentifiersListState.DialogState.Error -> { MifosAlertDialog( dialogTitle = stringResource(Res.string.client_identifiers_error_text), dialogText = state.dialogState.message, - onDismissRequest = { onAction.invoke(ClientIdentitiesListAction.CloseDialog) }, - onConfirmation = { onAction.invoke(ClientIdentitiesListAction.CloseDialog) }, + onDismissRequest = { onAction.invoke(ClientIdentifiersListAction.CloseDialog) }, + onConfirmation = { onAction.invoke(ClientIdentifiersListAction.CloseDialog) }, ) } - ClientIdentitiesListState.DialogState.Loading -> MifosLoadingDialog(LoadingDialogState.Shown) + ClientIdentifiersListState.DialogState.Loading -> { + if (state.isOverlayLoading) { + MifosProgressIndicatorOverlay() + } else { + MifosProgressIndicator() + } + } - is ClientIdentitiesListState.DialogState.DeletedSuccessfully -> { + is ClientIdentifiersListState.DialogState.DeletedSuccessfully -> { MifosAlertDialog( dialogTitle = stringResource(Res.string.client_identifiers_identities_success_text), dialogText = stringResource(Res.string.client_identifiers_identities_client_identifier_deletion_success) + " " + state.dialogState.id, - onDismissRequest = { onAction.invoke(ClientIdentitiesListAction.CloseDialog) }, - onConfirmation = { onAction.invoke(ClientIdentitiesListAction.CloseDialog) }, + onDismissRequest = { onAction.invoke(ClientIdentifiersListAction.CloseDialog) }, + onConfirmation = { onAction.invoke(ClientIdentifiersListAction.CloseDialog) }, ) } null -> {} - ClientIdentitiesListState.DialogState.NoInternet -> { + ClientIdentifiersListState.DialogState.NoInternet -> { MifosAlertDialog( dialogTitle = stringResource(Res.string.client_identifiers_error_text), dialogText = stringResource(Res.string.feature_client_error_not_connected_internet), - onDismissRequest = { onAction.invoke(ClientIdentitiesListAction.CloseDialog) }, - onConfirmation = { onAction.invoke(ClientIdentitiesListAction.Refresh) }, + onDismissRequest = { onAction.invoke(ClientIdentifiersListAction.CloseDialog) }, + onConfirmation = { onAction.invoke(ClientIdentifiersListAction.Refresh) }, confirmationText = stringResource(Res.string.client_identifiers_retry), ) } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListViewModel.kt new file mode 100644 index 00000000000..64f01b19111 --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersList/ClientIdentifiersListViewModel.kt @@ -0,0 +1,323 @@ +/* + * 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.clientIdentifiersList + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.mifos.core.common.utils.Constants +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.repository.ClientIdentifiersRepository +import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.domain.useCases.DeleteIdentifierUseCase +import com.mifos.core.domain.useCases.GetDocumentsListUseCase +import com.mifos.core.domain.useCases.RemoveDocumentUseCase +import com.mifos.core.model.objects.noncoreobjects.Identifier +import com.mifos.core.ui.util.BaseViewModel +import com.mifos.feature.client.clientIdentifiersAddUpdate.Feature +import com.mifos.feature.client.clientIdentifiersList.ClientIdentifiersListEvent.AddNewClientIdentity +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ClientIdentifiersListViewModel( + private val repository: ClientIdentifiersRepository, + private val deleteClientIdentifierUseCase: DeleteIdentifierUseCase, + private val getDocumentListUseCase: GetDocumentsListUseCase, + private val removeDocumentUseCase: RemoveDocumentUseCase, + private val networkMonitor: NetworkMonitor, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = ClientIdentifiersListState(), +) { + private val route = savedStateHandle.toRoute() + + override fun handleAction(action: ClientIdentifiersListAction) { + when (action) { + ClientIdentifiersListAction.AddNewClientIdentity -> { + sendEvent( + AddNewClientIdentity( + id = route.clientId, + feature = Feature.ADD_IDENTIFIER, + ), + ) + } + + is ClientIdentifiersListAction.ToggleShowMenu -> mutableStateFlow.update { + it.copy( + currentExpandedItem = action.index, + expandClientIdentity = !it.expandClientIdentity, + ) + } + + is ClientIdentifiersListAction.UploadAgain -> { + sendEvent( + AddNewClientIdentity( + id = route.clientId, + feature = Feature.ADD_UPDATE_DOCUMENT, + uniqueKeyForHandleDocument = action.uniqueKeyForHandleDocument, + ), + ) + } + + is ClientIdentifiersListAction.ViewDocument -> { + sendEvent( + AddNewClientIdentity( + id = route.clientId, + feature = Feature.VIEW_DOCUMENT, + uniqueKeyForHandleDocument = action.uniqueKeyForHandleDocument, + ), + ) + } + + is ClientIdentifiersListAction.DeleteDocument -> { + deleteClientIdentity(route.clientId, action.identifier, action.uniqueKeyForHandleDocument) + } + + ClientIdentifiersListAction.ToggleSearch -> { + mutableStateFlow.update { + it.copy(isSearchBarActive = !it.isSearchBarActive) + } + } + + ClientIdentifiersListAction.CloseDialog -> { + mutableStateFlow.update { + it.copy(dialogState = null) + } + } + + ClientIdentifiersListAction.Refresh -> checkInternetAndFetchIdentities() + ClientIdentifiersListAction.NavigateBack -> { + sendEvent(ClientIdentifiersListEvent.NavigateBack) + } + } + } + + init { + checkInternetAndFetchIdentities() + } + + private fun checkInternetAndFetchIdentities() { + viewModelScope.launch { + mutableStateFlow.update { + it.copy(dialogState = ClientIdentifiersListState.DialogState.Loading) + } + checkNetworkConnection() + } + } + + private suspend fun checkNetworkConnection() { + networkMonitor.isOnline.collect { status -> + when (status) { + true -> getClientListIdentities(route.clientId.toLong()) + false -> { + mutableStateFlow.update { + it.copy(dialogState = ClientIdentifiersListState.DialogState.NoInternet) + } + } + } + } + } + + private suspend fun deleteDocument(documentId: Int) { + removeDocumentUseCase( + Constants.ENTITY_TYPE_CLIENT_IDENTIFIERS, + route.clientId, + documentId, + ) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Loading, + isOverlayLoading = true, + ) + } + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + isOverlayLoading = false, + ) + } + } + } + } + } + + private suspend fun getDocumentId(documentKey: String?) { + getDocumentListUseCase(Constants.ENTITY_TYPE_CLIENT_IDENTIFIERS, route.clientId) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Error( + dataState.message, + ), + ) + } + } + + DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Loading, + ) + } + } + + is DataState.Success -> { + val deleteDocument = + dataState.data.firstOrNull { it.description == documentKey }?.id + + if (deleteDocument != null) { + deleteDocument(deleteDocument) + } + } + } + } + } + + private suspend fun getClientListIdentities(clientId: Long) { + repository.getClientListIdentifiers(clientId).collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Error( + (dataState.message), + ), + ) + } + } + + DataState.Loading -> mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Loading, + ) + } + + is DataState.Success -> { + val sortedList = dataState.data.sortedWith( + compareBy( + { identifier -> + val s = identifier.status?.lowercase() ?: "" + if (s.contains("active") && !s.contains("inactive")) 0 else 1 + }, + { identifier -> + identifier.description?.lowercase() ?: "" + }, + ), + ) + + mutableStateFlow.update { + it.copy( + dialogState = null, + clientIdentitiesList = sortedList, + ) + } + } + } + } + } + + private fun deleteClientIdentity(clientId: Int, identifierId: Int, documentKey: String?) { + viewModelScope.launch { + deleteClientIdentifierUseCase.invoke(clientId.toLong(), identifierId.toLong()) + .collect { state -> + when (state) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Error( + state.message, + ), + ) + } + } + + DataState.Loading -> mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.Loading, + isOverlayLoading = true, + ) + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = ClientIdentifiersListState.DialogState.DeletedSuccessfully( + identifierId, + ), + ) + } + + // it call first take document id then delete document + getDocumentId(documentKey) + + getClientListIdentities(route.clientId.toLong()) + } + } + } + } + } +} + +data class ClientIdentifiersListState( + val isSearchBarActive: Boolean = false, + val clientIdentitiesList: List = emptyList(), + val currentExpandedItem: Int = -1, + val expandClientIdentity: Boolean = false, + val dialogState: DialogState? = null, + val isOverlayLoading: Boolean = false, +) { + sealed interface DialogState { + data class Error(val message: String) : DialogState + data object Loading : DialogState + data object NoInternet : DialogState + data class DeletedSuccessfully(val id: Int) : DialogState + } +} + +sealed interface ClientIdentifiersListEvent { + data object NavigateBack : ClientIdentifiersListEvent + data class AddNewClientIdentity( + val id: Int, + val feature: Feature, + val uniqueKeyForHandleDocument: String? = null, + ) : + ClientIdentifiersListEvent +} + +sealed interface ClientIdentifiersListAction { + data object AddNewClientIdentity : ClientIdentifiersListAction + data object ToggleSearch : ClientIdentifiersListAction + data class ToggleShowMenu(val index: Int) : ClientIdentifiersListAction + data class ViewDocument(val uniqueKeyForHandleDocument: String?) : ClientIdentifiersListAction + data class DeleteDocument(val identifier: Int, val uniqueKeyForHandleDocument: String?) : ClientIdentifiersListAction + data class UploadAgain(val uniqueKeyForHandleDocument: String?) : ClientIdentifiersListAction + data object CloseDialog : ClientIdentifiersListAction + data object NavigateBack : ClientIdentifiersListAction + + data object Refresh : ClientIdentifiersListAction +} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListRoute.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListRoute.kt deleted file mode 100644 index 7407729bb12..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListRoute.kt +++ /dev/null @@ -1,38 +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.clientIdentitiesList - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import kotlinx.serialization.Serializable - -@Serializable -data class ClientIdentitiesListRoute( - val clientId: Int = -1, -) - -fun NavGraphBuilder.clientIdentitiesListDestination( - addNewClientIdentity: (Int) -> Unit, - navController: NavController, -) { - composable { - ClientIdentitiesListScreenRoute( - addNewClientIdentity = addNewClientIdentity, - navController = navController, - ) - } -} - -fun NavController.navigateToClientIdentifiersScreen( - clientId: Int, -) { - this.navigate(ClientIdentitiesListRoute(clientId = clientId)) -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListViewModel.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListViewModel.kt deleted file mode 100644 index 151bdd6eb06..00000000000 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentitiesList/ClientIdentitiesListViewModel.kt +++ /dev/null @@ -1,206 +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.clientIdentitiesList - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute -import com.mifos.core.common.utils.DataState -import com.mifos.core.data.repository.ClientIdentifiersRepository -import com.mifos.core.data.util.NetworkMonitor -import com.mifos.core.domain.useCases.DeleteIdentifierUseCase -import com.mifos.core.model.objects.noncoreobjects.Identifier -import com.mifos.core.ui.util.BaseViewModel -import com.mifos.feature.client.clientIdentitiesList.ClientIdentitiesListEvent.AddNewClientIdentity -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -class ClientIdentitiesListViewModel( - private val repository: ClientIdentifiersRepository, - private val deleteClientIdentifierUseCase: DeleteIdentifierUseCase, - private val networkMonitor: NetworkMonitor, - savedStateHandle: SavedStateHandle, -) : BaseViewModel( - initialState = ClientIdentitiesListState(), -) { - private val route = savedStateHandle.toRoute() - - override fun handleAction(action: ClientIdentitiesListAction) { - when (action) { - ClientIdentitiesListAction.AddNewClientIdentity -> sendEvent( - AddNewClientIdentity( - route.clientId, - ), - ) - - is ClientIdentitiesListAction.ToggleShowMenu -> mutableStateFlow.update { - it.copy( - currentExpandedItem = action.index, - expandClientIdentity = !it.expandClientIdentity, - ) - } - - ClientIdentitiesListAction.UploadAgain -> { - } - - ClientIdentitiesListAction.ViewDocument -> sendEvent(ClientIdentitiesListEvent.ViewDocument) - - is ClientIdentitiesListAction.DeleteDocument -> { - deleteClientIdentity(route.clientId, action.identifier) - } - - ClientIdentitiesListAction.ToggleSearch -> { - mutableStateFlow.update { - it.copy(isSearchBarActive = !it.isSearchBarActive) - } - } - - ClientIdentitiesListAction.CloseDialog -> { - mutableStateFlow.update { - it.copy(dialogState = null) - } - } - - ClientIdentitiesListAction.Refresh -> checkInternetAndFetchIdentities() - } - } - - init { - checkInternetAndFetchIdentities() - } - - private fun checkInternetAndFetchIdentities() { - viewModelScope.launch { - mutableStateFlow.update { - it.copy(dialogState = ClientIdentitiesListState.DialogState.Loading) - } - checkNetworkConnection() - } - } - - private suspend fun checkNetworkConnection() { - networkMonitor.isOnline.collect { status -> - when (status) { - true -> getClientIdentities(route.clientId) - false -> { - mutableStateFlow.update { - it.copy(dialogState = ClientIdentitiesListState.DialogState.NoInternet) - } - } - } - } - } - - private suspend fun getClientIdentities(clientId: Int) { - repository.getClientIdentifiers(clientId).collect { dataState -> - when (dataState) { - is DataState.Error -> { - mutableStateFlow.update { - it.copy( - dialogState = ClientIdentitiesListState.DialogState.Error( - dataState.message ?: "An unknown error occured", - ), - ) - } - } - - DataState.Loading -> mutableStateFlow.update { - it.copy(dialogState = ClientIdentitiesListState.DialogState.Loading) - } - - is DataState.Success -> { - val sortedList = dataState.data.sortedWith( - compareBy( - { identifier -> - val s = identifier.status?.lowercase() ?: "" - if (s.contains("active") && !s.contains("inactive")) 0 else 1 - }, - { identifier -> - identifier.description?.lowercase() ?: "" - }, - ), - ) - - mutableStateFlow.update { - it.copy( - dialogState = null, - clientIdentitiesList = sortedList, - ) - } - } - } - } - } - - private fun deleteClientIdentity(clientId: Int, identifierId: Int) { - viewModelScope.launch { - deleteClientIdentifierUseCase.invoke(clientId, identifierId).collect { state -> - when (state) { - is DataState.Error -> { - mutableStateFlow.update { - it.copy( - dialogState = ClientIdentitiesListState.DialogState.Error( - state.message ?: "An unknown error occured", - ), - ) - } - } - - DataState.Loading -> mutableStateFlow.update { - it.copy(dialogState = ClientIdentitiesListState.DialogState.Loading) - } - - is DataState.Success -> { - mutableStateFlow.update { - it.copy( - dialogState = ClientIdentitiesListState.DialogState.DeletedSuccessfully( - identifierId, - ), - ) - } - getClientIdentities(route.clientId) - } - } - } - } - } -} - -data class ClientIdentitiesListState( - val isSearchBarActive: Boolean = false, - val clientIdentitiesList: List = emptyList(), - val currentExpandedItem: Int = -1, - val expandClientIdentity: Boolean = false, - val dialogState: DialogState? = null, -) { - sealed interface DialogState { - data class Error(val message: String) : DialogState - data object Loading : DialogState - data object NoInternet : DialogState - data class DeletedSuccessfully(val id: Int) : DialogState - } -} - -sealed interface ClientIdentitiesListEvent { - data object ViewDocument : ClientIdentitiesListEvent - data class AddNewClientIdentity(val id: Int) : ClientIdentitiesListEvent -} - -sealed interface ClientIdentitiesListAction { - data object AddNewClientIdentity : ClientIdentitiesListAction - data object ToggleSearch : ClientIdentitiesListAction - data class ToggleShowMenu(val index: Int) : ClientIdentitiesListAction - data object ViewDocument : ClientIdentitiesListAction - data class DeleteDocument(val identifier: Int) : ClientIdentitiesListAction - data object UploadAgain : ClientIdentitiesListAction - data object CloseDialog : ClientIdentitiesListAction - data object Refresh : ClientIdentitiesListAction -} diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt index 787bb35c0f1..4a2928b9355 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/di/ClientModule.kt @@ -23,8 +23,8 @@ import com.mifos.feature.client.clientDocuments.ClientDocumentsViewModel import com.mifos.feature.client.clientEditDetails.ClientEditDetailsViewModel import com.mifos.feature.client.clientEditProfile.ClientProfileEditViewModel import com.mifos.feature.client.clientGeneral.ClientProfileGeneralViewmodel -import com.mifos.feature.client.clientIdentifiers.ClientIdentifiersViewModel -import com.mifos.feature.client.clientIdentitiesList.ClientIdentitiesListViewModel +import com.mifos.feature.client.clientIdentifiersAddUpdate.ClientIdentifiersAddUpdateViewModel +import com.mifos.feature.client.clientIdentifiersList.ClientIdentifiersListViewModel import com.mifos.feature.client.clientLoanAccounts.ClientLoanAccountsViewModel import com.mifos.feature.client.clientPinpoint.PinPointClientViewModel import com.mifos.feature.client.clientProfile.ClientProfileViewModel @@ -51,7 +51,6 @@ import org.koin.dsl.module val ClientModule = module { viewModelOf(::ClientChargesViewModel) viewModelOf(::ClientDetailsViewModel) - viewModelOf(::ClientIdentifiersViewModel) viewModelOf(::ClientAddressViewModel) viewModelOf(::ClientListViewModel) viewModelOf(::PinPointClientViewModel) @@ -73,7 +72,6 @@ val ClientModule = module { viewModelOf(::FixedDepositAccountViewModel) viewModelOf(::ClientCollateralViewModel) viewModelOf(::ClientLoanAccountsViewModel) - viewModelOf(::ClientIdentitiesListViewModel) viewModelOf(::ClientApplyNewApplicationsViewModel) viewModelOf(::ClientSignatureViewModel) viewModelOf(::ClientUpcomingChargesViewmodel) @@ -81,6 +79,8 @@ val ClientModule = module { viewModelOf(::ClientAddDocumentScreenViewmodel) viewModelOf(::DocumentPreviewScreenViewModel) viewModelOf(::ShareAccountsViewModel) + viewModelOf(::ClientIdentifiersListViewModel) + viewModelOf(::ClientIdentifiersAddUpdateViewModel) singleOf(::DocumentSelectAndUploadRepositoryImpl) { bind() diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt index 925ee76a73d..a4c645460ed 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientNavigation.kt @@ -45,9 +45,11 @@ import com.mifos.feature.client.clientEditProfile.clientEditProfileDestination import com.mifos.feature.client.clientEditProfile.navigateToClientProfileEditProfileRoute import com.mifos.feature.client.clientGeneral.clientProfileGeneralDestination import com.mifos.feature.client.clientGeneral.navigateToClientProfileGeneralRoute -import com.mifos.feature.client.clientIdentifiers.ClientIdentifiersScreen -import com.mifos.feature.client.clientIdentitiesList.clientIdentitiesListDestination -import com.mifos.feature.client.clientIdentitiesList.navigateToClientIdentifiersScreen +import com.mifos.feature.client.clientIdentifiersAddUpdate.clientIdentifiersAddUpdateDestination +import com.mifos.feature.client.clientIdentifiersAddUpdate.onNavigateToClientIdentifiersAddUpdateScreen +import com.mifos.feature.client.clientIdentifiersList.clientIdentifiersListDestination +import com.mifos.feature.client.clientIdentifiersList.navigateBackToUpdateClientIdentifiersListScreen +import com.mifos.feature.client.clientIdentifiersList.navigateToClientIdentifiersListScreen import com.mifos.feature.client.clientLoanAccounts.clientLoanAccountsDestination import com.mifos.feature.client.clientLoanAccounts.navigateToClientLoanAccountsRoute import com.mifos.feature.client.clientPinpoint.PinpointClientScreen @@ -115,7 +117,7 @@ fun NavGraphBuilder.clientNavGraph( addSavingsAccount = addSavingsAccount, charges = navController::navigateClientChargesScreen, documents = documents, - identifiers = navController::navigateClientIdentifierScreen, + identifiers = navController::navigateToClientIdentifiersListScreen, moreClientInfo = moreClientInfo, notes = notes, pinpointLocation = navController::navigateClientPinPointScreen, @@ -128,9 +130,10 @@ fun NavGraphBuilder.clientNavGraph( clientChargesRoute( onBackPressed = navController::popBackStack, ) - clientIdentifierRoute( - onDocumentClicked = onDocumentClicked, + clientIdentifiersAddUpdateDestination( onBackPressed = navController::popBackStack, + onUpdatedListBack = navController::navigateBackToUpdateClientIdentifiersListScreen, + navController = navController, ) clientPinPointRoute( onBackPressed = navController::popBackStack, @@ -156,7 +159,7 @@ fun NavGraphBuilder.clientNavGraph( onNavigateBack = navController::popBackStack, notes = notes, documents = navController::navigateToClientDocumentsRoute, - identifiers = navController::navigateToClientIdentifiersScreen, + identifiers = navController::navigateToClientIdentifiersListScreen, navigateToClientDetailsScreen = navController::navigateToClientDetailsProfileRoute, viewAddress = navController::navigateToClientAddressRoute, viewAssociatedAccounts = navController::navigateToClientProfileGeneralRoute, @@ -280,8 +283,9 @@ fun NavGraphBuilder.clientNavGraph( navigateToViewAccount = {}, navigateToMakeRepayment = {}, ) - clientIdentitiesListDestination( - addNewClientIdentity = {}, + clientIdentifiersListDestination( + addNewClientIdentity = navController::onNavigateToClientIdentifiersAddUpdateScreen, + onBackPress = navController::popBackStack, navController = navController, ) clientApplyNewApplicationRoute( @@ -356,21 +360,6 @@ fun NavGraphBuilder.clientDetailRoute( } } -fun NavGraphBuilder.clientIdentifierRoute( - onDocumentClicked: (Int, String) -> Unit, - onBackPressed: () -> Unit, -) { - composable( - route = ClientScreens.ClientIdentifierScreen.route, - arguments = listOf(navArgument(Constants.CLIENT_ID, builder = { type = NavType.IntType })), - ) { - ClientIdentifiersScreen( - onBackPressed = onBackPressed, - onDocumentClicked = { onDocumentClicked(it, Constants.ENTITY_TYPE_CLIENTS) }, - ) - } -} - fun NavGraphBuilder.clientChargesRoute( onBackPressed: () -> Unit, ) { @@ -444,10 +433,6 @@ fun NavController.navigateClientDetailsScreen(clientId: Int) { navigate(ClientScreens.ClientDetailScreen.argument(clientId)) } -fun NavController.navigateClientIdentifierScreen(clientId: Int) { - navigate(ClientScreens.ClientIdentifierScreen.argument(clientId)) -} - fun NavController.navigateClientChargesScreen(clientId: Int) { navigate(ClientScreens.ClientChargesScreen.argument(clientId)) } diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientScreens.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientScreens.kt index b3f12b25489..1fb4dc2eec6 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientScreens.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/navigation/ClientScreens.kt @@ -25,11 +25,6 @@ sealed class ClientScreens(val route: String) { fun argument(clientId: Int) = "client_charges_screen/$clientId" } - data object ClientIdentifierScreen : - ClientScreens("client_identifier_screen/{${Constants.CLIENT_ID}}") { - fun argument(clientId: Int) = "client_identifier_screen/$clientId" - } - data object ClientPinPointScreen : ClientScreens("client_pin_point_screen/{${Constants.CLIENT_ID}}") { fun argument(clientId: Int) = "client_pin_point_screen/$clientId" diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/ExtractErrorMessage.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/ExtractErrorMessage.kt new file mode 100644 index 00000000000..60a1ac64347 --- /dev/null +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/ExtractErrorMessage.kt @@ -0,0 +1,27 @@ +/* + * 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 kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject + +fun Map.extractErrorMessage(key: String): String? { + // Check in 'errors' array + val errors = this["errors"] + if (errors is JsonArray) { + val firstError = errors.firstOrNull()?.jsonObject + firstError?.get(key)?.let { msgElem -> + if (msgElem is JsonPrimitive && msgElem.isString) return msgElem.content + } + } + return null +} diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsClientIdIdentifiersTemplateResponse.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PdfViewer.kt similarity index 50% rename from core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsClientIdIdentifiersTemplateResponse.kt rename to feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PdfViewer.kt index 9b26a9348ec..964bfaeb37d 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/model/GetClientsClientIdIdentifiersTemplateResponse.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PdfViewer.kt @@ -7,19 +7,13 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.core.network.model +package com.mifos.feature.client.utils -import kotlinx.serialization.Serializable - -/** - * GetClientsClientIdIdentifiersTemplateResponse - * - * @param allowedDocumentTypes - */ - -@Serializable -data class GetClientsClientIdIdentifiersTemplateResponse( - - val allowedDocumentTypes: Set? = null, +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +@Composable +expect fun PdfPreview( + pdfBytes: ByteArray, + modifier: Modifier = Modifier, ) diff --git a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt index 8bde08df280..920e3e1af4c 100644 --- a/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt +++ b/feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt @@ -24,3 +24,9 @@ suspend fun ImageBitmap.toPlatformFile(fileName: String): PlatformFile { outFile.write(bytearray) return compressImage(outFile, fileName) } + +suspend fun ByteArray.toPlatformFile(fileName: String): PlatformFile { + val outFile = FileKit.filesDir / fileName + outFile.write(this) + return outFile +} diff --git a/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PdfViewer.desktop.kt b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PdfViewer.desktop.kt new file mode 100644 index 00000000000..b0d6e653ef5 --- /dev/null +++ b/feature/client/src/desktopMain/kotlin/com/mifos/feature/client/utils/PdfViewer.desktop.kt @@ -0,0 +1,45 @@ +/* + * 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 androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import com.mifos.core.ui.components.MifosViewPdf +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.rendering.PDFRenderer +import org.jetbrains.skia.Image +import java.awt.image.BufferedImage +import javax.imageio.ImageIO + +@Composable +actual fun PdfPreview(pdfBytes: ByteArray, modifier: Modifier) { + val bitmaps = remember { mutableStateListOf() } + + LaunchedEffect(pdfBytes) { + bitmaps.clear() + PDDocument.load(pdfBytes).use { doc -> + val renderer = PDFRenderer(doc) + for (i in 0 until doc.numberOfPages) { + val awtImg: BufferedImage = renderer.renderImageWithDPI(i, 150f) + val byteOut = java.io.ByteArrayOutputStream() + ImageIO.write(awtImg, "png", byteOut) + val skiaImage = Image.makeFromEncoded(byteOut.toByteArray()) + bitmaps.add(skiaImage.toComposeImageBitmap()) + } + } + } + + MifosViewPdf(bitmaps, modifier = modifier) +} diff --git a/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PdfViewer.native.kt b/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PdfViewer.native.kt new file mode 100644 index 00000000000..92fe471887e --- /dev/null +++ b/feature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PdfViewer.native.kt @@ -0,0 +1,29 @@ +/* + * 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 androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.UIKitView +import io.github.vinceglb.filekit.utils.toNSData +import platform.PDFKit.PDFDocument +import platform.PDFKit.PDFView + +@Composable +actual fun PdfPreview(pdfBytes: ByteArray, modifier: Modifier) { + val data = pdfBytes.toNSData() + val pdfDocument = PDFDocument(data) + val pdfView = PDFView().apply { + document = pdfDocument + autoScales = true + } + + UIKitView(factory = { pdfView }, modifier = modifier) +} diff --git a/feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt b/feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt index 9773c2f1fe3..2e602ab95d2 100644 --- a/feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt +++ b/feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt @@ -12,7 +12,7 @@ package com.mifos.feature.document.documentDialog import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.mifos.core.common.utils.DataState -import com.mifos.core.data.repository.DocumentDialogRepository +import com.mifos.core.data.repository.DocumentCreateUpdateRepository import com.mifos.core.ui.util.multipartRequestBody import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.PlatformFile @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class DocumentDialogViewModel( - private val repository: DocumentDialogRepository, + private val repository: DocumentCreateUpdateRepository, ) : ViewModel() { private val _documentDialogUiState = diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b778d1faffc..90fc201b806 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -151,6 +151,7 @@ jbCoreBundle = "1.0.1" jbSavedState = "1.2.2" lottie = "6.6.7" compottie = "2.0.0-rc05" +pdfbox = "2.0.30" # Desktop Version packageName = "AndroidClient" @@ -406,6 +407,9 @@ window-size = { group = "dev.chrisbanes.material3", name = "material3-window-siz compottie-lite = { module = "io.github.alexzhirkevich:compottie-lite", version.ref = "compottie" } compottie-resources = { module = "io.github.alexzhirkevich:compottie-resources", version.ref = "compottie" } +pdfbox = { module = "org.apache.pdfbox:pdfbox", version.ref = "pdfbox" } + + [plugins] # Android & Kotlin Plugins