From 73195ebc629bdf0a216c2d89dfbec98590064e7e Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:48:11 +0630 Subject: [PATCH] seperate code for Dashboard --- .../ui/dashboard/DashboardComponents.kt | 531 +++++++++++ .../ui/dashboard/DashboardMenuItems.kt | 142 +++ .../ui/dashboard/DashboardScreen.kt | 867 ++---------------- .../utsmyanmar/ui/dashboard/DashboardUtils.kt | 42 + 4 files changed, 783 insertions(+), 799 deletions(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardComponents.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardMenuItems.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUtils.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardComponents.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardComponents.kt new file mode 100644 index 0000000..936ee76 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardComponents.kt @@ -0,0 +1,531 @@ +package com.mob.utsmyanmar.ui.dashboard + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.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.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +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.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.mob.utsmyanmar.R +import com.mob.utsmyanmar.ui.theme.Color +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun AdvertisingArea( + modifier: Modifier = Modifier, + carouselUrls: List = emptyList(), +) { + val imageModels: List = carouselUrls.ifEmpty { + listOf(R.drawable.sunmi1, R.drawable.sunmi2, R.drawable.sunmi3) + } + val pageState = rememberPagerState(pageCount = { imageModels.size }) + + LaunchedEffect(pageState) { + while (true) { + delay(10000.milliseconds) + val nextPage = (pageState.currentPage + 1) % imageModels.size + pageState.animateScrollToPage( + page = nextPage, animationSpec = tween(durationMillis = 700) + ) + } + } + + Card( + modifier = modifier, + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + ) { + HorizontalPager( + state = pageState, modifier = Modifier.fillMaxSize() + ) { page -> + AsyncImage( + model = imageModels[page], + contentDescription = "ads images", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + } +} + +@Composable +fun SummaryCard( + onClickLastSync: () -> Unit, + lastSyncTime: Long?, + totalSaleCount: Int, + totalSaleAmount: Long +) { + var now by remember { mutableLongStateOf(System.currentTimeMillis()) } + LaunchedEffect(Unit) { + while (true) { + delay(30_000.milliseconds) + now = System.currentTimeMillis() + } + } + var isRotating by remember { mutableStateOf(false) } + val rotation by animateFloatAsState( + targetValue = if (isRotating) 360f else 0f, + animationSpec = tween(durationMillis = 3000, easing = LinearEasing), + label = "sync rotating", + finishedListener = { isRotating = false } + ) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + SummaryItem( + title = "Total Sale", + value = "MMK ${formatAmount(totalSaleAmount)}", + subtitle = "$totalSaleCount Transactions", + icon = Icons.Default.BarChart, + iconBg = Color.CrimsonRed + ) + + HorizontalDivider( + color = Color.Gray, + modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp) + ) + + SummaryItem( + title = "Settlement", + value = "Completed", + subtitle = "Today 11:00 PM", + icon = Icons.Default.Check, + iconBg = Color.CrimsonRed + ) + } + + VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp)) + + Column( + modifier = Modifier + .weight(0.75f) + .padding(start = 18.dp), + horizontalAlignment = Alignment.Start + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Last Sync", style = MaterialTheme.typography.bodySmall + ) + Box( + modifier = Modifier + .size(38.dp) + .clip(CircleShape) + .background(Color.CrimsonRed.copy(alpha = 0.1f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + if (!isRotating) isRotating = true + onClickLastSync() + }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.Sync, + contentDescription = null, + tint = Color.CrimsonRed, + modifier = Modifier + .size(22.dp) + .rotate(rotation) + ) + } + } + + Spacer(Modifier.height(6.dp)) + + Text( + text = formatRelativeTime(lastSyncTime, now), + style = MaterialTheme.typography.titleMedium, + color = Color.LegacyRed + ) + } + } + } +} + +@Composable +private fun SummaryItem( + title: String, + value: String, + subtitle: String, + icon: ImageVector, + iconBg: androidx.compose.ui.graphics.Color +) { + Row( + verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text(title, style = MaterialTheme.typography.bodySmall) + Text( + value, style = MaterialTheme.typography.titleSmall, color = Color.LegacyRed + ) + Text(subtitle, style = MaterialTheme.typography.bodySmall) + } + IconCircle(icon, iconBg) + } +} + +@Composable +private fun IconCircle( + icon: ImageVector, color: androidx.compose.ui.graphics.Color +) { + Box( + modifier = Modifier + .size(38.dp) + .clip(CircleShape) + .background(color.copy(alpha = 0.1f)), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = color, + modifier = Modifier.size(22.dp) + ) + } +} + +@Composable +fun MenuPager( + items: List, + modifier: Modifier = Modifier +) { + val pages = items.chunked(6) + val pagerState = rememberPagerState(pageCount = { pages.size }) + + Column(modifier = modifier) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth() + ) { pageIndex -> + MenuPage(items = pages[pageIndex]) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(pages.size) { index -> + val selected = pagerState.currentPage == index + Box( + modifier = Modifier + .padding(horizontal = 4.dp) + .size(if (selected) 8.dp else 6.dp) + .background( + color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f), + shape = CircleShape + ) + ) + } + } + } +} + +@Composable +private fun MenuPage(items: List) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.padding(horizontal = 16.dp) + ) { + items.chunked(3).forEach { rowItems -> + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + rowItems.forEach { item -> + MenuCard( + title = item.title, + icon = item.iconContent, + modifier = Modifier.weight(1f), + onClick = item.onClick + ) + } + repeat(3 - rowItems.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } +} + +@Composable +private fun MenuCard( + title: String, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null +) { + Card( + modifier = modifier + .height(100.dp) + .then( + if (onClick != null) { + Modifier.clickable(onClick = onClick) + } else { + Modifier + } + ), + shape = RoundedCornerShape(14.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(5.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(6.dp) + ) { + Column { + icon() + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.bodySmall, + color = Color.Black, + overflow = TextOverflow.Ellipsis, + maxLines = 2, + modifier = Modifier.weight(1f) + ) + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = Color.LegacyRed, + modifier = Modifier.size(28.dp) + ) + } + } + } + } +} + +fun LazyListScope.recentTransactionsItems(transactions: List) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Recent Transactions", + style = MaterialTheme.typography.titleSmall + ) + } + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color.Gray.copy(alpha = 0.2f) + ) + } + if (transactions.isEmpty()) { + item { + Text( + text = "No recent transactions", + color = Color.Gray, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp) + ) + } + } else { + items(items = transactions, key = { it.pid }) { record -> + TrnxRow(record = record) + } + } +} + +@Composable +private fun TrnxRow(record: TrnxRecord) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 4.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color.White) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(36.dp) + .background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = record.typeLabel.take(1), + color = Color.LegacyRed, + style = MaterialTheme.typography.bodyLarge + ) + } + + Spacer(Modifier.width(10.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text =record.typeLabel, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold), + color = Color.Black + ) + Text( + text = record.maskedCard, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + + Column(horizontalAlignment = Alignment.End) { + Text( + text = record.amountDisplay, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = if (record.isVoided) Color.Gray else Color.LegacyRed + ) + Text( + text = record.dateTime, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + } +} + +@Composable +fun Item( + onClick: () -> Unit, + title: String, + subTitle: String, + leadingIcon: (@Composable () -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, +) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + modifier = Modifier.fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + leadingIcon?.let { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color.LegacyRed.copy(alpha = 0.1f)) + .padding(8.dp) + ) { + it() + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text(text = title) + Text( + text = subTitle, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + + trailingIcon?.invoke() + } + } +} + +val previewTransactions = listOf( + TrnxRecord( + 1L, "Sale", "MMK 10,000", "**** 1234", "06/10 14:32", + isVoided = false, isApproved = true + ), + TrnxRecord( + 2L, "Void", "MMK 5,500", "**** 5678", "06/10 13:10", + isVoided = true, isApproved = false + ), + TrnxRecord( + 3L, "Refund", "MMK 2,000", "**** 9012", "06/09 09:45", + isVoided = false, isApproved = true + ), + TrnxRecord( + 4L, "Sale", "MMK 30,000", "**** 3456", "06/09 08:00", + isVoided = false, isApproved = true + ), + TrnxRecord( + 5L, "Settlement", "MMK 0", "----", "06/08 18:00", + isVoided = false, isApproved = true + ), +) 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 new file mode 100644 index 0000000..6cfcfa9 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardMenuItems.kt @@ -0,0 +1,142 @@ +package com.mob.utsmyanmar.ui.dashboard + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBalanceWallet +import androidx.compose.material.icons.filled.CreditCard +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.LockOpen +import androidx.compose.material.icons.filled.Replay +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.mob.utsmyanmar.R +import com.mob.utsmyanmar.ui.theme.Color + +class DashboardMenuItem( + val title: String, + val iconContent: @Composable () -> Unit, + val onClick: () -> Unit +) + +fun buildMenuItems( + onNavigateAmount: (String) -> Unit, + onNavigateSignOn: () -> Unit, + onNavigateSettlement: () -> Unit, + onNavigateHistory: () -> Unit, + onNavigateAction: (String) -> Unit +): List = listOf( + DashboardMenuItem( + "Sale", + { + Icon( + painterResource(R.drawable.ic_terminal), + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAmount("Sale") }, + DashboardMenuItem( + "History", + { + Icon( + painterResource(R.drawable.ic_history), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateHistory() }, + DashboardMenuItem( + "Sign On", + { + Icon( + painterResource(R.drawable.ic_sign_on), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateSignOn() }, + DashboardMenuItem( + "Settlement", + { + Icon( + painterResource(R.drawable.ic_settlement), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateSettlement() }, + DashboardMenuItem( + "Void", + { + Icon( + Icons.Default.Lock, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Void") }, + DashboardMenuItem( + "Refund", + { + Icon( + Icons.Default.Replay, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Refund") }, + DashboardMenuItem( + "Pre-Auth", + { + Icon( + Icons.Default.Lock, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Pre-Auth") }, + DashboardMenuItem( + "Pre-Auth Void", + { + Icon( + Icons.Default.LockOpen, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Pre-Auth Void") }, + DashboardMenuItem( + "Pre-Auth Complete", + { + Icon( + Icons.Default.CreditCard, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Pre-Auth Complete") }, + DashboardMenuItem( + "Pre-Auth Comp Void", + { + Icon( + Icons.Default.SwapHoriz, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Pre-Auth Complete Void") }, + DashboardMenuItem( + "Cash Out", + { + Icon( + Icons.Default.AccountBalanceWallet, + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = Color.LegacyRed + ) + }) { onNavigateAction("Cash Out") }, +) diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen.kt index 75ba947..7c3b6e5 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen.kt @@ -1,21 +1,10 @@ package com.mob.utsmyanmar.ui.dashboard -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities import android.os.Build import android.os.Handler import android.os.Looper import androidx.annotation.RequiresApi -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -26,34 +15,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountBalanceWallet -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.CreditCard -import androidx.compose.material.icons.filled.Lock -import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.Replay -import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material.icons.filled.Sync +import androidx.compose.animation.core.tween import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DrawerValue -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -63,30 +33,21 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.VerticalDivider import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.foundation.Image import coil3.compose.AsyncImage import com.mob.utsmyanmar.R import com.mob.utsmyanmar.ui.components.appbar.AppBar @@ -97,9 +58,7 @@ import com.mob.utsmyanmar.ui.theme.Color import com.utsmyanmar.paylibs.sign_on.EchoTestProcess import com.utsmyanmar.paylibs.sign_on.SignOnListener import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.milliseconds @RequiresApi(Build.VERSION_CODES.N) @Composable @@ -116,7 +75,6 @@ fun DashboardScreen2( deviceInfo: DeviceInfoUiState = DeviceInfoUiState(), onClickLastSync: () -> Unit = {}, ) { - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() val mainHandler = remember { Handler(Looper.getMainLooper()) } @@ -126,12 +84,9 @@ fun DashboardScreen2( var dialogMessage by remember { mutableStateOf("") } var reversalEnabled by remember { mutableStateOf( - runCatching { SystemParamsOperation.getInstance().isReversalOn }.getOrDefault( - false - ) + runCatching { SystemParamsOperation.getInstance().isReversalOn }.getOrDefault(false) ) } - val isOnline by rememberIsOnline() fun confirmationMessage(action: String) = "Do you want to start ${action.lowercase()}?" @@ -139,7 +94,6 @@ fun DashboardScreen2( fun successMessage(action: String) = "$action success." fun failureMessage(action: String, resultCode: Int?) = "$action failed. Response code: ${resultCode ?: -1}" - fun networkFailureMessage(action: String) = "Network error during $action." fun openHostActionDialog(action: String) { activeHostAction = action @@ -151,88 +105,81 @@ fun DashboardScreen2( if (showHostActionDialog) { AlertDialog( onDismissRequest = { - if (!isHostActionRunning) { - showHostActionDialog = false - } - }, title = { + if (!isHostActionRunning) showHostActionDialog = false + }, + title = { Text( - text = activeHostAction, color = Color.LegacyRed, fontWeight = FontWeight.Bold + text = activeHostAction, + color = Color.LegacyRed, + fontWeight = FontWeight.Bold ) - }, text = { - Text(text = dialogMessage) - }, confirmButton = { - TextButton( - onClick = { - if (isHostActionRunning) return@TextButton - - if (dialogMessage != confirmationMessage(activeHostAction)) { - showHostActionDialog = false - dialogMessage = confirmationMessage(activeHostAction) - return@TextButton + }, + text = { Text(text = dialogMessage) }, + confirmButton = { + TextButton(onClick = { + if (isHostActionRunning) return@TextButton + if (dialogMessage != confirmationMessage(activeHostAction)) { + showHostActionDialog = false + dialogMessage = confirmationMessage(activeHostAction) + return@TextButton + } + isHostActionRunning = true + dialogMessage = processingMessage(activeHostAction) + val signOnProcess = EchoTestProcess.getInstance() + val request = when (activeHostAction) { + "Echo Test" -> signOnProcess.enqueue(false) + "Log-Off" -> signOnProcess.enqueueLogOff() + else -> signOnProcess.enqueueLogOn() + } + request.startSignOn(object : SignOnListener { + override fun onSuccessSignOn() { + mainHandler.post { + isHostActionRunning = false + dialogMessage = successMessage(activeHostAction) + } } - - isHostActionRunning = true - dialogMessage = processingMessage(activeHostAction) - - val signOnProcess = EchoTestProcess.getInstance() - val request = when (activeHostAction) { - "Echo Test" -> signOnProcess.enqueue(false) - "Log-Off" -> signOnProcess.enqueueLogOff() - else -> signOnProcess.enqueueLogOn() + override fun onFailureSignOn(resultCode: Int?) { + mainHandler.post { + isHostActionRunning = false + dialogMessage = failureMessage(activeHostAction, resultCode) + } } - - request.startSignOn(object : SignOnListener { - override fun onSuccessSignOn() { - mainHandler.post { - isHostActionRunning = false - dialogMessage = successMessage(activeHostAction) - } + override fun onNetworkFailSignOn(message: String?) { + mainHandler.post { + isHostActionRunning = false + dialogMessage = message?.takeIf { it.isNotBlank() } + ?: networkFailureMessage(activeHostAction) } - - override fun onFailureSignOn(resultCode: Int?) { - mainHandler.post { - isHostActionRunning = false - mainHandler.post { - isHostActionRunning = false - dialogMessage = failureMessage(activeHostAction, resultCode) - } - } - } - - - override fun onNetworkFailSignOn(message: String?) { - mainHandler.post { - isHostActionRunning = false - dialogMessage = message?.takeIf { it.isNotBlank() } - ?: networkFailureMessage(activeHostAction) - } - } - }) - }) { + } + }) + }) { Text( text = when { isHostActionRunning -> "Processing" dialogMessage == confirmationMessage(activeHostAction) -> "Start" else -> "Close" - }, color = Color.LegacyRed + }, + color = Color.LegacyRed ) } - }, dismissButton = { + }, + dismissButton = { if (!isHostActionRunning) { - TextButton( - onClick = { - showHostActionDialog = false - dialogMessage = confirmationMessage(activeHostAction) - }) { + TextButton(onClick = { + showHostActionDialog = false + dialogMessage = confirmationMessage(activeHostAction) + }) { Text(text = "Cancel", color = Color.Gray) } } - }, containerColor = Color.White + }, + containerColor = Color.White ) } ModalNavigationDrawer( - drawerState = drawerState, drawerContent = { + drawerState = drawerState, + drawerContent = { ModalDrawerSheet( modifier = Modifier.fillMaxWidth(0.78f), drawerContainerColor = Color.White, @@ -249,7 +196,8 @@ fun DashboardScreen2( modifier = Modifier .size(80.dp) .clip(CircleShape) - .background(Color.White), contentAlignment = Alignment.Center + .background(Color.White), + contentAlignment = Alignment.Center ) { Image( painter = painterResource(R.drawable.logo_mob), @@ -260,7 +208,6 @@ fun DashboardScreen2( contentScale = ContentScale.Fit ) } - Column { Text( text = "MOB Merchant", @@ -398,12 +345,12 @@ fun DashboardScreen2( }, trailingIcon = {}, ) - } - }) { -//body start + } + ) { Scaffold( - containerColor = Color.IvoryBeige, topBar = { + containerColor = Color.IvoryBeige, + topBar = { AppBar( title = "Dashboard", icon = Icons.Default.Menu, @@ -427,8 +374,9 @@ fun DashboardScreen2( } } ) - }) { paddingValues -> - LazyColumn( + } + ) { paddingValues -> + androidx.compose.foundation.lazy.LazyColumn( modifier = Modifier .padding(paddingValues) .fillMaxSize() @@ -460,9 +408,7 @@ fun DashboardScreen2( onNavigateHistory = onNavigateHistory, onNavigateAction = onNavigateAction ), - modifier = Modifier - .fillMaxWidth() -// .height(250.dp) + modifier = Modifier.fillMaxWidth() ) } item { Spacer(Modifier.height(8.dp)) } @@ -472,595 +418,6 @@ fun DashboardScreen2( } } -@Composable -private fun AdvertisingArea( - modifier: Modifier = Modifier, - carouselUrls: List = emptyList(), -) { - val imageModels: List = carouselUrls.ifEmpty { - listOf(R.drawable.sunmi1, R.drawable.sunmi2, R.drawable.sunmi3) - } - val pageState = rememberPagerState(pageCount = { imageModels.size }) - - LaunchedEffect(pageState) { - while (true) { - delay(10000.milliseconds) - val nextPage = (pageState.currentPage + 1) % imageModels.size - - pageState.animateScrollToPage( - page = nextPage, animationSpec = tween(durationMillis = 700) - ) - } - } - - Card( - modifier = modifier, - shape = RoundedCornerShape(0.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - ) { - HorizontalPager( - state = pageState, modifier = Modifier.fillMaxSize() - ) { page -> - AsyncImage( - model = imageModels[page], - contentDescription = "ads images", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - } -} - -private fun formatRelativeTime(timestamp: Long?, now: Long): String { - if (timestamp == null) return "Never" - val diff = now - timestamp - return when { - diff < 60_000L -> "Just now" - diff < 3_600_000L -> "${diff / 60_000L} mins ago" - diff < 86_400_000L -> "${diff / 3_600_000L} hours ago" - else -> "${diff / 86_400_000L} days ago" - } -} - -@Composable -private fun SummaryCard( - onClickLastSync: () -> Unit, - lastSyncTime: Long?, - totalSaleCount: Int, - totalSaleAmount: Long -) { - var now by remember { mutableLongStateOf(System.currentTimeMillis()) } - LaunchedEffect(Unit) { - while (true) { - delay(30_000.milliseconds) - now = System.currentTimeMillis() - } - } - var isRotating by remember { mutableStateOf(false) } - val rotation by animateFloatAsState( - targetValue = if (isRotating) 360f else 0f, - animationSpec = tween(durationMillis = 3000, easing = LinearEasing), - label = "sync rotating", - finishedListener = { isRotating = false } - ) - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(4.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - SummaryItem( - title = "Total Sale", - value = "MMK ${formatAmount(totalSaleAmount)}", - subtitle = "$totalSaleCount Transactions", - icon = Icons.Default.BarChart, - iconBg = Color.CrimsonRed - ) - - HorizontalDivider( - color = Color.Gray, - modifier = Modifier.padding(vertical = 6.dp, horizontal = 6.dp) - ) - - SummaryItem( - title = "Settlement", - value = "Completed", - subtitle = "Today 11:00 PM", - icon = Icons.Default.Check, - iconBg = Color.CrimsonRed - ) - } - - VerticalDivider(color = Color.Gray, modifier = Modifier.height(160.dp)) - - Column( - modifier = Modifier - .weight(0.75f) - .padding(start = 18.dp), - horizontalAlignment = Alignment.Start - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Last Sync", style = MaterialTheme.typography.bodySmall - ) - Box( - modifier = Modifier - .size(38.dp) - .clip(CircleShape) - .background(Color.CrimsonRed.copy(alpha = 0.1f)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { - if (!isRotating) isRotating = true - onClickLastSync() - }, - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.Sync, - contentDescription = null, - tint = Color.CrimsonRed, - modifier = Modifier - .size(22.dp) - .rotate(rotation) - ) - } - } - - Spacer(Modifier.height(6.dp)) - - Text( - text = formatRelativeTime(lastSyncTime, now), - style = MaterialTheme.typography.titleMedium, - color = Color.LegacyRed - ) - } - } - } -} - -@Composable -private fun SummaryItem( - title: String, - value: String, - subtitle: String, - icon: ImageVector, - iconBg: androidx.compose.ui.graphics.Color -) { - Row( - verticalAlignment = Alignment.Top, modifier = Modifier.padding(6.dp) - ) { - Column(modifier = Modifier.weight(1f)) { - Text(title, style = MaterialTheme.typography.bodySmall) - Text( - value, style = MaterialTheme.typography.titleSmall, color = Color.LegacyRed - ) - Text(subtitle, style = MaterialTheme.typography.bodySmall) - } - IconCircle(icon, iconBg) - } -} - -@Composable -private fun IconCircle( - icon: ImageVector, color: androidx.compose.ui.graphics.Color -) { - Box( - modifier = Modifier - .size(38.dp) - .clip(CircleShape) - .background(color.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = color, - modifier = Modifier.size(22.dp) - ) - } -} - -private class DashboardMenuItem( - val title: String, - val iconContent: @Composable () -> Unit, - val onClick: () -> Unit -) - -@Composable -private fun buildMenuItems( - onNavigateAmount: (String) -> Unit, - onNavigateSignOn: () -> Unit, - onNavigateSettlement: () -> Unit, - onNavigateHistory: () -> Unit, - onNavigateAction: (String) -> Unit -): List = listOf( - DashboardMenuItem( - "Sale", - { - Icon( - painterResource(R.drawable.ic_terminal), - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAmount("Sale") }, -// DashboardMenuItem( -// "MMQR", -// { -// Image( -// painter = painterResource(R.drawable.ic_mmqr_logo), -// contentDescription = null, -// modifier = Modifier.height(48.dp) -// ) -// }) { }, - DashboardMenuItem( - "History", - { - Icon( - painterResource(R.drawable.ic_history), - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateHistory() }, - DashboardMenuItem( - "Sign On", - { - Icon( - painterResource(R.drawable.ic_sign_on), - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateSignOn() }, - DashboardMenuItem( - "Settlement", - { - Icon( - painterResource(R.drawable.ic_settlement), - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateSettlement() }, - DashboardMenuItem( - "Void", - { - Icon( - Icons.Default.Lock, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Void") }, - DashboardMenuItem( - "Refund", - { - Icon( - Icons.Default.Replay, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Refund") }, - DashboardMenuItem( - "Pre-Auth", - { - Icon( - Icons.Default.Lock, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Pre-Auth") }, - DashboardMenuItem( - "Pre-Auth Void", - { - Icon( - Icons.Default.LockOpen, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Pre-Auth Void") }, - DashboardMenuItem( - "Pre-Auth Complete", - { - Icon( - Icons.Default.CreditCard, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Pre-Auth Complete") }, - DashboardMenuItem( - "Pre-Auth Comp Void", - { - Icon( - Icons.Default.SwapHoriz, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Pre-Auth Complete Void") }, - DashboardMenuItem( - "Cash Out", - { - Icon( - Icons.Default.AccountBalanceWallet, - contentDescription = null, - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }) { onNavigateAction("Cash Out") }, -) - -@Composable -private fun MenuPager( - items: List, - modifier: Modifier = Modifier -) { - val pages = items.chunked(6) - val pagerState = rememberPagerState(pageCount = { pages.size }) - - Column(modifier = modifier) { - HorizontalPager( - state = pagerState, - modifier = Modifier - .fillMaxWidth() -// .height(236.dp) - ) { pageIndex -> - MenuPage(items = pages[pageIndex]) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - repeat(pages.size) { index -> - val selected = pagerState.currentPage == index - Box( - modifier = Modifier - .padding(horizontal = 4.dp) - .size(if (selected) 8.dp else 6.dp) - .background( - color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f), - shape = CircleShape - ) - ) - } - } - } -} - -@Composable -private fun MenuPage(items: List) { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.padding(horizontal = 16.dp) - ) { - items.chunked(3).forEach { rowItems -> - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - rowItems.forEach { item -> - MenuCard( - title = item.title, - icon = item.iconContent, - modifier = Modifier.weight(1f), - onClick = item.onClick - ) - } - repeat(3 - rowItems.size) { - Spacer(modifier = Modifier.weight(1f)) - } - } - } - } -} - -@Composable -private fun MenuCard( - title: String, - icon: @Composable () -> Unit, - modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null -) { - Card( - modifier = modifier - .height(100.dp) - .then( - if (onClick != null) { - Modifier.clickable(onClick = onClick) - } else { - Modifier - } - ), - shape = RoundedCornerShape(14.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - elevation = CardDefaults.cardElevation(5.dp) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(6.dp) - ) { - Column { - icon() - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = title, - style = MaterialTheme.typography.bodySmall, - color = Color.Black, - overflow = TextOverflow.Ellipsis, - maxLines = 2, - modifier = Modifier.weight(1f) - ) - Icon( - imageVector = Icons.Default.ChevronRight, - contentDescription = null, - tint = Color.LegacyRed, - modifier = Modifier.size(28.dp) - ) - } - } - } - } -} - -private fun LazyListScope.recentTransactionsItems(transactions: List) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Recent Transactions", - style = MaterialTheme.typography.titleSmall - ) - } - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = Color.Gray.copy(alpha = 0.2f) - ) - } - if (transactions.isEmpty()) { - item { - Text( - text = "No recent transactions", - color = Color.Gray, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp) - ) - } - } else { - items(items = transactions, key = { it.pid }) { record -> - TrnxRow(record = record) - } - } -} - -@Composable -private fun TrnxRow(record: TrnxRecord) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 4.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color.White) - .padding(horizontal = 12.dp, vertical = 10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .size(36.dp) - .background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape), - contentAlignment = Alignment.Center - ) { - Text( - text = record.typeLabel.take(1), - color = Color.LegacyRed, - style = MaterialTheme.typography.bodyLarge - ) - } - - Spacer(Modifier.width(10.dp)) - - Column(modifier = Modifier.weight(1f)) { - Text( - text =record.typeLabel, - style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold), - color = Color.Black - ) - Text( - text = record.maskedCard, - style = MaterialTheme.typography.labelSmall, - color = Color.Gray - ) - } - - Column(horizontalAlignment = Alignment.End) { - Text( - text = record.amountDisplay, - style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), - color = if (record.isVoided) Color.Gray else Color.LegacyRed - ) - Text( - text = record.dateTime, - style = MaterialTheme.typography.labelSmall, - color = Color.Gray - ) - } - } -} - -private val previewTransactions = listOf( - TrnxRecord( - 1L, - "Sale", - "MMK 10,000", - "**** 1234", - "06/10 14:32", - isVoided = false, - isApproved = true - ), - TrnxRecord( - 2L, - "Void", - "MMK 5,500", - "**** 5678", - "06/10 13:10", - isVoided = true, - isApproved = false - ), - TrnxRecord( - 3L, - "Refund", - "MMK 2,000", - "**** 9012", - "06/09 09:45", - isVoided = false, - isApproved = true - ), - TrnxRecord( - 4L, - "Sale", - "MMK 30,000", - "**** 3456", - "06/09 08:00", - isVoided = false, - isApproved = true - ), - TrnxRecord( - 5L, - "Settlement", - "MMK 0", - "----", - "06/08 18:00", - isVoided = false, - isApproved = true - ), -) - @RequiresApi(Build.VERSION_CODES.N) @P3Preview @P2Preview @@ -1068,91 +425,3 @@ private val previewTransactions = listOf( fun PreviewDashboardScreen2() { DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions)) } - -@Preview -@Composable -fun PreviewItem() { - Item( - onClick = {}, - title = "title", - subTitle = "sub-title" - ) -} - - -@RequiresApi(Build.VERSION_CODES.N) -@Composable -private fun rememberIsOnline() = run { - val context = LocalContext.current - val cm = remember(context) { - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - } - produceState( - initialValue = cm.activeNetwork - ?.let { cm.getNetworkCapabilities(it)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } - ?: false - ) { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { value = true } - override fun onLost(network: Network) { value = false } - } - cm.registerDefaultNetworkCallback(callback) - awaitDispose { cm.unregisterNetworkCallback(callback) } - } -} - -@Composable -fun Item( - onClick: () -> Unit, - title: String, - subTitle: String, - leadingIcon: (@Composable () -> Unit)? = null, - trailingIcon: (@Composable () -> Unit)? = null, -) { - Button( - onClick = onClick, - colors = ButtonDefaults.buttonColors( - containerColor = Color.White, - contentColor = Color.Black - ), - modifier = Modifier.fillMaxWidth() - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - ) { - // Square icon background - leadingIcon?.let { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(16.dp)) // change to RectangleShape for sharp corners - .background(Color.LegacyRed.copy(alpha = 0.1f)) - .padding(8.dp) - ) { - it() - } - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column( - modifier = Modifier - .weight(1f) - .padding(horizontal = 8.dp) - ) { - Text(text = title) - Text( - text = subTitle, - style = MaterialTheme.typography.bodySmall, - color = Color.Gray - ) - } - - trailingIcon?.invoke() - } - } -} diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUtils.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUtils.kt new file mode 100644 index 0000000..19a057f --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUtils.kt @@ -0,0 +1,42 @@ +package com.mob.utsmyanmar.ui.dashboard + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +fun formatRelativeTime(timestamp: Long?, now: Long): String { + if (timestamp == null) return "Never" + val diff = now - timestamp + return when { + diff < 60_000L -> "Just now" + diff < 3_600_000L -> "${diff / 60_000L} mins ago" + diff < 86_400_000L -> "${diff / 3_600_000L} hours ago" + else -> "${diff / 86_400_000L} days ago" + } +} + +@Composable +fun rememberIsOnline(): State = run { + val context = LocalContext.current + val cm = remember(context) { + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + } + produceState( + initialValue = cm.activeNetwork + ?.let { cm.getNetworkCapabilities(it)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } + ?: false + ) { + val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { value = true } + override fun onLost(network: Network) { value = false } + } + cm.registerDefaultNetworkCallback(callback) + awaitDispose { cm.unregisterNetworkCallback(callback) } + } +}