diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardMenuItems.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardMenuItems.kt index 65556e4..9a1a700 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardMenuItems.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardMenuItems.kt @@ -71,7 +71,7 @@ fun buildMenuItems( modifier = Modifier.height(48.dp) ) } - ) { } + ) { onNavigateAction("MMQR") } ) } add( diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt index ba5f773..e8c272a 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt @@ -5,10 +5,21 @@ import android.os.Build import android.widget.Toast import androidx.activity.ComponentActivity import androidx.annotation.RequiresApi +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.mob.utsmyanmar.ui.theme.Color as AppColor import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -74,11 +85,14 @@ fun AppNavGraph( val activity = LocalContext.current as ComponentActivity val currentBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = currentBackStackEntry?.destination?.route + val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity) + val isSyncBlockedGlobal by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle() - LaunchedEffect(autoSettlementRequested, currentRoute) { + LaunchedEffect(autoSettlementRequested, currentRoute, isSyncBlockedGlobal) { if (!autoSettlementRequested || currentRoute == null || - currentRoute == Routes.TmsSetup.route + currentRoute == Routes.TmsSetup.route || + isSyncBlockedGlobal ) return@LaunchedEffect if (currentRoute == Routes.Settlement.route) { return@LaunchedEffect @@ -98,12 +112,11 @@ fun AppNavGraph( startDestination = Routes.TmsSetup.route ) { composable(Routes.TmsSetup.route) { - val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel() TmsSetupRoute( viewModel = tmsSetupViewModel, onNavigateDashboard = { navController.navigate( - if (autoSettlementRequested) Routes.Settlement.route else Routes.Dashboard.route + if (autoSettlementRequested && !isSyncBlockedGlobal) Routes.Settlement.route else Routes.Dashboard.route ) { popUpTo(Routes.TmsSetup.route) { inclusive = true @@ -150,6 +163,9 @@ fun AppNavGraph( } val tmsSetupUiState by tmsSetupViewModel.uiState.collectAsStateWithLifecycle() + val isSyncBlocked by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle() + val syncBlockedMessage by tmsSetupViewModel.syncBlockedMessage.collectAsStateWithLifecycle() + var showBlockedSaleAlert by remember { mutableStateOf(false) } LaunchedEffect(tmsSetupViewModel) { tmsSetupViewModel.onSyncCompleted.collect { @@ -201,96 +217,109 @@ fun AppNavGraph( } }, onNavigateAmount = { action -> - when (action) { - "Pre-Auth", - "Cash Adv", - "Sale" -> { - navController.navigate(Routes.Amount.createRoute(action)) { - launchSingleTop = true + if (isSyncBlocked) { + showBlockedSaleAlert = true + } else { + when (action) { + "Pre-Auth", + "Cash Adv", + "Sale" -> { + navController.navigate(Routes.Amount.createRoute(action)) { + launchSingleTop = true + } } - } - "Pre-Auth", - "Cash Adv" -> { - navController.navigate(Routes.Amount.createRoute(action)) { - launchSingleTop = true - } - } - else -> { - navController.navigate( - Routes.Password.createRoute( - destination = Routes.Amount.createRoute(action), - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + else -> { + navController.navigate( + Routes.Password.createRoute( + destination = Routes.Amount.createRoute(action), + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } } } }, onNavigateAction = { action -> - when (action) { - "Void" -> { - sharedViewModel.transactionsType.value = TransactionsType.VOID - navController.navigate( - Routes.Password.createRoute( - destination = Routes.VoidTrace.route, - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + if (isSyncBlocked) { + showBlockedSaleAlert = true + } else { + when (action) { + "Void" -> { + sharedViewModel.transactionsType.value = TransactionsType.VOID + navController.navigate( + Routes.Password.createRoute( + destination = Routes.VoidTrace.route, + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } - } - "Pre-Auth", - "Cash Adv" -> { - sharedViewModel.transactionsType.value = when (action) { - "Pre-Auth" -> TransactionsType.PRE_AUTH_SALE - else -> TransactionsType.CASH_OUT + "Pre-Auth", + "Cash Adv" -> { + sharedViewModel.transactionsType.value = when (action) { + "Pre-Auth" -> TransactionsType.PRE_AUTH_SALE + else -> TransactionsType.CASH_OUT + } + navController.navigate(Routes.Amount.createRoute(action)) { + launchSingleTop = true + } } - navController.navigate(Routes.Amount.createRoute(action)) { - launchSingleTop = true - } - } - "Pre-Auth Complete Void" -> { - sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID - navController.navigate( - Routes.Password.createRoute( - destination = Routes.VoidTrace.route, - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + "Pre-Auth Complete Void" -> { + sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID + navController.navigate( + Routes.Password.createRoute( + destination = Routes.VoidTrace.route, + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } - } - else -> { - navController.navigate( - Routes.Password.createRoute( - destination = Routes.Amount.createRoute(action), - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + "MMQR" -> { + } + + else -> { + navController.navigate( + Routes.Password.createRoute( + destination = Routes.Amount.createRoute(action), + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } } } }, onNavigateSignOn = { - navController.navigate(Routes.SignOn.route) { - launchSingleTop = true + if (isSyncBlocked) { + showBlockedSaleAlert = true + } else { + navController.navigate(Routes.SignOn.route) { + launchSingleTop = true + } } }, onNavigateSettlement = { - navController.navigate( - Routes.Password.createRoute( - destination = Routes.Settlement.route, - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + if (isSyncBlocked) { + showBlockedSaleAlert = true + } else { + navController.navigate( + Routes.Password.createRoute( + destination = Routes.Settlement.route, + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } }, onNavigateVersion = { @@ -317,61 +346,137 @@ fun AppNavGraph( navController.navigate(Routes.History.route) } ) + + if (showBlockedSaleAlert) { + AlertDialog( + onDismissRequest = { showBlockedSaleAlert = false }, + title = { + Text( + text = "Maintenance Alert", + color = AppColor.LegacyRed, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium + ) + }, + text = { + Text( + text = syncBlockedMessage.ifBlank { + "Terminal sync is blocked due to an active maintenance window." + }, + color = AppColor.Black, + style = MaterialTheme.typography.bodyMedium + ) + }, + confirmButton = { + TextButton(onClick = { showBlockedSaleAlert = false }) { + Text( + text = "OK", + color = AppColor.LegacyRed, + fontWeight = FontWeight.Bold + ) + } + }, + containerColor = AppColor.White, + shape = RoundedCornerShape(16.dp) + ) + } } composable(Routes.SeeMore.route) { val sharedViewModel: SharedViewModel = hiltViewModel(activity) + val isSyncBlocked by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle() + val syncBlockedMessage by tmsSetupViewModel.syncBlockedMessage.collectAsStateWithLifecycle() + var showBlockedSaleAlert by remember { mutableStateOf(false) } + SeeMoreScreen( onBack = { navController.popBackStack() }, onNavigateAmount = { action -> - when (action) { - "Void" -> { - sharedViewModel.transactionsType.value = TransactionsType.VOID - navController.navigate( - Routes.Password.createRoute( - destination = Routes.VoidTrace.route, - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + if (isSyncBlocked) { + showBlockedSaleAlert = true + } else { + when (action) { + "Void" -> { + sharedViewModel.transactionsType.value = TransactionsType.VOID + navController.navigate( + Routes.Password.createRoute( + destination = Routes.VoidTrace.route, + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } - } - "Pre-Auth", - "Cash Adv" -> { - sharedViewModel.transactionsType.value = when (action) { - "Pre-Auth" -> TransactionsType.PRE_AUTH_SALE - else -> TransactionsType.CASH_OUT + "Pre-Auth", + "Cash Adv" -> { + sharedViewModel.transactionsType.value = when (action) { + "Pre-Auth" -> TransactionsType.PRE_AUTH_SALE + else -> TransactionsType.CASH_OUT + } + navController.navigate(Routes.Amount.createRoute(action)) { + launchSingleTop = true + } } - navController.navigate(Routes.Amount.createRoute(action)) { - launchSingleTop = true - } - } - "Pre-Auth Complete Void" -> { - sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID - navController.navigate( - Routes.Password.createRoute( - destination = Routes.VoidTrace.route, - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + "Pre-Auth Complete Void" -> { + sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID + navController.navigate( + Routes.Password.createRoute( + destination = Routes.VoidTrace.route, + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } - } - else -> { - navController.navigate( - Routes.Password.createRoute( - destination = Routes.Amount.createRoute(action), - passwordType = PasswordType.SYSTEM - ) - ) { - launchSingleTop = true + else -> { + navController.navigate( + Routes.Password.createRoute( + destination = Routes.Amount.createRoute(action), + passwordType = PasswordType.SYSTEM + ) + ) { + launchSingleTop = true + } } } } } ) + + if (showBlockedSaleAlert) { + AlertDialog( + onDismissRequest = { showBlockedSaleAlert = false }, + title = { + Text( + text = "Maintenance Alert", + color = AppColor.LegacyRed, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium + ) + }, + text = { + Text( + text = syncBlockedMessage.ifBlank { + "Terminal sync is blocked due to an active maintenance window." + }, + color = AppColor.Black, + style = MaterialTheme.typography.bodyMedium + ) + }, + confirmButton = { + TextButton(onClick = { showBlockedSaleAlert = false }) { + Text( + text = "OK", + color = AppColor.LegacyRed, + fontWeight = FontWeight.Bold + ) + } + }, + containerColor = AppColor.White, + shape = RoundedCornerShape(16.dp) + ) + } } composable(Routes.Version.route){ diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/tms_setup/TmsSetupViewModel.kt b/app/src/main/java/com/mob/utsmyanmar/ui/tms_setup/TmsSetupViewModel.kt index d26c12c..ccd944d 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/tms_setup/TmsSetupViewModel.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/tms_setup/TmsSetupViewModel.kt @@ -32,6 +32,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.json.JSONObject import sunmi.sunmiui.utils.LogUtil +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -40,7 +43,9 @@ data class TmsSetupUiState( val statusText: String = "Initializing...", val isError: Boolean = false, val errorMessage: String = "", - val isTerminalDisabled: Boolean = false + val isTerminalDisabled: Boolean = false, + val isSyncBlocked: Boolean = false, + val syncBlockedMessage: String = "" ) @HiltViewModel @@ -49,8 +54,30 @@ class TmsSetupViewModel @Inject constructor( private val emvParamOperation: EmvParamOperation, @ApplicationContext private val context: Context ) : ViewModel() { + companion object { + const val PREF_KEY_SYNC_BLOCKED = "is_sync_blocked" + const val PREF_KEY_SYNC_BLOCKED_MSG = "sync_blocked_message" + } + private val TAG = TmsSetupViewModel::class.simpleName - private val _uiState = MutableStateFlow(TmsSetupUiState()) + private val prefs = context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE) + + private val _isSyncBlocked = MutableStateFlow( + prefs.getBoolean(PREF_KEY_SYNC_BLOCKED, false) + ) + val isSyncBlocked: StateFlow = _isSyncBlocked.asStateFlow() + + private val _syncBlockedMessage = MutableStateFlow( + prefs.getString(PREF_KEY_SYNC_BLOCKED_MSG, "").orEmpty() + ) + val syncBlockedMessage: StateFlow = _syncBlockedMessage.asStateFlow() + + private val _uiState = MutableStateFlow( + TmsSetupUiState( + isSyncBlocked = prefs.getBoolean(PREF_KEY_SYNC_BLOCKED, false), + syncBlockedMessage = prefs.getString(PREF_KEY_SYNC_BLOCKED_MSG, "").orEmpty() + ) + ) private val _dashboardUiState = MutableStateFlow(DashboardUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -100,7 +127,16 @@ class TmsSetupViewModel @Inject constructor( .doFinally { requestInProgress = false } .subscribe( { response -> + prefs.edit { + putBoolean(PREF_KEY_SYNC_BLOCKED, false) + remove(PREF_KEY_SYNC_BLOCKED_MSG) + } + _isSyncBlocked.value = false + _syncBlockedMessage.value = "" + _uiState.update { it.copy( + isSyncBlocked = false, + syncBlockedMessage = "", statusText = "Applying configuration...") } @@ -117,24 +153,59 @@ class TmsSetupViewModel @Inject constructor( onConfigApplied(onSuccess) }, { error -> - val errorMessage = if (error is retrofit2.HttpException) { + var isSyncBlocked = false + var errorMessage = error.message.toString() + + if (error is retrofit2.HttpException) { + val httpCode = error.code() try { val body = error.response()?.errorBody()?.string() if (!body.isNullOrBlank()) { - JSONObject(body).optString("message", "Unknown server error") + val json = JSONObject(body) + val code = json.optString("code") + val statusCode = json.optInt("statusCode", httpCode) + val msg = json.optString("message", "") + if (msg.isNotBlank()) { + errorMessage = msg + } + + if (httpCode == 423 || statusCode == 423 || + code.equals("TERMINAL_SYNC_BLOCKED", ignoreCase = true) || + (msg.contains("blocked", ignoreCase = true) && msg.contains("maintenance", ignoreCase = true)) + ) { + isSyncBlocked = true + errorMessage = buildAlertDetailsMessage(json, msg.ifBlank { errorMessage }) + } } else { - "Empty error response (${error.code()})" + if (httpCode == 423) { + isSyncBlocked = true + } + errorMessage = "Empty error response ($httpCode)" } } catch (e: Exception) { - "Server error: ${error.code()}" + if (httpCode == 423) { + isSyncBlocked = true + } + errorMessage = "Server error: $httpCode" } - } else { - error.message.toString() } + + if (isSyncBlocked) { + LogUtil.i(TAG, "Terminal sync is blocked due to maintenance window ($errorMessage).") + prefs.edit { + putBoolean(PREF_KEY_SYNC_BLOCKED, true) + putString(PREF_KEY_SYNC_BLOCKED_MSG, errorMessage) + } + _isSyncBlocked.value = true + _syncBlockedMessage.value = errorMessage + } + _uiState.update { it.copy( isError = true, isLoading = false, + isSyncBlocked = isSyncBlocked, + syncBlockedMessage = if (isSyncBlocked) errorMessage else "", statusText = "Download failed!", errorMessage = errorMessage ) @@ -181,12 +252,20 @@ class TmsSetupViewModel @Inject constructor( val validity = TMSUtil.getInstance().checkParams() if (validity.status == ValidityStatus.SUCCESS) { - context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE) - .edit { - putLong("last_sync_time", System.currentTimeMillis()) - } + prefs.edit { + putLong("last_sync_time", System.currentTimeMillis()) + putBoolean(PREF_KEY_SYNC_BLOCKED, false) + remove(PREF_KEY_SYNC_BLOCKED_MSG) + } + _isSyncBlocked.value = false + _syncBlockedMessage.value = "" onSuccess() - _uiState.update { it.copy(isLoading = false, statusText = "Ready.") } + _uiState.update { it.copy( + isLoading = false, + isSyncBlocked = false, + syncBlockedMessage = "", + statusText = "Ready." + ) } viewModelScope.launch { _onSyncCompleted.emit(Unit) _navigateToDashboard.emit(Unit) @@ -222,6 +301,65 @@ class TmsSetupViewModel @Inject constructor( } } + private fun buildAlertDetailsMessage(json: JSONObject, defaultMessage: String): String { + val builder = StringBuilder() + val msg = json.optString("message", defaultMessage).trim() + if (msg.isNotEmpty()) { + builder.append(msg) + } + + val alertsArray = json.optJSONArray("alerts") + if (alertsArray != null && alertsArray.length() > 0) { + for (i in 0 until alertsArray.length()) { + val alert = alertsArray.optJSONObject(i) ?: continue + if (builder.isNotEmpty()) { + builder.append("\n\n") + } + val title = alert.optString("title").trim() + val desc = alert.optString("description").trim() + val type = alert.optString("type").trim() + val startAt = alert.optString("startAt").trim() + val endAt = alert.optString("endAt").trim() + + val lines = mutableListOf() + if (title.isNotEmpty()) lines.add("Title: $title") + if (desc.isNotEmpty()) lines.add("Description: $desc") + if (type.isNotEmpty()) lines.add("Type: $type") + if (startAt.isNotEmpty()) lines.add("Start At: ${formatDateTime(startAt)}") + if (endAt.isNotEmpty()) lines.add("End At: ${formatDateTime(endAt)}") + + builder.append(lines.joinToString("\n")) + } + } + + return builder.toString().ifBlank { defaultMessage } + } + + private fun formatDateTime(isoString: String): String { + if (isoString.isBlank()) return "" + val patterns = arrayOf( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "yyyy-MM-dd'T'HH:mm:ss'Z'", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss" + ) + for (pattern in patterns) { + try { + val parser = SimpleDateFormat(pattern, Locale.US).apply { + if (pattern.endsWith("'Z'")) { + timeZone = TimeZone.getTimeZone("UTC") + } + } + val date = parser.parse(isoString) + if (date != null) { + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) + return formatter.format(date) + } + } catch (_: Exception) {} + } + return isoString + } + override fun onCleared() { super.onCleared() disposables.clear() diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml index f12aa05..16449c3 100644 --- a/app/src/main/res/xml/network_security_config.xml +++ b/app/src/main/res/xml/network_security_config.xml @@ -3,6 +3,7 @@ posuat.myanmarorientalbank.com:5033 + 192.168.10.104 diff --git a/paylibs/src/main/java/com/utsmyanmar/paylibs/utils/core_utils/SystemParamsSettings.java b/paylibs/src/main/java/com/utsmyanmar/paylibs/utils/core_utils/SystemParamsSettings.java index 6414bc1..0d2976a 100644 --- a/paylibs/src/main/java/com/utsmyanmar/paylibs/utils/core_utils/SystemParamsSettings.java +++ b/paylibs/src/main/java/com/utsmyanmar/paylibs/utils/core_utils/SystemParamsSettings.java @@ -38,9 +38,10 @@ public class SystemParamsSettings implements Serializable { private boolean checkExpSwitch = false; - private String tmsAddress = "https://posmgmt.myanmarorientalbank.com/api/v1"; +// private String tmsAddress = "https://posmgmt.myanmarorientalbank.com/api/v1"; // private String tmsAddress = "https://sirius-nest.utsmyanmar.com/api/v1"; // private String tmsAddress = "http://128.199.170.203"; + private String tmsAddress = "http://192.168.10.104:3333/api/v1"; private String terminalCapability = "E0E8C8"; private String masterTerminalCapability = "E008C8";