From 8db915a2715ebec62566f89920430394125e05ba Mon Sep 17 00:00:00 2001 From: Arin Yadav Date: Thu, 28 Aug 2025 23:52:04 +0530 Subject: [PATCH 1/2] Client Add/Edit Note --- .../navigation/navigation/FeatureNavHost.kt | 2 +- .../com/mifos/core/common/utils/DateHelper.kt | 20 ++ .../mifos/core/model/objects/notes/Note.kt | 4 +- .../com/mifos/core/network/GenericResponse.kt | 16 +- .../composeResources/values/res.xml | 13 + .../note/addEditNotes/AddEditNoteRoute.kt | 36 +++ .../note/addEditNotes/AddEditNoteScreen.kt | 220 +++++++++++++++ .../note/addEditNotes/AddEditNoteViewModel.kt | 260 ++++++++++++++++++ .../com/mifos/feature/note/di/NoteModule.kt | 2 + .../feature/note/navigation/NoteNavigation.kt | 59 ++-- .../feature/note/navigation/NoteScreens.kt | 27 -- .../com/mifos/feature/note/notes/NoteRoute.kt | 39 +++ .../mifos/feature/note/notes/NoteScreen.kt | 49 ++-- .../mifos/feature/note/notes/NoteViewModel.kt | 92 ++++--- 14 files changed, 704 insertions(+), 135 deletions(-) create mode 100644 feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt create mode 100644 feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt create mode 100644 feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt delete mode 100644 feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteScreens.kt create mode 100644 feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt diff --git a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt index 91f97a2d5e0..0396107f59c 100644 --- a/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt +++ b/cmp-navigation/src/commonMain/kotlin/cmp/navigation/navigation/FeatureNavHost.kt @@ -46,8 +46,8 @@ import com.mifos.feature.loan.navigation.loanNavGraph import com.mifos.feature.loan.navigation.navigateToGroupLoanScreen import com.mifos.feature.loan.navigation.navigateToLoanAccountScreen import com.mifos.feature.loan.navigation.navigateToLoanAccountSummaryScreen -import com.mifos.feature.note.navigation.navigateToNoteScreen import com.mifos.feature.note.navigation.noteNavGraph +import com.mifos.feature.note.notes.navigateToNoteScreen import com.mifos.feature.offline.navigation.offlineNavGraph import com.mifos.feature.path.tracking.navigation.pathTrackingNavGraph import com.mifos.feature.report.navigation.reportNavGraph diff --git a/core/common/src/commonMain/kotlin/com/mifos/core/common/utils/DateHelper.kt b/core/common/src/commonMain/kotlin/com/mifos/core/common/utils/DateHelper.kt index 249a0c8de1c..c02cd359064 100644 --- a/core/common/src/commonMain/kotlin/com/mifos/core/common/utils/DateHelper.kt +++ b/core/common/src/commonMain/kotlin/com/mifos/core/common/utils/DateHelper.kt @@ -409,4 +409,24 @@ object DateHelper { require(month in 1..12) { "Month should be between 1 and 12" } return Month.entries[month - 1].name.lowercase().replaceFirstChar { it.uppercase() } } + + /** + * This method is used to Convert IOS string date into dd MM yyyy formate + * @param isoString take IOS date as a String + * Example : ISO string 2025-08-28T16:02:32.242705+05:30 and return 28 08 2025 + */ + fun formatIsoDateToDdMmYyyy(isoString: String): String { + // Parse the string into an Instant + val instant = Instant.parse(isoString) + + // Convert to LocalDateTime in system timezone + val localDate = instant.toLocalDateTime(TimeZone.currentSystemDefault()).date + + val day = localDate.dayOfMonth.toString().padStart(2, '0') + val monthName = localDate.month.name.lowercase() + .replaceFirstChar { it.uppercase() } // "January", "February", etc. + val year = localDate.year + + return "$day $monthName $year" + } } diff --git a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/notes/Note.kt b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/notes/Note.kt index c090a974159..7fc0025721a 100644 --- a/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/notes/Note.kt +++ b/core/model/src/commonMain/kotlin/com/mifos/core/model/objects/notes/Note.kt @@ -34,7 +34,7 @@ data class Note( val createdByUsername: String? = null, - val createdOn: Long? = null, + val createdOn: String? = null, val id: Long? = null, @@ -44,6 +44,6 @@ data class Note( val updatedByUsername: String? = null, - val updatedOn: Long? = null, + val updatedOn: String? = null, ) diff --git a/core/network/src/commonMain/kotlin/com/mifos/core/network/GenericResponse.kt b/core/network/src/commonMain/kotlin/com/mifos/core/network/GenericResponse.kt index aff3ed915e8..8f89af54312 100644 --- a/core/network/src/commonMain/kotlin/com/mifos/core/network/GenericResponse.kt +++ b/core/network/src/commonMain/kotlin/com/mifos/core/network/GenericResponse.kt @@ -9,14 +9,14 @@ */ package com.mifos.core.network -/** - * Created by ishankhanna on 24/06/14. - */ -class GenericResponse { - var responseFields = HashMap() +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + +@Serializable +data class GenericResponse( + val responseFields: Map = emptyMap(), +) { override fun toString(): String { - return "GenericResponse{" + - "responseFields=" + responseFields + - '}' + return "GenericResponse{responseFields=$responseFields}" } } diff --git a/feature/note/src/commonMain/composeResources/values/res.xml b/feature/note/src/commonMain/composeResources/values/res.xml index b7d7003a53e..1d3c9cd5ffb 100644 --- a/feature/note/src/commonMain/composeResources/values/res.xml +++ b/feature/note/src/commonMain/composeResources/values/res.xml @@ -23,4 +23,17 @@ Item "Unexpected error" Once deleted, this note cannot be recovered. Do you want to continue? + + Write a Note + Edit a Note + Add Note + Update Note + Add + Update + Back + Note Updated Successfully + Note Added Successfully + Confirm + Warning + Discard changes? Unsaved data will be lost. \ No newline at end of file diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt new file mode 100644 index 00000000000..5c61d4e6b6b --- /dev/null +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt @@ -0,0 +1,36 @@ +/* + * 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.note.addEditNotes + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import kotlinx.serialization.Serializable + +@Serializable +data class AddEditNoteRoute( + val resourceId: Int, + val resourceType: String?, + val noteId: Long?, +) + +fun NavGraphBuilder.addEditNoteRoute( + onBackPressed: (Boolean?) -> Unit, +) { + composable { + AddEditNoteScreen( + onBackPressed = onBackPressed, + ) + } +} + +fun NavController.navigateToAddEditNoteScreen(resourceId: Int, resourceType: String?, noteId: Long?) { + this.navigate(AddEditNoteRoute(resourceId, resourceType, noteId)) +} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt new file mode 100644 index 00000000000..32db4b02057 --- /dev/null +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt @@ -0,0 +1,220 @@ +/* + * 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.note.addEditNotes + +import androidclient.feature.note.generated.resources.Res +import androidclient.feature.note.generated.resources.feature_note_button_back +import androidclient.feature.note.generated.resources.feature_note_button_confirm +import androidclient.feature.note.generated.resources.feature_note_dialog_warning +import androidclient.feature.note.generated.resources.feature_note_dialog_warning_message +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.core.designsystem.component.MifosCircularProgress +import com.mifos.core.designsystem.component.MifosOutlinedButton +import com.mifos.core.designsystem.component.MifosScaffold +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.MifosErrorComponent +import com.mifos.core.ui.util.EventsEffect +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun AddEditNoteScreen( + onBackPressed: (Boolean?) -> Unit, + viewModel: AddEditNoteViewModel = koinViewModel(), +) { + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + EventsEffect(viewModel.eventFlow) { event -> + when (event) { + AddEditNoteEvent.NavigateBack -> onBackPressed(state.isRequiredUpdateListNote) + } + } + + AddEditNoteScreenScaffold( + state = state, + onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + ) + + AddEditNoteScreenDialog( + onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, + state = state, + ) +} + +@Composable +fun AddEditNoteScreenDialog( + onAction: (AddEditNoteAction) -> Unit, + state: AddEditNoteState, +) { + when (state.dialogState) { + is AddEditNoteState.DialogState.Error -> { + MifosErrorComponent( + isNetworkConnected = state.networkConnection, + message = stringResource(state.dialogState.message), + isRetryEnabled = true, + onRetry = { + onAction(AddEditNoteAction.OnRetry) + }, + ) + } + AddEditNoteState.DialogState.Loading -> { + MifosCircularProgress() + } + AddEditNoteState.DialogState.MisTouchBack -> { + MifosAlertDialog( + onDismissRequest = { + onAction(AddEditNoteAction.DismissDialog) + }, + confirmationText = stringResource(Res.string.feature_note_button_confirm), + dialogTitle = stringResource(Res.string.feature_note_dialog_warning), + dialogText = stringResource(Res.string.feature_note_dialog_warning_message), + onConfirmation = { + onAction(AddEditNoteAction.NavigateBack) + }, + icon = null, + ) + } + null -> Unit + } +} + +@Composable +internal fun AddEditNoteScreenScaffold( + onAction: (AddEditNoteAction) -> Unit, + state: AddEditNoteState, +) { + MifosScaffold( + title = "", + onBackPressed = { onAction(AddEditNoteAction.MisTouchBackDialog) }, + ) { paddingValues -> + + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + AddEditNote( + state = state, + onAction = onAction, + ) + } + } +} + +@Composable +private fun AddEditNote( + state: AddEditNoteState, + onAction: (AddEditNoteAction) -> Unit, +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .verticalScroll(scrollState) + .fillMaxWidth() + .padding(horizontal = DesignToken.spacing.large), + verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.large), + ) { + Text( + text = stringResource(state.title), + style = MifosTypography.labelLargeEmphasized, + color = MaterialTheme.colorScheme.onSurface, + ) + + OutlinedTextField( + value = state.textFieldNotesPayload.note ?: "", + onValueChange = { + onAction(AddEditNoteAction.TextFieldNotesPayload(state.textFieldNotesPayload.copy(note = it))) + }, + singleLine = false, + shape = DesignToken.shapes.large, + label = { + Text( + text = stringResource(state.label), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 600.dp), + textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Start), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer, + ), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignToken.spacing.extraSmall), + ) { + MifosOutlinedButton( + modifier = Modifier.weight(1f), + text = { + Text( + text = stringResource(Res.string.feature_note_button_back), + ) + }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.primary, + containerColor = MaterialTheme.colorScheme.onPrimary, + ), + onClick = { + onAction(AddEditNoteAction.MisTouchBackDialog) + }, + ) + + MifosOutlinedButton( + modifier = Modifier.weight(1f), + text = { + Text(text = stringResource(state.addUpdateButton)) + }, + onClick = { + if (state.editEnabled) { + onAction(AddEditNoteAction.EditNote(state.textFieldNotesPayload)) + } else { + onAction(AddEditNoteAction.AddNote(state.textFieldNotesPayload)) + } + if (state.isError) { + onAction(AddEditNoteAction.NavigateBack) + } + }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primary, + ), + enabled = !state.textFieldNotesPayload.note.isNullOrEmpty(), + ) + } + } +} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt new file mode 100644 index 00000000000..c6acd175e57 --- /dev/null +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt @@ -0,0 +1,260 @@ +/* + * 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.note.addEditNotes + +import androidclient.feature.note.generated.resources.Res +import androidclient.feature.note.generated.resources.feature_note_Unexpected_error +import androidclient.feature.note.generated.resources.feature_note_add_note +import androidclient.feature.note.generated.resources.feature_note_add_success +import androidclient.feature.note.generated.resources.feature_note_button_add +import androidclient.feature.note.generated.resources.feature_note_button_update +import androidclient.feature.note.generated.resources.feature_note_edit_note_label +import androidclient.feature.note.generated.resources.feature_note_edit_success +import androidclient.feature.note.generated.resources.feature_note_update_note +import androidclient.feature.note.generated.resources.feature_note_write_note_label +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.mifos.core.common.utils.DataState +import com.mifos.core.data.repositoryImp.NoteRepositoryImp +import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.model.objects.payloads.NotesPayload +import com.mifos.core.ui.util.BaseViewModel +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.StringResource + +class AddEditNoteViewModel( + private val repository: NoteRepositoryImp, + savedStateHandle: SavedStateHandle, + private val networkMonitor: NetworkMonitor, +) : BaseViewModel( + initialState = AddEditNoteState(), +) { + private val route = savedStateHandle.toRoute() + + init { + getAddEditNoteOptionsAndObserveNetwork() + + if (route.noteId != null) { + viewModelScope.launch { + loadSingleNote() + } + mutableStateFlow.update { + it.copy( + editEnabled = true, + addUpdateButton = Res.string.feature_note_button_update, + label = Res.string.feature_note_edit_note_label, + title = Res.string.feature_note_update_note, + ) + } + } + } + + private fun getAddEditNoteOptionsAndObserveNetwork() { + viewModelScope.launch { + observeNetwork() + } + } + + private fun observeNetwork() { + viewModelScope.launch { + networkMonitor.isOnline.collect { isConnected -> + mutableStateFlow.update { it.copy(networkConnection = isConnected) } + } + } + } + + private suspend fun loadSingleNote() { + route.resourceType?.let { type -> + route.noteId?.let { id -> + repository.retrieveNote(type, route.resourceId.toLong(), id) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy(dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error)) + } + } + DataState.Loading -> { + mutableStateFlow.update { + it.copy(dialogState = AddEditNoteState.DialogState.Loading) + } + } + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + textFieldNotesPayload = NotesPayload(dataState.data.note), + notesPayloadInitialData = dataState.data.note, + ) + } + } + } + } + } + } + } + + private suspend fun addNote(notesPayload: NotesPayload) { + runCatching { + route.resourceType?.let { type -> + repository.addNewNote(route.resourceType, route.resourceId.toLong(), notesPayload) + } + }.onFailure { + mutableStateFlow.update { + it.copy( + dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error), + isError = false, + ) + } + }.onSuccess { + mutableStateFlow.update { + it.copy( + isError = true, + dialogState = null, + successMessage = Res.string.feature_note_add_success, + notesPayloadInitialData = state.textFieldNotesPayload.note, + ) + } + } + } + + private suspend fun editNote(notesPayload: NotesPayload) { + runCatching { + route.resourceType?.let { type -> + route.noteId?.let { id -> + repository.updateNote(type, route.resourceId.toLong(), id, notesPayload) + } + } + }.onFailure { + mutableStateFlow.update { + it.copy( + dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error), + isError = false, + ) + } + }.onSuccess { + mutableStateFlow.update { + it.copy( + isError = true, + dialogState = null, + successMessage = Res.string.feature_note_edit_success, + notesPayloadInitialData = state.textFieldNotesPayload.note, + ) + } + } + } + + override fun handleAction(action: AddEditNoteAction) { + when (action) { + AddEditNoteAction.NavigateBack -> { + mutableStateFlow.update { + it.copy( + isRequiredUpdateListNote = true, + ) + } + sendEvent(AddEditNoteEvent.NavigateBack) + } + is AddEditNoteAction.AddNote -> { + viewModelScope.launch { + addNote(action.notesPayload) + } + } + is AddEditNoteAction.EditNote -> { + viewModelScope.launch { + editNote(action.notesPayload) + } + } + AddEditNoteAction.OnRetry -> { + if (state.editEnabled) { + viewModelScope.launch { + loadSingleNote() + editNote(state.textFieldNotesPayload) + } + } else { + viewModelScope.launch { + addNote(state.textFieldNotesPayload) + } + } + } + + is AddEditNoteAction.TextFieldNotesPayload -> { + mutableStateFlow.update { + it.copy( + textFieldNotesPayload = action.notesPayload, + ) + } + } + + AddEditNoteAction.DismissDialog -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + showDialog = false, + ) + } + } + + AddEditNoteAction.MisTouchBackDialog -> { + if (state.notesPayloadInitialData != state.textFieldNotesPayload.note) { + mutableStateFlow.update { + it.copy( + dialogState = AddEditNoteState.DialogState.MisTouchBack, + showDialog = true, + ) + } + } else { + mutableStateFlow.update { + it.copy( + isRequiredUpdateListNote = false, + ) + } + sendEvent(AddEditNoteEvent.NavigateBack) + } + } + } + } +} + +data class AddEditNoteState( + val editEnabled: Boolean = false, + val successMessage: StringResource? = null, + val addUpdateButton: StringResource = Res.string.feature_note_button_add, + val label: StringResource = Res.string.feature_note_write_note_label, + val title: StringResource = Res.string.feature_note_add_note, + val textFieldNotesPayload: NotesPayload = NotesPayload(null), + val notesPayloadInitialData: String? = null, + val showDialog: Boolean = false, + val isError: Boolean = true, + val dialogState: DialogState? = null, + val networkConnection: Boolean = false, + val isRequiredUpdateListNote: Boolean = false, +) { + sealed interface DialogState { + data class Error(val message: StringResource) : DialogState + data object Loading : DialogState + data object MisTouchBack : DialogState + } +} + +sealed interface AddEditNoteEvent { + data object NavigateBack : AddEditNoteEvent +} + +sealed interface AddEditNoteAction { + data object NavigateBack : AddEditNoteAction + data object OnRetry : AddEditNoteAction + data class AddNote(val notesPayload: NotesPayload) : AddEditNoteAction + data class EditNote(val notesPayload: NotesPayload) : AddEditNoteAction + data object DismissDialog : AddEditNoteAction + data object MisTouchBackDialog : AddEditNoteAction + data class TextFieldNotesPayload(val notesPayload: NotesPayload) : AddEditNoteAction +} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/di/NoteModule.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/di/NoteModule.kt index a92f4a8b3aa..c7b708f1b20 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/di/NoteModule.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/di/NoteModule.kt @@ -9,10 +9,12 @@ */ package com.mifos.feature.note.di +import com.mifos.feature.note.addEditNotes.AddEditNoteViewModel import com.mifos.feature.note.notes.NoteViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val NoteModule = module { viewModelOf(::NoteViewModel) + viewModelOf(::AddEditNoteViewModel) } diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt index 1357a16acd5..61d99eff9c9 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt @@ -11,54 +11,39 @@ package com.mifos.feature.note.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable import androidx.navigation.navigation -import com.mifos.feature.note.notes.NoteScreen +import com.mifos.feature.note.addEditNotes.addEditNoteRoute +import com.mifos.feature.note.addEditNotes.navigateToAddEditNoteScreen +import com.mifos.feature.note.notes.NoteRoute +import com.mifos.feature.note.notes.noteRoute +import kotlinx.serialization.Serializable + +@Serializable +object NoteNavigationRoute + +object Update { + const val NOTE_LIST = "update note list" +} fun NavGraphBuilder.noteNavGraph( navController: NavController, onBackPressed: () -> Unit, ) { navigation( - startDestination = NoteScreenRoute::class, + startDestination = NoteRoute::class, ) { - noteScreen( + noteRoute( onNavigateBack = onBackPressed, - onNavigateNext = navController::navigateToAddNoteScreen, + onNavigateAddEditNote = navController::navigateToAddEditNoteScreen, ) - addNoteRoute( - onBackPressed = navController::popBackStack, + addEditNoteRoute( + onBackPressed = { updateNoteList -> + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(Update.NOTE_LIST, updateNoteList) + navController.popBackStack() + }, ) } } - -fun NavGraphBuilder.noteScreen( - onNavigateBack: () -> Unit, - onNavigateNext: (Int, String?) -> Unit, -) { - composable { - NoteScreen( - onNavigateBack = onNavigateBack, - onNavigateNext = onNavigateNext, - ) - } -} - -fun NavGraphBuilder.addNoteRoute( - onBackPressed: () -> Unit, -) { - composable { -// AddNoteScreen( -// onBackPressed = onBackPressed, -// ) - } -} - -fun NavController.navigateToNoteScreen(entityId: Int, entityType: String?) { - this.navigate(NoteScreenRoute(entityId, entityType)) -} - -fun NavController.navigateToAddNoteScreen(entityId: Int, entityType: String?) { - this.navigate(AddNoteScreenRoute(entityId, entityType)) -} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteScreens.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteScreens.kt deleted file mode 100644 index 360f2adcf98..00000000000 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteScreens.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.note.navigation - -import kotlinx.serialization.Serializable - -@Serializable -object NoteNavigationRoute - -@Serializable -data class NoteScreenRoute( - val entityId: Int, - val entityType: String?, -) - -@Serializable -data class AddNoteScreenRoute( - val entityId: Int, - val entityType: String?, -) diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt new file mode 100644 index 00000000000..90d11e82808 --- /dev/null +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt @@ -0,0 +1,39 @@ +/* + * 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.note.notes + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.mifos.feature.note.navigation.Update +import kotlinx.serialization.Serializable + +@Serializable +data class NoteRoute( + val resourceId: Int, + val resourceType: String?, +) + +fun NavGraphBuilder.noteRoute( + onNavigateBack: () -> Unit, + onNavigateAddEditNote: (Int, String?, Long?) -> Unit, +) { + composable { updateNoteList -> + NoteScreenScaffold( + onNavigateBack = onNavigateBack, + onNavigateAddEditNote = onNavigateAddEditNote, + updateNoteList = updateNoteList.savedStateHandle.get(Update.NOTE_LIST), + ) + } +} + +fun NavController.navigateToNoteScreen(entityId: Int, entityType: String?) { + this.navigate(NoteRoute(entityId, entityType)) +} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt index c189b8b8226..8639a11cec1 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt @@ -47,6 +47,7 @@ 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 @@ -68,26 +69,36 @@ import com.mifos.core.ui.components.MifosErrorComponent import com.mifos.core.ui.util.DevicePreview import com.mifos.core.ui.util.EventsEffect import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @Composable -internal fun NoteScreen( +internal fun NoteScreenScaffold( onNavigateBack: () -> Unit, - onNavigateNext: (Int, String?) -> Unit, + onNavigateAddEditNote: (Int, String?, Long?) -> Unit, + updateNoteList: Boolean? = null, viewModel: NoteViewModel = koinViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() + LaunchedEffect(updateNoteList) { + if (updateNoteList == true) { + viewModel.trySendAction(NoteAction.OnRetry) + } + } + EventsEffect(viewModel.eventFlow) { event -> when (event) { NoteEvent.NavigateBack -> onNavigateBack() - NoteEvent.NavigateNext -> onNavigateNext(state.entityId, state.entityType) + NoteEvent.NavigateAddNote -> onNavigateAddEditNote(state.resourceId, state.resourceType, null) + NoteEvent.NavigateEditNote -> onNavigateAddEditNote(state.resourceId, state.resourceType, state.expandedNoteId) } } - if (!state.isDeleteError) { - NoteScreen( + if (!state.isError && state.notes.isNotEmpty()) { + NoteScreenScaffold( state = state, onAction = remember(viewModel) { { viewModel.trySendAction(it) } }, ) @@ -124,7 +135,7 @@ private fun NoteScreenDialog( @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun NoteScreen( +internal fun NoteScreenScaffold( onAction: (NoteAction) -> Unit, state: NoteState, modifier: Modifier = Modifier, @@ -190,7 +201,7 @@ private fun NoteContent( imageVector = MifosIcons.Add, contentDescription = null, modifier.clickable { - onAction(NoteAction.OnNext) + onAction(NoteAction.OnClickAddScreen) }.size(DesignToken.sizes.iconAverage), ) } @@ -230,7 +241,7 @@ private fun NoteContent( } } } else { - items(state.notes) { note -> + items(state.notes.reversed()) { note -> NoteItem( id = note.id, note = note.note, @@ -251,7 +262,7 @@ private fun NoteItem( note: String?, onAction: (NoteAction) -> Unit, createdByUsername: String?, - createdOn: Long?, + createdOn: String?, state: NoteState, ) { var shape by remember { mutableStateOf(RoundedCornerShape(0.dp)) } @@ -307,7 +318,7 @@ private fun NoteItem( ) Text( - text = DateHelper.getDateAsStringFromLong(createdOn!!), + text = DateHelper.formatIsoDateToDdMmYyyy(createdOn ?: "Not found"), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface, ) @@ -365,7 +376,9 @@ private fun ContextualActions( verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.medium), ) { Row( - modifier = Modifier.clickable {}, + modifier = Modifier.clickable { + onAction(NoteAction.OnClickEditScreen) + }, horizontalArrangement = Arrangement.spacedBy( DesignToken.spacing.medium, ), @@ -429,10 +442,10 @@ internal val demoNotes = listOf( note = "This is the first demo note.", createdById = 1001, createdByUsername = "creator_1", - createdOn = Clock.System.now().toEpochMilliseconds(), + createdOn = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString(), updatedById = 1002, updatedByUsername = "updater_1", - updatedOn = Clock.System.now().toEpochMilliseconds(), + updatedOn = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString(), ), Note( id = 2, @@ -440,10 +453,10 @@ internal val demoNotes = listOf( note = "This is the second demo note.", createdById = 1003, createdByUsername = "creator_2", - createdOn = Clock.System.now().toEpochMilliseconds(), + createdOn = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString(), updatedById = 1004, updatedByUsername = "updater_2", - updatedOn = Clock.System.now().toEpochMilliseconds(), + updatedOn = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString(), ), Note( id = 3, @@ -451,17 +464,17 @@ internal val demoNotes = listOf( note = "This is the third demo note.", createdById = 1005, createdByUsername = "creator_3", - createdOn = Clock.System.now().toEpochMilliseconds(), + createdOn = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString(), updatedById = 1006, updatedByUsername = "updater_3", - updatedOn = Clock.System.now().toEpochMilliseconds(), + updatedOn = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).toString(), ), ) @DevicePreview @Composable fun PreviewSuccessNoteScreen() { - NoteScreen( + NoteScreenScaffold( onAction = {}, state = NoteState(notes = demoNotes), ) diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt index b6d0a0d0ec0..c7dab35fa56 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt @@ -17,7 +17,6 @@ import com.mifos.core.data.repositoryImp.NoteRepositoryImp import com.mifos.core.data.util.NetworkMonitor import com.mifos.core.model.objects.notes.Note import com.mifos.core.ui.util.BaseViewModel -import com.mifos.feature.note.navigation.NoteScreenRoute import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -28,17 +27,16 @@ class NoteViewModel( ) : BaseViewModel( initialState = NoteState(), ) { - - private val route = savedStateHandle.toRoute() + private val route = savedStateHandle.toRoute() init { + getNoteOptionsAndObserveNetwork() mutableStateFlow.update { it.copy( - entityId = route.entityId, - entityType = route.entityType, + resourceId = route.resourceId, + resourceType = route.resourceType, ) } - getNoteOptionsAndObserveNetwork() } private fun getNoteOptionsAndObserveNetwork() { @@ -57,13 +55,15 @@ class NoteViewModel( } private suspend fun loadNote() { - route.entityType?.let { entityType -> - repository.retrieveListNotes(entityType, route.entityId.toLong()) + route.resourceType?.let { entityType -> + repository.retrieveListNotes(entityType, route.resourceId.toLong()) .collect { dataState -> when (dataState) { is DataState.Error -> mutableStateFlow.update { it.copy( dialogState = NoteState.DialogState.Error(dataState.message), + isError = true, + isRefreshing = false, ) } @@ -72,6 +72,7 @@ class NoteViewModel( mutableStateFlow.update { it.copy( dialogState = NoteState.DialogState.Loading, + isRefreshing = false, ) } } @@ -82,7 +83,8 @@ class NoteViewModel( it.copy( dialogState = null, notes = dataState.data, - isDeleteError = false, + isError = false, + expandedNoteId = null, isRefreshing = false, ) } @@ -90,51 +92,59 @@ class NoteViewModel( } } } + } + private suspend fun deleteNote(id: Long?) { mutableStateFlow.update { it.copy( - isRefreshing = false, + dialogState = NoteState.DialogState.Loading, ) } - } - private suspend fun deleteNote(id: Long?) { - route.entityType?.let { type -> + try { + route.resourceType?.let { type -> + id?.let { id -> + repository.deleteNote(type, route.resourceId.toLong(), id) + } + } mutableStateFlow.update { it.copy( - dialogState = NoteState.DialogState.Loading, + dialogState = null, ) } - try { - id?.let { id -> - repository.deleteNote(type, route.entityId.toLong(), id) - } - getNoteOptionsAndObserveNetwork() - mutableStateFlow.update { - it.copy( - isDeleteError = false, - ) - } - } catch (e: Exception) { - mutableStateFlow.update { - it.copy( - dialogState = NoteState.DialogState.Error(e.message.toString()), - isDeleteError = true, - ) - } + getNoteOptionsAndObserveNetwork() + } catch (e: Exception) { + mutableStateFlow.update { + it.copy( + dialogState = NoteState.DialogState.Error(e.message.toString()), + isError = true, + ) } } } override fun handleAction(action: NoteAction) { when (action) { - NoteAction.NavigateBack -> sendEvent(NoteEvent.NavigateBack) + NoteAction.NavigateBack -> { + sendEvent(NoteEvent.NavigateBack) + mutableStateFlow.update { + it.copy(expandedNoteId = null) + } + } + NoteAction.OnRetry -> { getNoteOptionsAndObserveNetwork() } - NoteAction.OnNext -> sendEvent(NoteEvent.NavigateNext) + NoteAction.OnClickEditScreen -> sendEvent(NoteEvent.NavigateEditNote) + NoteAction.OnClickAddScreen -> sendEvent(NoteEvent.NavigateAddNote) is NoteAction.DeleteNote -> { + mutableStateFlow.update { + it.copy( + showDialog = false, + isError = false, + ) + } viewModelScope.launch { deleteNote(action.id) } @@ -171,16 +181,14 @@ class NoteViewModel( } data class NoteState( - val entityId: Int = -1, - val entityType: String? = null, + val resourceId: Int = -1, + val resourceType: String? = null, val isRefreshing: Boolean = false, val notes: List = emptyList(), - val isLoading: Boolean = false, - val error: String? = null, val showDialog: Boolean = false, val dialogState: DialogState? = null, val expandedNoteId: Long? = null, - val isDeleteError: Boolean = false, + val isError: Boolean = false, val networkConnection: Boolean = false, ) { sealed interface DialogState { @@ -191,18 +199,18 @@ data class NoteState( sealed interface NoteEvent { data object NavigateBack : NoteEvent - data object NavigateNext : NoteEvent + data object NavigateEditNote : NoteEvent + data object NavigateAddNote : NoteEvent } sealed interface NoteAction { data object NavigateBack : NoteAction data object OnRetry : NoteAction data object OnRefresh : NoteAction - data object OnNext : NoteAction + data object OnClickAddScreen : NoteAction + data object OnClickEditScreen : NoteAction data object ShowDialog : NoteAction data object DismissDialog : NoteAction - data class OnToggleExpanded(val id: Long?) : NoteAction - data class DeleteNote(val id: Long?) : NoteAction } From 06958a333d21c1644324f3f52be83b63295120be Mon Sep 17 00:00:00 2001 From: Arin Yadav Date: Fri, 29 Aug 2025 20:53:26 +0530 Subject: [PATCH 2/2] update --- .../com/mifos/core/domain/di/UseCaseModule.kt | 6 + .../core/domain/useCases/AddNoteUseCase.kt | 30 ++++ .../core/domain/useCases/DeleteNoteUseCase.kt | 29 ++++ .../core/domain/useCases/UpdateNoteUseCase.kt | 31 ++++ feature/note/build.gradle.kts | 1 + .../note/addEditNotes/AddEditNoteRoute.kt | 4 +- .../note/addEditNotes/AddEditNoteScreen.kt | 141 ++++++++++-------- .../note/addEditNotes/AddEditNoteViewModel.kt | 130 +++++++++------- .../feature/note/navigation/NoteNavigation.kt | 11 +- .../com/mifos/feature/note/notes/NoteRoute.kt | 11 +- .../mifos/feature/note/notes/NoteScreen.kt | 8 - .../mifos/feature/note/notes/NoteViewModel.kt | 54 ++++--- 12 files changed, 299 insertions(+), 157 deletions(-) create mode 100644 core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/AddNoteUseCase.kt create mode 100644 core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteNoteUseCase.kt create mode 100644 core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateNoteUseCase.kt 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 1ddac0dfbcb..2e4ec16198d 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 @@ -15,6 +15,7 @@ import com.mifos.core.domain.useCases.ActivateGroupUseCase import com.mifos.core.domain.useCases.ActivateSavingsUseCase import com.mifos.core.domain.useCases.AddClientPinpointLocationUseCase import com.mifos.core.domain.useCases.AddDataTableEntryUseCase +import com.mifos.core.domain.useCases.AddNoteUseCase import com.mifos.core.domain.useCases.ApproveCheckerUseCase import com.mifos.core.domain.useCases.ApproveSavingsApplicationUseCase import com.mifos.core.domain.useCases.CreateChargesUseCase @@ -28,6 +29,7 @@ import com.mifos.core.domain.useCases.DeleteCheckerUseCase import com.mifos.core.domain.useCases.DeleteClientAddressPinpointUseCase import com.mifos.core.domain.useCases.DeleteDataTableEntryUseCase import com.mifos.core.domain.useCases.DeleteIdentifierUseCase +import com.mifos.core.domain.useCases.DeleteNoteUseCase import com.mifos.core.domain.useCases.DownloadDocumentUseCase import com.mifos.core.domain.useCases.FetchCenterDetailsUseCase import com.mifos.core.domain.useCases.FetchCollectionSheetUseCase @@ -75,6 +77,7 @@ 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.UpdateClientPinpointUseCase +import com.mifos.core.domain.useCases.UpdateNoteUseCase import com.mifos.core.domain.useCases.UploadClientImageUseCase import com.mifos.core.domain.useCases.UsernameValidationUseCase import com.mifos.core.domain.useCases.ValidateServerApiPathUseCase @@ -159,4 +162,7 @@ val UseCaseModule = module { factoryOf(::GetGroupDetailsUseCase) factoryOf(::GetLoanAndLoanRepaymentUseCase) factoryOf(::GetSavingsAccountAndTemplateUseCase) + factoryOf(::AddNoteUseCase) + factoryOf(::UpdateNoteUseCase) + factoryOf(::DeleteNoteUseCase) } diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/AddNoteUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/AddNoteUseCase.kt new file mode 100644 index 00000000000..259573e22f9 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/AddNoteUseCase.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.domain.useCases + +import com.mifos.core.common.utils.DataState +import com.mifos.core.common.utils.asDataStateFlow +import com.mifos.core.data.repository.NoteRepository +import com.mifos.core.model.objects.payloads.NotesPayload +import com.mifos.core.network.GenericResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class AddNoteUseCase( + val repository: NoteRepository, +) { + operator fun invoke( + resourceType: String, + resourceId: Long, + notesPayload: NotesPayload, + ): Flow> = flow { + emit(repository.addNewNote(resourceType, resourceId, notesPayload)) + }.asDataStateFlow() +} diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteNoteUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteNoteUseCase.kt new file mode 100644 index 00000000000..fb365d6044d --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/DeleteNoteUseCase.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.core.domain.useCases + +import com.mifos.core.common.utils.DataState +import com.mifos.core.common.utils.asDataStateFlow +import com.mifos.core.data.repository.NoteRepository +import com.mifos.core.network.GenericResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class DeleteNoteUseCase( + val repository: NoteRepository, +) { + operator fun invoke( + resourceType: String, + resourceId: Long, + noteId: Long, + ): Flow> = flow { + emit(repository.deleteNote(resourceType, resourceId, noteId)) + }.asDataStateFlow() +} diff --git a/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateNoteUseCase.kt b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateNoteUseCase.kt new file mode 100644 index 00000000000..47e7490de31 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/com/mifos/core/domain/useCases/UpdateNoteUseCase.kt @@ -0,0 +1,31 @@ +/* + * 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.domain.useCases + +import com.mifos.core.common.utils.DataState +import com.mifos.core.common.utils.asDataStateFlow +import com.mifos.core.data.repository.NoteRepository +import com.mifos.core.model.objects.payloads.NotesPayload +import com.mifos.core.network.GenericResponse +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class UpdateNoteUseCase( + val repository: NoteRepository, +) { + operator fun invoke( + resourceType: String, + resourceId: Long, + noteId: Long, + notesPayload: NotesPayload, + ): Flow> = flow { + emit(repository.updateNote(resourceType, resourceId, noteId, notesPayload)) + }.asDataStateFlow() +} diff --git a/feature/note/build.gradle.kts b/feature/note/build.gradle.kts index e52e40451e3..76ea812ccbc 100644 --- a/feature/note/build.gradle.kts +++ b/feature/note/build.gradle.kts @@ -25,6 +25,7 @@ kotlin { implementation(compose.ui) implementation(projects.core.common) implementation(projects.core.model) + implementation(projects.core.domain) implementation(libs.kotlinx.serialization.json) } } diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt index 5c61d4e6b6b..88f7de6ae55 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteRoute.kt @@ -22,11 +22,13 @@ data class AddEditNoteRoute( ) fun NavGraphBuilder.addEditNoteRoute( - onBackPressed: (Boolean?) -> Unit, + onBackPressed: () -> Unit, + onNavigateWithUpdatedList: (Int, String?) -> Unit, ) { composable { AddEditNoteScreen( onBackPressed = onBackPressed, + onNavigateWithUpdatedList = onNavigateWithUpdatedList, ) } } diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt index 32db4b02057..0df203047b7 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -38,6 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosOutlinedButton +import com.mifos.core.designsystem.component.MifosOutlinedTextField import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.theme.DesignToken import com.mifos.core.designsystem.theme.MifosTypography @@ -49,14 +49,19 @@ import org.koin.compose.viewmodel.koinViewModel @Composable internal fun AddEditNoteScreen( - onBackPressed: (Boolean?) -> Unit, + onBackPressed: () -> Unit, + onNavigateWithUpdatedList: (Int, String?) -> Unit, viewModel: AddEditNoteViewModel = koinViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() EventsEffect(viewModel.eventFlow) { event -> when (event) { - AddEditNoteEvent.NavigateBack -> onBackPressed(state.isRequiredUpdateListNote) + AddEditNoteEvent.NavigateBack -> onBackPressed() + AddEditNoteEvent.NavigateBackWithUpdateList -> onNavigateWithUpdatedList( + state.resourceId, + state.resourceType, + ) } } @@ -87,9 +92,11 @@ fun AddEditNoteScreenDialog( }, ) } + AddEditNoteState.DialogState.Loading -> { MifosCircularProgress() } + AddEditNoteState.DialogState.MisTouchBack -> { MifosAlertDialog( onDismissRequest = { @@ -104,6 +111,7 @@ fun AddEditNoteScreenDialog( icon = null, ) } + null -> Unit } } @@ -140,9 +148,11 @@ private fun AddEditNote( Column( modifier = Modifier - .verticalScroll(scrollState) .fillMaxWidth() - .padding(horizontal = DesignToken.spacing.large), + .padding( + horizontal = DesignToken.spacing.large, + vertical = DesignToken.spacing.small, + ), verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.large), ) { Text( @@ -151,70 +161,77 @@ private fun AddEditNote( color = MaterialTheme.colorScheme.onSurface, ) - OutlinedTextField( - value = state.textFieldNotesPayload.note ?: "", - onValueChange = { - onAction(AddEditNoteAction.TextFieldNotesPayload(state.textFieldNotesPayload.copy(note = it))) - }, - singleLine = false, - shape = DesignToken.shapes.large, - label = { - Text( - text = stringResource(state.label), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, + Column( modifier = Modifier - .fillMaxWidth() - .heightIn(min = 600.dp), - textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Start), - colors = OutlinedTextFieldDefaults.colors( - unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer, - ), - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(DesignToken.spacing.extraSmall), + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.large), ) { - MifosOutlinedButton( - modifier = Modifier.weight(1f), - text = { - Text( - text = stringResource(Res.string.feature_note_button_back), + MifosOutlinedTextField( + value = state.textFieldNotesPayload.note ?: "", + onValueChange = { + onAction( + AddEditNoteAction.TextFieldNotesPayload( + state.textFieldNotesPayload.copy( + note = it, + ), + ), ) }, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.primary, - containerColor = MaterialTheme.colorScheme.onPrimary, + maxLines = 18, + singleLine = false, + shape = DesignToken.shapes.large, + label = stringResource(state.label), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 550.dp), + textStyle = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Start), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer, ), - onClick = { - onAction(AddEditNoteAction.MisTouchBackDialog) - }, ) - MifosOutlinedButton( - modifier = Modifier.weight(1f), - text = { - Text(text = stringResource(state.addUpdateButton)) - }, - onClick = { - if (state.editEnabled) { - onAction(AddEditNoteAction.EditNote(state.textFieldNotesPayload)) - } else { - onAction(AddEditNoteAction.AddNote(state.textFieldNotesPayload)) - } - if (state.isError) { - onAction(AddEditNoteAction.NavigateBack) - } - }, - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.onPrimary, - containerColor = MaterialTheme.colorScheme.primary, - ), - enabled = !state.textFieldNotesPayload.note.isNullOrEmpty(), - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(DesignToken.spacing.extraSmall), + ) { + MifosOutlinedButton( + modifier = Modifier.weight(1f), + text = { + Text( + text = stringResource(Res.string.feature_note_button_back), + ) + }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.primary, + containerColor = MaterialTheme.colorScheme.onPrimary, + ), + onClick = { + onAction(AddEditNoteAction.MisTouchBackDialog) + }, + ) + + MifosOutlinedButton( + modifier = Modifier.weight(1f), + text = { + Text(text = stringResource(state.addUpdateButton)) + }, + onClick = { + if (state.editEnabled) { + onAction(AddEditNoteAction.EditNote(state.textFieldNotesPayload)) + } else { + onAction(AddEditNoteAction.AddNote(state.textFieldNotesPayload)) + } + if (state.isError) { + onAction(AddEditNoteAction.NavigateBack) + } + }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primary, + ), + enabled = !state.textFieldNotesPayload.note.isNullOrEmpty(), + ) + } } } } diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt index c6acd175e57..cb4015b7b79 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/addEditNotes/AddEditNoteViewModel.kt @@ -25,6 +25,8 @@ import androidx.navigation.toRoute import com.mifos.core.common.utils.DataState import com.mifos.core.data.repositoryImp.NoteRepositoryImp import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.domain.useCases.AddNoteUseCase +import com.mifos.core.domain.useCases.UpdateNoteUseCase import com.mifos.core.model.objects.payloads.NotesPayload import com.mifos.core.ui.util.BaseViewModel import kotlinx.coroutines.flow.update @@ -33,6 +35,8 @@ import org.jetbrains.compose.resources.StringResource class AddEditNoteViewModel( private val repository: NoteRepositoryImp, + private val updateNoteUseCase: UpdateNoteUseCase, + private val addNoteUseCase: AddNoteUseCase, savedStateHandle: SavedStateHandle, private val networkMonitor: NetworkMonitor, ) : BaseViewModel( @@ -56,6 +60,13 @@ class AddEditNoteViewModel( ) } } + + mutableStateFlow.update { + it.copy( + resourceId = route.resourceId, + resourceType = route.resourceType, + ) + } } private fun getAddEditNoteOptionsAndObserveNetwork() { @@ -83,11 +94,13 @@ class AddEditNoteViewModel( it.copy(dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error)) } } + DataState.Loading -> { mutableStateFlow.update { it.copy(dialogState = AddEditNoteState.DialogState.Loading) } } + is DataState.Success -> { mutableStateFlow.update { it.copy( @@ -104,51 +117,67 @@ class AddEditNoteViewModel( } private suspend fun addNote(notesPayload: NotesPayload) { - runCatching { - route.resourceType?.let { type -> - repository.addNewNote(route.resourceType, route.resourceId.toLong(), notesPayload) - } - }.onFailure { - mutableStateFlow.update { - it.copy( - dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error), - isError = false, - ) - } - }.onSuccess { - mutableStateFlow.update { - it.copy( - isError = true, - dialogState = null, - successMessage = Res.string.feature_note_add_success, - notesPayloadInitialData = state.textFieldNotesPayload.note, - ) - } + route.resourceType?.let { type -> + addNoteUseCase(route.resourceType, route.resourceId.toLong(), notesPayload) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error), + isError = false, + ) + } + } + + DataState.Loading -> { + // no need to show loading for this + } + + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + isError = true, + dialogState = null, + successMessage = Res.string.feature_note_add_success, + notesPayloadInitialData = state.textFieldNotesPayload.note, + ) + } + } + } + } } } private suspend fun editNote(notesPayload: NotesPayload) { - runCatching { - route.resourceType?.let { type -> - route.noteId?.let { id -> - repository.updateNote(type, route.resourceId.toLong(), id, notesPayload) - } - } - }.onFailure { - mutableStateFlow.update { - it.copy( - dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error), - isError = false, - ) - } - }.onSuccess { - mutableStateFlow.update { - it.copy( - isError = true, - dialogState = null, - successMessage = Res.string.feature_note_edit_success, - notesPayloadInitialData = state.textFieldNotesPayload.note, - ) + route.resourceType?.let { type -> + route.noteId?.let { id -> + updateNoteUseCase(type, route.resourceId.toLong(), id, notesPayload) + .collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = AddEditNoteState.DialogState.Error(Res.string.feature_note_Unexpected_error), + isError = false, + ) + } + } + DataState.Loading -> { + // no need to show loading for this + } + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + isError = true, + dialogState = null, + successMessage = Res.string.feature_note_edit_success, + notesPayloadInitialData = state.textFieldNotesPayload.note, + ) + } + } + } + } } } } @@ -156,23 +185,21 @@ class AddEditNoteViewModel( override fun handleAction(action: AddEditNoteAction) { when (action) { AddEditNoteAction.NavigateBack -> { - mutableStateFlow.update { - it.copy( - isRequiredUpdateListNote = true, - ) - } - sendEvent(AddEditNoteEvent.NavigateBack) + sendEvent(AddEditNoteEvent.NavigateBackWithUpdateList) } + is AddEditNoteAction.AddNote -> { viewModelScope.launch { addNote(action.notesPayload) } } + is AddEditNoteAction.EditNote -> { viewModelScope.launch { editNote(action.notesPayload) } } + AddEditNoteAction.OnRetry -> { if (state.editEnabled) { viewModelScope.launch { @@ -212,11 +239,6 @@ class AddEditNoteViewModel( ) } } else { - mutableStateFlow.update { - it.copy( - isRequiredUpdateListNote = false, - ) - } sendEvent(AddEditNoteEvent.NavigateBack) } } @@ -225,6 +247,8 @@ class AddEditNoteViewModel( } data class AddEditNoteState( + val resourceId: Int = -1, + val resourceType: String? = null, val editEnabled: Boolean = false, val successMessage: StringResource? = null, val addUpdateButton: StringResource = Res.string.feature_note_button_add, @@ -236,7 +260,6 @@ data class AddEditNoteState( val isError: Boolean = true, val dialogState: DialogState? = null, val networkConnection: Boolean = false, - val isRequiredUpdateListNote: Boolean = false, ) { sealed interface DialogState { data class Error(val message: StringResource) : DialogState @@ -247,6 +270,7 @@ data class AddEditNoteState( sealed interface AddEditNoteEvent { data object NavigateBack : AddEditNoteEvent + data object NavigateBackWithUpdateList : AddEditNoteEvent } sealed interface AddEditNoteAction { diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt index 61d99eff9c9..5697ac5a0ef 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/navigation/NoteNavigation.kt @@ -15,16 +15,13 @@ import androidx.navigation.navigation import com.mifos.feature.note.addEditNotes.addEditNoteRoute import com.mifos.feature.note.addEditNotes.navigateToAddEditNoteScreen import com.mifos.feature.note.notes.NoteRoute +import com.mifos.feature.note.notes.navigateToNoteScreenWithUpdatedList import com.mifos.feature.note.notes.noteRoute import kotlinx.serialization.Serializable @Serializable object NoteNavigationRoute -object Update { - const val NOTE_LIST = "update note list" -} - fun NavGraphBuilder.noteNavGraph( navController: NavController, onBackPressed: () -> Unit, @@ -38,12 +35,10 @@ fun NavGraphBuilder.noteNavGraph( ) addEditNoteRoute( - onBackPressed = { updateNoteList -> - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(Update.NOTE_LIST, updateNoteList) + onBackPressed = { navController.popBackStack() }, + onNavigateWithUpdatedList = navController::navigateToNoteScreenWithUpdatedList, ) } } diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt index 90d11e82808..c88ccedfad9 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteRoute.kt @@ -12,7 +12,6 @@ package com.mifos.feature.note.notes import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.mifos.feature.note.navigation.Update import kotlinx.serialization.Serializable @Serializable @@ -25,11 +24,10 @@ fun NavGraphBuilder.noteRoute( onNavigateBack: () -> Unit, onNavigateAddEditNote: (Int, String?, Long?) -> Unit, ) { - composable { updateNoteList -> + composable { NoteScreenScaffold( onNavigateBack = onNavigateBack, onNavigateAddEditNote = onNavigateAddEditNote, - updateNoteList = updateNoteList.savedStateHandle.get(Update.NOTE_LIST), ) } } @@ -37,3 +35,10 @@ fun NavGraphBuilder.noteRoute( fun NavController.navigateToNoteScreen(entityId: Int, entityType: String?) { this.navigate(NoteRoute(entityId, entityType)) } + +fun NavController.navigateToNoteScreenWithUpdatedList(entityId: Int, entityType: String?) { + this.navigate(NoteRoute(entityId, entityType)) { + popUpTo(NoteRoute(entityId, entityType)) { inclusive = true } + launchSingleTop = true + } +} diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt index 8639a11cec1..77d147ead36 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteScreen.kt @@ -47,7 +47,6 @@ 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 @@ -78,17 +77,10 @@ import org.koin.compose.viewmodel.koinViewModel internal fun NoteScreenScaffold( onNavigateBack: () -> Unit, onNavigateAddEditNote: (Int, String?, Long?) -> Unit, - updateNoteList: Boolean? = null, viewModel: NoteViewModel = koinViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() - LaunchedEffect(updateNoteList) { - if (updateNoteList == true) { - viewModel.trySendAction(NoteAction.OnRetry) - } - } - EventsEffect(viewModel.eventFlow) { event -> when (event) { NoteEvent.NavigateBack -> onNavigateBack() diff --git a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt index c7dab35fa56..363e7907397 100644 --- a/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt +++ b/feature/note/src/commonMain/kotlin/com/mifos/feature/note/notes/NoteViewModel.kt @@ -15,6 +15,7 @@ import androidx.navigation.toRoute import com.mifos.core.common.utils.DataState import com.mifos.core.data.repositoryImp.NoteRepositoryImp import com.mifos.core.data.util.NetworkMonitor +import com.mifos.core.domain.useCases.DeleteNoteUseCase import com.mifos.core.model.objects.notes.Note import com.mifos.core.ui.util.BaseViewModel import kotlinx.coroutines.flow.update @@ -22,6 +23,7 @@ import kotlinx.coroutines.launch class NoteViewModel( private val repository: NoteRepositoryImp, + private val deleteNoteUseCase: DeleteNoteUseCase, savedStateHandle: SavedStateHandle, private val networkMonitor: NetworkMonitor, ) : BaseViewModel( @@ -30,6 +32,7 @@ class NoteViewModel( private val route = savedStateHandle.toRoute() init { + getNoteOptionsAndObserveNetwork() mutableStateFlow.update { it.copy( @@ -95,31 +98,38 @@ class NoteViewModel( } private suspend fun deleteNote(id: Long?) { - mutableStateFlow.update { - it.copy( - dialogState = NoteState.DialogState.Loading, - ) - } + route.resourceType?.let { type -> + id?.let { id -> + deleteNoteUseCase(type, route.resourceId.toLong(), id).collect { dataState -> + when (dataState) { + is DataState.Error -> { + mutableStateFlow.update { + it.copy( + dialogState = NoteState.DialogState.Error(dataState.message), + isError = true, + ) + } + } + + is DataState.Loading -> { + mutableStateFlow.update { + it.copy( + dialogState = NoteState.DialogState.Loading, + ) + } + } - try { - route.resourceType?.let { type -> - id?.let { id -> - repository.deleteNote(type, route.resourceId.toLong(), id) + is DataState.Success -> { + mutableStateFlow.update { + it.copy( + dialogState = null, + ) + } + getNoteOptionsAndObserveNetwork() + } + } } } - mutableStateFlow.update { - it.copy( - dialogState = null, - ) - } - getNoteOptionsAndObserveNetwork() - } catch (e: Exception) { - mutableStateFlow.update { - it.copy( - dialogState = NoteState.DialogState.Error(e.message.toString()), - isError = true, - ) - } } }