diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/utils/TimeExtention.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/utils/TimeExtention.kt new file mode 100644 index 0000000..cee9ecb --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/utils/TimeExtention.kt @@ -0,0 +1,28 @@ +package com.syaroful.agrilinkvocpro.control_feature.core.utils + +import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +fun getRelativeTime(isoTime: String): String { + return try { + val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME + val dateTime = OffsetDateTime.parse(isoTime, formatter) + val now = OffsetDateTime.now(ZoneId.systemDefault()) + + val duration = Duration.between(dateTime, now) + + when { + duration.toMinutes() < 1 -> "baru saja" + duration.toMinutes() < 60 -> "${duration.toMinutes()} menit yang lalu" + duration.toHours() < 24 -> "${duration.toHours()} jam yang lalu" + duration.toDays() < 7 -> "${duration.toDays()} hari yang lalu" + duration.toDays() < 30 -> "${duration.toDays() / 7} minggu yang lalu" + duration.toDays() < 365 -> "${duration.toDays() / 30} bulan yang lalu" + else -> "${duration.toDays() / 365} tahun yang lalu" + } + } catch (e: Exception) { + "Format tidak valid" + } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorHistoryResponse.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorHistoryResponse.kt new file mode 100644 index 0000000..66fdeba --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorHistoryResponse.kt @@ -0,0 +1,13 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.model + +data class ActuatorHistoryResponse( + val `data`: List?, + val lastPage: Int?, + val message: String?, + val nextPage: Any?, + val page: Int?, + val perPage: Int?, + val previousPage: Any?, + val statusCode: Int?, + val total: Int? +) \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt index 620a3fa..b233730 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt @@ -1,12 +1,12 @@ package com.syaroful.agrilinkvocpro.control_feature.navigation -import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlActuatorScreen import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel +import com.syaroful.agrilinkvocpro.control_feature.presentation.history.ControlHistoryScreen import org.koin.androidx.compose.koinViewModel @Composable @@ -15,10 +15,12 @@ fun NavGraph(navController: NavHostController) { NavHost(navController, startDestination = "control_screen") { composable("control_screen") { - ControlActuatorScreen(controlViewModel) + ControlActuatorScreen(controlViewModel, navController) } - composable("history_control_screen") { - Box() {} + composable("control_history") { + ControlHistoryScreen( + navController = navController + ) } } } \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/history/ControlHistoryScreen.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/history/ControlHistoryScreen.kt new file mode 100644 index 0000000..104b76c --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/history/ControlHistoryScreen.kt @@ -0,0 +1,145 @@ +package com.syaroful.agrilinkvocpro.control_feature.presentation.history + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState +import com.syaroful.agrilinkvocpro.control_feature.core.utils.getRelativeTime +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryResponse +import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent +import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ControlHistoryScreen( + controlHistoryViewModel: ControlHistoryViewModel = koinViewModel(), + navController: NavController +) { + val historyState by controlHistoryViewModel.historyState.collectAsState() + + val isRefreshing = remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text("Riwayat Kontrol Aktuator", style = MaterialTheme.typography.titleMedium) + } + }, + navigationIcon = { + IconButton(onClick = { + navController.popBackStack() + }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }, + ) + }, + + ) { innerPadding -> + PullToRefreshBox( + isRefreshing = isRefreshing.value, + onRefresh = { + isRefreshing.value = true + controlHistoryViewModel.getActuatorHistory() + }, + ) { + LazyColumn( + modifier = Modifier + .padding(innerPadding) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + when (historyState) { + is ControlState.Error -> { + isRefreshing.value = false + item { + DefaultErrorComponent( + message = (historyState as ControlState.Error).message, + label = "Oops!", + ) + } + } + + is ControlState.Success -> { + isRefreshing.value = false + val data = + (historyState as ControlState.Success).data?.data + if (!data.isNullOrEmpty()) { + val reversedData = data.reversed() + items(reversedData) { historyData -> + ListItem( + leadingContent = { + Text("${historyData?.actuatorId}") + }, + headlineContent = { + val state = + if (historyData?.action == "ON") "Diaktifkan oleh " else "Dinonaktifkan oleh " + Text(text = "$state ${historyData?.triggeredBy}") + }, + supportingContent = { + Text( + text = getRelativeTime(historyData?.createdAt.toString()) + ) + }, + trailingContent = { + Text("${historyData?.action}", + color = if (historyData?.action == "ON") MainGreen else Color.Red + ) + }, + shadowElevation = 2.dp + + ) + } + } else { + item { + Text("Tidak ada data riwayat kontrol") + } + } + } + + is ControlState.Loading -> { + item { + CircularProgressIndicator( + color = MainGreen + ) + } + } + + else -> {} + } + } + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/history/ControlHistoryViewModel.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/history/ControlHistoryViewModel.kt new file mode 100644 index 0000000..0faf9f7 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/history/ControlHistoryViewModel.kt @@ -0,0 +1,56 @@ +package com.syaroful.agrilinkvocpro.control_feature.presentation.history + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryResponse +import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository +import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError +import com.syaroful.agrilinkvocpro.data.UserPreferences +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +private const val TAG = "ControlHistoryViewModel" + +class ControlHistoryViewModel( + private val userPreferences: UserPreferences, + private val repository: ControlRepository +) : ViewModel() { + + private val _historyState = + MutableStateFlow>(ControlState.Idle) + val historyState: MutableStateFlow> = _historyState + + init { + getActuatorHistory() + } + + fun getActuatorHistory() { + _historyState.value = ControlState.Loading + viewModelScope.launch { + val token = userPreferences.tokenFlow.first() + val authHeader = "Bearer $token" + + try { + delay(500L) + val response = repository.getActuatorHistory(authHeader = authHeader) + if (response.isSuccessful) { + val data = response.body() + _historyState.value = ControlState.Success(data) + Log.d(TAG, "Successfully get Actuator History ${response.body()}") + } else { + val errorBody = response.errorBody()?.string() + val errorMessage = errorBody ?: "Error ${response.code()}" + _historyState.value = ControlState.Error(errorMessage) + } + } catch (e: Exception) { + val errorMessage = mapToUserFriendlyError(e) + _historyState.value = ControlState.Error(errorMessage) + + } + } + } +} \ No newline at end of file