diff --git a/agrilinkvocpro/app/build.gradle.kts b/agrilinkvocpro/app/build.gradle.kts index 9ac7e6e..6cebdae 100644 --- a/agrilinkvocpro/app/build.gradle.kts +++ b/agrilinkvocpro/app/build.gradle.kts @@ -13,7 +13,7 @@ android { applicationId = "com.syaroful.agrilinkvocpro" minSdk = 29 targetSdk = 35 - versionCode = 13 + versionCode = 15 versionName = "1.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/DownloadModuleConfrmatioDialog.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/DownloadModuleConfrmatioDialog.kt index f24ceca..74cfc1b 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/DownloadModuleConfrmatioDialog.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/core/components/DownloadModuleConfrmatioDialog.kt @@ -23,8 +23,8 @@ fun DownloadModuleConfirmationDialog( val message = when (moduleName) { "control_feature" -> "Apakah Anda ingin mendownload fitur Kontrol Aktuator?" - "recipe_feature" -> "Apakah Anda ingin mendownload fitur Resep Pertumbuhan?" - "price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?" + "growth_recipe_feature" -> "Apakah Anda ingin mendownload fitur Formula Pertumbuhan Optimal?" + "commodity_price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?" "plant_disease_detection_feature" -> "Apakah Anda ingin mendownload fitur Deteksi Penyakit Tanaman?" else -> "Apakah Anda ingin mendownload modul ini?" } @@ -69,7 +69,7 @@ fun DownloadModuleConfirmationDialog( fun PreviewDownloadModuleConfirmationDialog() { DownloadModuleConfirmationDialog( - moduleName = "price_prediction_feature", + moduleName = "commodity_price_prediction_feature", onClickConfirm = {}, onDismiss = {} ) diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/model/BedLocationResponse.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/model/BedLocationResponse.kt new file mode 100644 index 0000000..f7281b2 --- /dev/null +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/model/BedLocationResponse.kt @@ -0,0 +1,32 @@ +package com.syaroful.agrilinkvocpro.data.model + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BedLocationResponse( + @SerialName("data") + val `data`: BedLocationInfo?, + @SerialName("message") + val message: String?, + @SerialName("statusCode") + val statusCode: Int? +) +@Serializable +data class BedLocationInfo( + @SerialName("address") + val address: String?, + @SerialName("createdAt") + val createdAt: String?, + @SerialName("deletedAt") + val deletedAt: String?, + @SerialName("description") + val description: String?, + @SerialName("id") + val id: Int?, + @SerialName("name") + val name: String?, + @SerialName("updatedAt") + val updatedAt: String? +) \ No newline at end of file diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/model/SensorInformationResponse.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/model/SensorInformationResponse.kt new file mode 100644 index 0000000..36aedd6 --- /dev/null +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/model/SensorInformationResponse.kt @@ -0,0 +1,36 @@ +package com.syaroful.agrilinkvocpro.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SensorInformationResponse( + @SerialName("data") + val `data`: SensorInformation?, + @SerialName("message") + val message: String?, + @SerialName("statusCode") + val statusCode: Int? +) + +@Serializable +data class SensorInformation( + @SerialName("bedLocationId") + val bedLocationId: Int?, + @SerialName("createdAt") + val createdAt: String?, + @SerialName("deletedAt") + val deletedAt: String?, + @SerialName("desc") + val desc: String?, + @SerialName("id") + val id: Int?, + @SerialName("name") + val name: String?, + @SerialName("publicName") + val publicName: String?, + @SerialName("sensorTypeId") + val sensorTypeId: Int?, + @SerialName("updatedAt") + val updatedAt: String? +) \ No newline at end of file diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/network/ApiService.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/network/ApiService.kt index 618a9a6..4adb5a4 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/network/ApiService.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/network/ApiService.kt @@ -1,15 +1,18 @@ package com.syaroful.agrilinkvocpro.data.network +import com.syaroful.agrilinkvocpro.data.model.BedLocationResponse import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse import com.syaroful.agrilinkvocpro.data.model.LoginResponse import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse import com.syaroful.agrilinkvocpro.data.model.RegisterResponse import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse +import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query @@ -61,4 +64,16 @@ interface ApiService { @Query("sensor") sensor: String ): Response + @GET("api/sensors/{sensorId}") + suspend fun getSensorInformation( + @Header("Authorization") authHeader: String, + @Path("sensorId") sensorId: Int + ): Response + + @GET("api/bed-locations/{bedId}") + suspend fun getBedLocation( + @Header("Authorization") authHeader: String, + @Path("bedId") bedId: Int + ): Response + } \ No newline at end of file diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/repository/SensorDataRepository.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/repository/SensorDataRepository.kt index e8426ef..da92369 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/repository/SensorDataRepository.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/data/repository/SensorDataRepository.kt @@ -1,8 +1,10 @@ package com.syaroful.agrilinkvocpro.data.repository +import com.syaroful.agrilinkvocpro.data.model.BedLocationResponse import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse +import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse import com.syaroful.agrilinkvocpro.data.network.ApiService import retrofit2.Response @@ -38,4 +40,19 @@ class SensorDataRepository(private val apiService: ApiService) { ): Response { return apiService.getDhtDataSensor(authHeader, startDate, endDate, timeRange, sensor) } + + suspend fun getSensorInformation( + authHeader: String, + sensorId: Int + ): Response{ + return apiService.getSensorInformation(authHeader, sensorId = sensorId) + } + + suspend fun getBedLocation( + authHeader: String, + bedId: Int + ): Response{ + return apiService.getBedLocation(authHeader, bedId = bedId) + } + } \ No newline at end of file diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailScreen.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailScreen.kt index 2966f5e..476388f 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailScreen.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailScreen.kt @@ -1,9 +1,16 @@ package com.syaroful.agrilinkvocpro.presentation.screen.detail +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -17,6 +24,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.dp +import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect +import com.syaroful.agrilinkvocpro.core.utils.ResultState import com.syaroful.agrilinkvocpro.core.utils.extention.getCurrentDate import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DetailDhtContent @@ -33,23 +44,27 @@ fun DetailScreen( detailViewModel: DetailViewModel = koinViewModel(), ) { val date = remember { mutableStateOf(Date().toFormattedString()) } + + val id = if (sensorId == "dht") 1 else if (sensorId == "npk1") 2 else 3 LaunchedEffect(sensorId) { if (sensorId == "dht") { detailViewModel.fetchDhtData(date = date.value, sensor = sensorId) + detailViewModel.fetchSensorInformation(id) } else { detailViewModel.fetchNpkData(date = date.value, sensor = sensorId) + detailViewModel.fetchSensorInformation(id) } } val npkDataState by detailViewModel.npkDataState.collectAsState() val dhtDataState by detailViewModel.dhtDataState.collectAsState() + val bedState by detailViewModel.bedState.collectAsState() val currentData = detailViewModel.currentSensorData val isRefreshing = remember { mutableStateOf(false) } val currentDate = remember { mutableStateOf(getCurrentDate()) } - Scaffold( topBar = { DetailTopBar() }) { innerPadding -> PullToRefreshBox( @@ -61,26 +76,72 @@ fun DetailScreen( detailViewModel.fetchNpkData(date = date.value, sensor = sensorId) } }) { - if (sensorId == "dht") DetailDhtContent( - modifier = Modifier.padding(innerPadding), - viewModel = detailViewModel, - dhtDataState = dhtDataState, - currentData = currentData, - sensorId = sensorId, - isRefreshing = isRefreshing, - date = date, - currentDate = currentDate - ) - else DetailNpkContent( - modifier = Modifier.padding(innerPadding), - viewModel = detailViewModel, - npkDataState = npkDataState, - currentData = currentData, - sensorId = sensorId, - isRefreshing = isRefreshing, - date = date, - currentDate = currentDate - ) + Column( + modifier = modifier + .padding(innerPadding) + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + ) { + when (bedState) { + is ResultState.Success -> { + val data = (bedState as ResultState.Success).data + ListItem( + modifier = Modifier + .padding(horizontal = 16.dp) + .border( + shape = RoundedCornerShape(8.dp), + width = 1.dp, + color = MaterialTheme.colorScheme.surfaceContainerHighest.copy( + alpha = 0.5f + ) + ), + headlineContent = { + Text(text = (data?.data?.address ?: "unknown"), style = MaterialTheme.typography.titleSmall) + }, + supportingContent = { + Text(text = (data?.data?.description ?: "unknown"), style = MaterialTheme.typography.bodySmall, modifier = Modifier.alpha(0.6f)) + }, + trailingContent = { + Box( + modifier = Modifier.height(48.dp).padding(vertical = 16.dp), + ){ Text(text = (data?.data?.name ?: "unknown")) } + } + ) + } + + is ResultState.Error -> { + val errorMessage = (bedState as ResultState.Error).message + Text(errorMessage) + } + + ResultState.Idle -> {} + ResultState.Loading -> Box( + modifier + .fillMaxWidth() + .height(80.dp) + .shimmerEffect() + ) + } + if (sensorId == "dht") DetailDhtContent( + viewModel = detailViewModel, + dhtDataState = dhtDataState, + currentData = currentData, + sensorId = sensorId, + isRefreshing = isRefreshing, + date = date, + currentDate = currentDate + ) + else DetailNpkContent( + viewModel = detailViewModel, + npkDataState = npkDataState, + currentData = currentData, + sensorId = sensorId, + isRefreshing = isRefreshing, + date = date, + currentDate = currentDate + ) + + } } } } diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailViewModel.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailViewModel.kt index 143f79c..c6f676f 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailViewModel.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/DetailViewModel.kt @@ -7,9 +7,11 @@ import com.syaroful.agrilinkvocpro.core.utils.ResultState import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString import com.syaroful.agrilinkvocpro.data.UserPreferences +import com.syaroful.agrilinkvocpro.data.model.BedLocationResponse import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse +import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -30,14 +32,75 @@ class DetailViewModel( val currentSensorData: SensorDataResponse? get() = sensorDataRepository.latestSensorData - private val _npkDataState = MutableStateFlow>(ResultState.Idle) + private val _npkDataState = + MutableStateFlow>(ResultState.Idle) val npkDataState: StateFlow> = _npkDataState.asStateFlow() - private val _dhtDataState = MutableStateFlow>(ResultState.Idle) + private val _dhtDataState = + MutableStateFlow>(ResultState.Idle) val dhtDataState: StateFlow> = _dhtDataState.asStateFlow() + private val _sensorInformationState = + MutableStateFlow>(ResultState.Idle) + val sensorInformationState: StateFlow> = + _sensorInformationState.asStateFlow() + + private val _bedState = + MutableStateFlow>(ResultState.Idle) + val bedState: StateFlow> = + _bedState.asStateFlow() + private val today = Date() + fun fetchSensorInformation(sensorId: Int) { + _bedState.value = ResultState.Loading + Log.d(TAG, "Sensor ID: $sensorId") + viewModelScope.launch { + val token = userPreferences.tokenFlow.first() + val authHeader = "Bearer $token" + try { + delay(300L) + val response = sensorDataRepository.getSensorInformation( + authHeader = authHeader, + sensorId = sensorId + ) + if (response.isSuccessful) { + Log.d(TAG, "Sensor Info: ${response.body()}") + response.body()?.let { body -> + val responseBed = sensorDataRepository.getBedLocation( + authHeader, + body.data?.bedLocationId ?: 1 + ) + if (responseBed.isSuccessful) { + Log.d(TAG, "Bed Location: ${responseBed.body()}") + responseBed.body()?.let { bedBody -> + _bedState.value = ResultState.Success(bedBody) + } ?: run { + _bedState.value = + ResultState.Error("Informasi Bed tidak ditemukan") + } + } else { + _bedState.value = + ResultState.Error("Error: ${responseBed.code()} - ${responseBed.message()}") + } + } ?: run { + _sensorInformationState.value = + ResultState.Error("Informasi Sensor tidak ditemukan") + } + } else { + _sensorInformationState.value = + ResultState.Error("Error: ${response.code()} - ${response.message()}") + } + } catch (e: Exception) { + val errorMessage = mapToUserFriendlyError(e) + _sensorInformationState.value = ResultState.Error(errorMessage) + _bedState.value = ResultState.Error(errorMessage) + Log.d(TAG, "Failed to fetch data: ${e.message}") + } + + } + } + fun fetchNpkData( date: String = today.toFormattedString(), sensor: String, @@ -62,7 +125,8 @@ class DetailViewModel( _npkDataState.value = ResultState.Error("Data tidak ditemukan") } } else { - _npkDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}") + _npkDataState.value = + ResultState.Error("Error: ${response.code()} - ${response.message()}") } } catch (e: Exception) { val errorMessage = mapToUserFriendlyError(e) @@ -96,7 +160,8 @@ class DetailViewModel( _dhtDataState.value = ResultState.Error("Data tidak ditemukan") } } else { - _dhtDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}") + _dhtDataState.value = + ResultState.Error("Error: ${response.code()} - ${response.message()}") } } catch (e: Exception) { val errorMessage = mapToUserFriendlyError(e) diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailDhtContent.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailDhtContent.kt index c6a7c36..cbe2a69 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailDhtContent.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailDhtContent.kt @@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -54,7 +52,6 @@ fun DetailDhtContent( Column( modifier = modifier .padding(16.dp) - .verticalScroll(rememberScrollState()) ) { when (dhtDataState) { is ResultState.Loading -> { diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailNpkContent.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailNpkContent.kt index 3b83fab..17f4ad1 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailNpkContent.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DetailNpkContent.kt @@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -57,7 +55,6 @@ fun DetailNpkContent( Column( modifier = modifier .padding(16.dp) - .verticalScroll(rememberScrollState()) ) { when (npkDataState) { is ResultState.Loading -> { diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DynamicBottomSheet.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DynamicBottomSheet.kt index 1730d01..a41fcdb 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DynamicBottomSheet.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/detail/component/DynamicBottomSheet.kt @@ -4,8 +4,10 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown @@ -53,6 +55,7 @@ fun DynamicBottomSheet( Spacer(modifier = Modifier.weight(1f)) Icon( imageVector = Icons.Filled.ArrowDropDown, + tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f), contentDescription = "Dropdown Arrow" ) } @@ -62,19 +65,24 @@ fun DynamicBottomSheet( onDismissRequest = { setShowSheet(false) }, sheetState = sheetState ) { - options.forEach { option -> - Text( - text = option, - modifier = Modifier - .fillMaxWidth() - .clickable { - onValueSelected(option) - setSelectedOption(option) - setShowSheet(false) - } - .padding(16.dp) - ) + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(options.size) { index -> + val option = options[index] + Text( + text = option, + modifier = Modifier + .fillMaxWidth() + .clickable { + onValueSelected(option) + setSelectedOption(option) + setShowSheet(false) + } + .padding(16.dp) + ) + } } } } -} +} \ No newline at end of file diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeScreen.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeScreen.kt index 351b4f2..c66e689 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeScreen.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeScreen.kt @@ -324,22 +324,22 @@ fun DynamicFeatureSection( horizontalArrangement = Arrangement.SpaceBetween ) { MenuItemButton( - label = "Kontrol\nAktuator", + label = "Control\nActuator", icon = painterResource(id = R.drawable.control_actuator_icon), onClick = { onFeatureClick("control_feature") }, ) MenuItemButton( - label = "Resep\nPertumbuhan", + label = "Growth\nFormula", icon = painterResource(id = R.drawable.growth_recipe_icon), onClick = { onFeatureClick("growth_recipe_feature") }, ) MenuItemButton( - label = "Harga\nKomoditas", + label = "Commodity\nPrice", icon = painterResource(id = R.drawable.commodity_price_prediction_icon), onClick = { onFeatureClick("commodity_price_prediction_feature") }, ) MenuItemButton( - label = "Deteksi\nPenyakit", + label = "Disease\nDetection", icon = painterResource(id = R.drawable.plant_disease_detection_icon), onClick = { onFeatureClick("plant_disease_detection_feature") }, ) @@ -378,7 +378,7 @@ fun GreenHouseInformationSection(navController: NavController) { modifier = Modifier.weight(1f) ) { - Text(text = "2 Komoditas", color = MainGreen, style = textTheme.bodyMedium) + Text(text = "2 Commodities", color = MainGreen, style = textTheme.bodyMedium) Spacer(modifier = Modifier.height(24.dp)) Text(text = "Green House Bumiaji", style = textTheme.bodyLarge) Text( @@ -397,15 +397,4 @@ fun GreenHouseInformationSection(navController: NavController) { .clip(RoundedCornerShape(10.dp)) ) } -} - -//@Preview(showBackground = true, name = "Light Mode") -//@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES) -//@Composable -//fun HomePreview() { -// val navController = rememberNavController() -// HomeScreen( -// navController = navController, -// onFeatureClick = {}, -// ) -//} \ No newline at end of file +} \ No newline at end of file diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeViewModel.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeViewModel.kt index 5ce0550..d7c6d1a 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeViewModel.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/home/HomeViewModel.kt @@ -24,9 +24,6 @@ class HomeViewModel( private val _homeState = MutableStateFlow>(ResultState.Idle) val homeState: StateFlow> = _homeState -// var currentDataSensor: SensorDataResponse? = null -// private set - init { getGreenHouseData() } diff --git a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/register/RegisterScreen.kt b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/register/RegisterScreen.kt index b1fe9c5..af86e8a 100644 --- a/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/register/RegisterScreen.kt +++ b/agrilinkvocpro/app/src/main/java/com/syaroful/agrilinkvocpro/presentation/screen/register/RegisterScreen.kt @@ -94,11 +94,11 @@ fun RegisterScreen( ) Text( modifier = Modifier.fillMaxWidth(), - text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center + text = "Register", style = textTheme.titleMedium, textAlign = TextAlign.Center ) Text( modifier = Modifier.fillMaxWidth(), - text = "Halo! yuk masuk ke dalam akunmu", + text = "Halo! yuk daftarkan akun ke aplikasi", style = textTheme.titleSmall.copy(color = DarkGrey), textAlign = TextAlign.Center ) diff --git a/agrilinkvocpro/app/src/main/res/values/strings.xml b/agrilinkvocpro/app/src/main/res/values/strings.xml index 0ac4689..88885bf 100644 --- a/agrilinkvocpro/app/src/main/res/values/strings.xml +++ b/agrilinkvocpro/app/src/main/res/values/strings.xml @@ -7,7 +7,7 @@ Kontrol Aktuator Google Play Store - Unduh Modul Fitur Dinamis + Unduh Fitur anda perlu mengunduh modul fitur dinamis agar fitur ini dapat digunakan Download Cancel diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/PricePredictionActivity.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/PricePredictionActivity.kt index b1f923f..60ac920 100644 --- a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/PricePredictionActivity.kt +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/PricePredictionActivity.kt @@ -4,17 +4,31 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PricePredictionScreen +import androidx.navigation.compose.rememberNavController +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di.appModule +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di.networkModule +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di.viewModelModule +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.navigation.SetupNavigation import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme +import org.koin.core.context.loadKoinModules +import org.koin.core.context.unloadKoinModules class PricePredictionActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + loadKoinModules(listOf(appModule, viewModelModule, networkModule)) setContent { AgrilinkVocproTheme { - PricePredictionScreen() + val navController = rememberNavController() + SetupNavigation(navController) } } } + + override fun onDestroy() { + super.onDestroy() + unloadKoinModules(listOf(appModule, viewModelModule, networkModule)) + } } \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/constant/AppConstant.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/constant/AppConstant.kt new file mode 100644 index 0000000..2e70c76 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/constant/AppConstant.kt @@ -0,0 +1,64 @@ +package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.constant + +class AppConstant { + val locationOption: List = listOf( + "Kota Batu", + "Kota Blitar", + "Kota Kediri", + "Kota Madiun", + "Kota Malang", + "Kota Mojokerto", + "Kota Pasuruan", + "Kota Probolinggo", + "Kota Surabaya", + "Kabupaten Bangkalan", + "Kabupaten Banyuwangi", + "Kabupaten Blitar", + "Kabupaten Bojonegoro", + "Kabupaten Bondowoso", + "Kabupaten Gresik", + "Kabupaten Jember", + "Kabupaten Jombang", + "Kabupaten Kediri", + "Kabupaten Lamongan", + "Kabupaten Lumajang", + "Kabupaten Madiun", + "Kabupaten Magetan", + "Kabupaten Malang", + "Kabupaten Mojokerto", + "Kabupaten Nganjuk", + "Kabupaten Ngawi", + "Kabupaten Pacitan", + "Kabupaten Pamekasan", + "Kabupaten Pasuruan", + "Kabupaten Ponorogo", + "Kabupaten Probolinggo", + "Kabupaten Sampang", + "Kabupaten Sidoarjo", + "Kabupaten Situbondo", + "Kabupaten Sumenep", + "Kabupaten Trenggalek", + "Kabupaten Tuban", + "Kabupaten Tulungagung", + ) + + val commodityOptions: List = listOf( + "Beras Premium", + "Beras Medium", + "Bawang Merah", + "Bawang Putih Sinco/Honan", + "BUNCIS", + "Cabe Merah Besar", + "Cabe Merah Keriting", + "Cabe Rawit Merah", + "Daging Ayam Kampung", + "Daging Ayam Ras", + "Daging Sapi Paha Belakang", + "Telur Ayam Kampung", + "Telur Ayam Ras", + "Tomat Merah", + "KENTANG", + "KOL/KUBIS", + "WORTEL", + ) +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityPriceScreen.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityPriceScreen.kt new file mode 100644 index 0000000..98f5fab --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityPriceScreen.kt @@ -0,0 +1,233 @@ +package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +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.LaunchedEffect +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.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.syaroful.agrilinkvocpro.R +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.constant.AppConstant +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.component.CommodityListItem +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.component.HeaderSection +import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent +import com.syaroful.agrilinkvocpro.core.components.Loader +import com.syaroful.agrilinkvocpro.core.utils.ResultState +import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString +import org.koin.androidx.compose.koinViewModel +import java.util.Calendar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CommodityPriceScreen( + navController: NavHostController, + viewModel: CommodityViewModel = koinViewModel() +) { + val state by viewModel.state.collectAsState() + + val commodityOptions = AppConstant().commodityOptions + val locationOptions = AppConstant().locationOption + + val selectedCommodity = remember { mutableStateOf(commodityOptions[0]) } + val selectedLocation = remember { mutableStateOf(locationOptions[0]) } + + val calendar = remember { Calendar.getInstance().apply { add(Calendar.DATE, -2) } } + val dateYesterday = calendar.time.toFormattedString() + + val isRefreshing = remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if (state == ResultState.Idle) { + viewModel.loadCommodityPrice( + commodityName = selectedCommodity.value, + market = "", + date = dateYesterday, + city = selectedLocation.value + ) + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + "Commodity Price", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + }, + ) + } + ) { innerPadding -> + + PullToRefreshBox( + isRefreshing = isRefreshing.value, + onRefresh = { + viewModel.loadCommodityPrice( + commodityName = selectedCommodity.value, + market = "", + date = dateYesterday, + city = selectedLocation.value + ) + } + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .padding(16.dp) + ) { + item { + HeaderSection( + navController = navController, + commodityOptions = commodityOptions, + locationOptions = locationOptions, + selectedCommodity = selectedCommodity, + selectedLocation = selectedLocation, + viewModel = viewModel, + dateYesterday = dateYesterday + ) + } + + + when (state) { + is ResultState.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + Loader() + } + } + } + + is ResultState.Success<*> -> { + val result = state as ResultState.Success + val data = result.data ?: emptyList() + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text("Data Komoditas", style = MaterialTheme.typography.titleSmall) + Text(dateYesterday, style = MaterialTheme.typography.titleSmall) + } + } + if (data.isEmpty()) { + item { + DefaultErrorComponent( + modifier = Modifier.padding(top = 16.dp), + label = "Waduh!", + message = "Belum ada data yang tersedia", + painter = painterResource(id = R.drawable.mascot_confused) + ) + } + } else { + items(data) { + CommodityListItem(it) + Spacer(modifier = Modifier.height(4.dp)) + } + } + } + + is ResultState.Error -> { + item { + DefaultErrorComponent( + label = "Oops!", + message = (state as ResultState.Error).message + ) + } + } + + ResultState.Idle -> {} + } + } + } + } +} + +// Column( +// modifier = Modifier +// .padding(16.dp) +// .fillMaxWidth() +// .background( +// color = MaterialTheme.colorScheme.surfaceContainerHigh, +// shape = RoundedCornerShape(12.dp) +// ) +// .padding(16.dp), +// verticalArrangement = Arrangement.spacedBy(12.dp) +// ) { +// Text( +// text = "Harga Hari Ini", +// style = MaterialTheme.typography.labelMedium, +// modifier = Modifier.alpha(0.5f) +// ) +// Row( +// horizontalArrangement = Arrangement.SpaceBetween +// ) { +// Text( +// text = "Rp 30.000 ", +// style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.W400) +// ) +// Text( +// text = "↑ 2.5%", +// style = MaterialTheme.typography.bodyLarge, +// color = MainGreen +// ) +// } +// Text( +// text = "Prediksi Besok", +// style = MaterialTheme.typography.labelMedium, +// modifier = Modifier.alpha(0.5f) +// ) +// Row( +// horizontalArrangement = Arrangement.SpaceBetween +// ) { +// Text( +// text = "Rp 28.000 ", +// style = MaterialTheme.typography.headlineMedium, +// color = MainGreen +// ) +// Text( +// text = "↓ 7,6%", +// style = MaterialTheme.typography.bodyLarge, +// color = Color.Red +// ) +// } +// PriceChart( +// modifier = Modifier +// .fillMaxWidth() +// .height(140.dp), +// day = listOf(1, 2, 3, 4, 5, 6, 7), +// values = listOf(5000.0, 6000.0, 4000.0, 7000.0, 8500.0, 5450.0, 6400.0), +// ) +// } diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/HeaderSection.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/HeaderSection.kt new file mode 100644 index 0000000..a80f39e --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/HeaderSection.kt @@ -0,0 +1,121 @@ +package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +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.NavHostController +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.component.FeatureBanner +import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.CommodityViewModel +import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DynamicBottomSheet +import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen + +@Composable +fun HeaderSection( + navController: NavHostController, + commodityOptions: List, + locationOptions: List, + selectedCommodity: MutableState, + selectedLocation: MutableState, + viewModel: CommodityViewModel, + dateYesterday: String, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(12.dp) + ) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + FeatureBanner() + PricePredictionButton { navController.navigate("prediction_screen") } + Text("Pilih Komoditas", style = MaterialTheme.typography.labelLarge) + + DynamicBottomSheet( + width = 1f, + options = commodityOptions, + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(8.dp) + ) + ) { + selectedCommodity.value = it + viewModel.loadCommodityPrice( + commodityName = it, + market = "", + date = dateYesterday, + city = selectedLocation.value + ) + } + + Text("Pilih Lokasi", style = MaterialTheme.typography.labelLarge) + + DynamicBottomSheet( + width = 1f, + options = locationOptions, + modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(8.dp) + ) + ) { + selectedLocation.value = it + viewModel.loadCommodityPrice( + commodityName = selectedCommodity.value, + market = "", + date = dateYesterday, + city = it + ) + } + } +} + + +@Composable +private fun PricePredictionButton(onClick: () -> Unit) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MainGreen + ), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(color = MainGreen, width = 1.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("✨ Lihat Prediksi Harga") + Image( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null + ) + } + } +} diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PriceChart.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PriceChart.kt index 4dc2037..17a63d9 100644 --- a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PriceChart.kt +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PriceChart.kt @@ -138,7 +138,7 @@ fun PriceChart( "Harga", spacing / 2f, labelPadding - 100, - textPaint.apply { color = android.graphics.Color.GREEN } + textPaint ) horizontalValue?.let { value -> diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PricePredictionScreen.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PricePredictionScreen.kt deleted file mode 100644 index 68adde7..0000000 --- a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PricePredictionScreen.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DynamicBottomSheet -import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PricePredictionScreen() { - val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry", "kentang", "Bayam") - val locationOptions = listOf("Malang", "Jombang", "Surabaya") - - Scaffold( - topBar = { - TopAppBar( - title = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - Text( - "Prediksi Harga Komoditas", - style = MaterialTheme.typography.titleMedium - ) - } - }, - navigationIcon = { - IconButton(onClick = { }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - ) - } - ) { innerPadding -> - Column( - modifier = Modifier.padding(innerPadding) - ) { - Column( - modifier = Modifier - .padding(16.dp) - .background( - color = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(12.dp) - ) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text(text = "Pilih Komoditas", style = MaterialTheme.typography.labelLarge) - DynamicBottomSheet( - width = 1f, - options = commodityOptions, - modifier = Modifier.background( - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(8.dp) - ) - ) { - - } - Text(text = "Pilih Lokasi", style = MaterialTheme.typography.labelLarge) - DynamicBottomSheet( - width = 1f, - options = locationOptions, - modifier = Modifier.background( - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(8.dp) - ) - ) { - - } - } - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - .background( - color = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(12.dp) - ) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "Harga Hari Ini", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.alpha(0.5f) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "Rp 30.000 ", - style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.W400) - ) - Text( - text = "↑ 2.5%", - style = MaterialTheme.typography.bodyLarge, - color = MainGreen - ) - } - Text( - text = "Prediksi Besok", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.alpha(0.5f) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "Rp 28.000 ", - style = MaterialTheme.typography.headlineMedium, - color = MainGreen - ) - Text( - text = "↓ 7,6%", - style = MaterialTheme.typography.bodyLarge, - color = Color.Red - ) - } - PriceChart( - modifier = Modifier - .fillMaxWidth() - .height(140.dp), - day = listOf(1, 2, 3, 4, 5, 6, 7), - values = listOf(5000.0, 6000.0, 4000.0, 7000.0, 8500.0, 5450.0, 6400.0), - ) - } - } - } - -} \ 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 index 66fdeba..421c2e9 100644 --- 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 @@ -1,13 +1,69 @@ package com.syaroful.agrilinkvocpro.control_feature.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable data class ActuatorHistoryResponse( - val `data`: List?, + @SerialName("data") + val `data`: List?, + @SerialName("lastPage") val lastPage: Int?, + @SerialName("message") val message: String?, + @SerialName("nextPage") val nextPage: Any?, + @SerialName("page") val page: Int?, + @SerialName("perPage") val perPage: Int?, + @SerialName("previousPage") val previousPage: Any?, + @SerialName("statusCode") val statusCode: Int?, + @SerialName("total") val total: Int? +) + +@Serializable +data class ActuatorHistoryData( + @SerialName("action") + val action: String?, + @SerialName("actuator") + val actuator: Actuator?, + @SerialName("actuatorId") + val actuatorId: Int?, + @SerialName("createdAt") + val createdAt: String?, + @SerialName("id") + val id: Int?, + @SerialName("triggeredBy") + val triggeredBy: String?, + @SerialName("turnOffAt") + val turnOffAt: Any? +) + +@Serializable +data class Actuator( + @SerialName("actuatorTypeId") + val actuatorTypeId: Int?, + @SerialName("bedLocationId") + val bedLocationId: Any?, + @SerialName("createdAt") + val createdAt: String?, + @SerialName("deletedAt") + val deletedAt: Any?, + @SerialName("id") + val id: Int?, + @SerialName("maxDuration") + val maxDuration: Int?, + @SerialName("name") + val name: String?, + @SerialName("relayPin") + val relayPin: Int?, + @SerialName("slug") + val slug: String?, + @SerialName("updatedAt") + val updatedAt: String? ) \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt index fd00174..05e5a0b 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt @@ -10,6 +10,7 @@ import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.Query interface ControlService { // get all actuator status @@ -43,8 +44,17 @@ interface ControlService { ): Response // get controls log - @GET("api//actuator-control-logs") + @GET("api/actuator-control-logs") suspend fun getActuatorsControlLog( @Header("Authorization") authHeader: String, + @Query("actuatorId") actuatorId: Int? = null, + @Query("status") status: String? = null, + @Query("startDate") startDate: String? = null, + @Query("endDate") endDate: String? = null, + @Query("page") page: Int? = null, + @Query("limit") limit: Int? = null, + @Query("sort_field") sortField: String? = null, + @Query("sort_direction") sortDirection: String? = null ): Response + } \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt index d42b534..9044291 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt @@ -37,8 +37,29 @@ class ControlRepository( return controlService.getActuatorStatus(authHeader) } - suspend fun getActuatorHistory(authHeader: String): Response { - return controlService.getActuatorsControlLog(authHeader) + suspend fun getActuatorHistory( + authHeader: String, + actuatorId: Int? = null, + status: String? = null, + startDate: String? = null, + endDate: String? = null, + page: Int? = null, + limit: Int? = null, + sortField: String? = null, + sortDirection: String? = null + ): Response { + return controlService.getActuatorsControlLog( + authHeader = authHeader, + actuatorId = actuatorId, + status = status, + startDate = startDate, + endDate = endDate, + page = page, + limit = limit, + sortField = sortField, + sortDirection = sortDirection + ) } + } \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt deleted file mode 100644 index 03fa266..0000000 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt +++ /dev/null @@ -1,75 +0,0 @@ -//package com.syaroful.agrilinkvocpro.control_feature.page -// -//import android.content.res.Configuration.UI_MODE_NIGHT_YES -//import androidx.compose.foundation.layout.Arrangement -//import androidx.compose.foundation.layout.Column -//import androidx.compose.foundation.layout.Spacer -//import androidx.compose.foundation.layout.fillMaxSize -//import androidx.compose.foundation.layout.height -//import androidx.compose.foundation.layout.padding -//import androidx.compose.material3.MaterialTheme -//import androidx.compose.material3.Scaffold -//import androidx.compose.material3.Switch -//import androidx.compose.material3.Text -//import androidx.compose.runtime.Composable -//import androidx.compose.runtime.collectAsState -//import androidx.compose.runtime.getValue -//import androidx.compose.ui.Alignment -//import androidx.compose.ui.Modifier -//import androidx.compose.ui.tooling.preview.Preview -//import androidx.compose.ui.unit.dp -//import androidx.lifecycle.viewmodel.compose.viewModel -//import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel -// -//@Composable -//fun ControlScreen( -// modifier: Modifier = Modifier, -// relayState: Boolean, -// onRelayStateChange: (Boolean) -> Unit -//) { -// Scaffold { innerPadding -> -// Column( -// modifier = Modifier -// .fillMaxSize() -// .padding(innerPadding) -// .padding(24.dp), -// verticalArrangement = Arrangement.Center, -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Text(text = "Kontrol Relay", style = MaterialTheme.typography.headlineMedium) -// -// Spacer(modifier = Modifier.height(24.dp)) -// -// Switch( -// checked = relayState, -// onCheckedChange = { isChecked -> -// onRelayStateChange(isChecked) -// } -// ) -// } -// } -//} -// -//@Composable -//fun ControlScreenRoute( -// modifier: Modifier = Modifier, -// viewModel: ControlViewModel = viewModel() -//) { -// val relayState by viewModel.relayState.collectAsState() -// -// ControlScreen( -// modifier = modifier, -// relayState = relayState, -// onRelayStateChange = { viewModel.setRelayState(it) } -// ) -//} -// -//@Preview(showBackground = true, name = "Light Mode") -//@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES) -//@Composable -//fun ControlScreenPreview() { -// ControlScreen( -// relayState = false, -// onRelayStateChange = {} -// ) -//} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt index c56f4c8..0c172a5 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt @@ -1,21 +1,24 @@ package com.syaroful.agrilinkvocpro.control_feature.presentation.control +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -38,9 +41,11 @@ import com.syaroful.agrilinkvocpro.control_feature.R import com.syaroful.agrilinkvocpro.control_feature.core.components.ControlCard import com.syaroful.agrilinkvocpro.control_feature.core.components.CustomSnackBar import com.syaroful.agrilinkvocpro.control_feature.core.components.DisableControlCard +import com.syaroful.agrilinkvocpro.control_feature.core.components.FeatureBanner import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect +import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen import kotlinx.coroutines.flow.collectLatest @OptIn(ExperimentalMaterial3Api::class) @@ -88,14 +93,6 @@ fun ControlActuatorScreen( Text("Control Actuator", style = MaterialTheme.typography.titleMedium) } }, - - actions = { - IconButton(onClick = { - navController.navigate("control_history") - }) { - Icon(Icons.Filled.Refresh, contentDescription = "History") - } - } ) } ) { innerPadding -> @@ -115,9 +112,11 @@ fun ControlActuatorScreen( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { + FeatureBanner() + HistoryButton { + navController.navigate("control_history") + } Text("Daftar Actuator", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(1.dp)) - when (allActuators) { is ControlState.Loading -> { isRefreshing.value = false @@ -181,3 +180,30 @@ fun ControlActuatorScreen( } } } + +@Composable +private fun HistoryButton(onClick: () -> Unit) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MainGreen + ), + shape = RoundedCornerShape(8.dp), + border = BorderStroke(color = MainGreen, width = 1.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Control History") + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + tint = MainGreen, + contentDescription = "button history" + ) + } + } +} 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 index 104b76c..bd6958c 100644 --- 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 @@ -1,11 +1,17 @@ package com.syaroful.agrilinkvocpro.control_feature.presentation.history +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement 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.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.CircularProgressIndicator @@ -19,15 +25,19 @@ 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.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import com.syaroful.agrilinkvocpro.control_feature.R 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 @@ -53,7 +63,10 @@ fun ControlHistoryScreen( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - Text("Riwayat Kontrol Aktuator", style = MaterialTheme.typography.titleMedium) + Text( + "Riwayat Kontrol Aktuator", + style = MaterialTheme.typography.titleMedium + ) } }, navigationIcon = { @@ -66,27 +79,53 @@ fun ControlHistoryScreen( ) }, - ) { innerPadding -> + ) { innerPadding -> PullToRefreshBox( + modifier = Modifier + .fillMaxSize(), isRefreshing = isRefreshing.value, onRefresh = { isRefreshing.value = true - controlHistoryViewModel.getActuatorHistory() + controlHistoryViewModel.loadInitialHistory() }, ) { + val historyList = controlHistoryViewModel.historyList + val listState = rememberLazyListState() + + LaunchedEffect(listState) { + snapshotFlow { + val layoutInfo = listState.layoutInfo + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val totalItems = layoutInfo.totalItemsCount + lastVisibleItem to totalItems + }.collect { (lastVisible, totalItems) -> + if (lastVisible >= totalItems - 1 && !controlHistoryViewModel.isLastPage) { + controlHistoryViewModel.loadMoreHistory() + } + } + } + LazyColumn( + state = listState, modifier = Modifier .padding(innerPadding) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center ) { - when (historyState) { + when (val state = historyState) { + is ControlState.Loading -> { + if (historyList.isEmpty()) { + item { + CircularProgressIndicator(color = MainGreen) + } + } + } + is ControlState.Error -> { isRefreshing.value = false item { DefaultErrorComponent( - message = (historyState as ControlState.Error).message, + message = state.message, label = "Oops!", ) } @@ -94,52 +133,95 @@ fun ControlHistoryScreen( 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 - + if (historyList.isEmpty()) { + item { + DefaultErrorComponent( + label = "Data kosong", + message = "Tidak ada riwayat kontrol yang tersedia" ) } } else { item { - Text("Tidak ada data riwayat kontrol") + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + (historyState as? ControlState.Success)?.data?.let { response -> + Text("Total: ${response.total}", style = MaterialTheme.typography.titleSmall) + } ?: run { + Text("Total: -", style = MaterialTheme.typography.titleSmall) + } + +// IconButton( +// modifier = Modifier +// .background( +// shape = RoundedCornerShape(4.dp), +// color = MainGreen.copy(alpha = 0.1f) +// ) +// .size(40.dp), +// onClick = { }, +// ) { +// Icon( +// modifier = Modifier.size(24.dp), +// painter = painterResource(R.drawable.ic_filter), +// tint = MainGreen, +// contentDescription = "Choose bet" +// ) +// } + } + } + items(historyList) { historyData -> + historyData.let { + ListItem( + leadingContent = { + Icon( + painter = painterResource(id = R.drawable.power), + contentDescription = null, + tint = if (it.action == "ON") MainGreen else Color.Red + ) + }, + headlineContent = { + val stateText = + it.actuator?.name + Text("$stateText") + }, + supportingContent = { + Text(it.triggeredBy ?: "", style = MaterialTheme.typography.bodySmall) + }, + trailingContent = { + Text(getRelativeTime(it.createdAt ?: ""), style = MaterialTheme.typography.bodySmall) + }, + shadowElevation = 0.dp, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 4.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(24.dp) + ) + ) + } + } + + // Infinite scroll loading indicator + if (!controlHistoryViewModel.isLastPage) { + item { + CircularProgressIndicator( + color = MainGreen, + modifier = Modifier + .padding(16.dp) + .size(24.dp) + ) + } } } } - is ControlState.Loading -> { - item { - CircularProgressIndicator( - color = MainGreen - ) - } - } - - else -> {} + else -> Unit } } } + } } \ 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 index 0faf9f7..26d1718 100644 --- 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 @@ -1,15 +1,16 @@ 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.ActuatorHistoryData 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.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -22,35 +23,88 @@ class ControlHistoryViewModel( private val _historyState = MutableStateFlow>(ControlState.Idle) - val historyState: MutableStateFlow> = _historyState + val historyState: StateFlow> = _historyState + + private val _historyList = mutableListOf() + val historyList: List get() = _historyList + + private var currentPage = 1 + private val limitPerPage = 10 + private var isLoadingMore = false + var isLastPage = false + private set init { - getActuatorHistory() + loadInitialHistory() } - fun getActuatorHistory() { + fun loadInitialHistory() { _historyState.value = ControlState.Loading + + // Reset pagination state + currentPage = 1 + isLastPage = false + isLoadingMore = false + _historyList.clear() + viewModelScope.launch { val token = userPreferences.tokenFlow.first() val authHeader = "Bearer $token" - try { delay(500L) - val response = repository.getActuatorHistory(authHeader = authHeader) + val response = repository.getActuatorHistory( + authHeader = authHeader, + page = currentPage, + limit = limitPerPage + ) if (response.isSuccessful) { - val data = response.body() - _historyState.value = ControlState.Success(data) - Log.d(TAG, "Successfully get Actuator History ${response.body()}") + val body = response.body() + _historyList.clear() + val safeData = body?.data?.filterNotNull() ?: emptyList() + _historyList.addAll(safeData) + isLastPage = currentPage >= (body?.lastPage ?: 1) + _historyState.value = ControlState.Success(body) } else { - val errorBody = response.errorBody()?.string() - val errorMessage = errorBody ?: "Error ${response.code()}" - _historyState.value = ControlState.Error(errorMessage) + _historyState.value = ControlState.Error(response.message()) } } catch (e: Exception) { - val errorMessage = mapToUserFriendlyError(e) - _historyState.value = ControlState.Error(errorMessage) - + _historyState.value = ControlState.Error(mapToUserFriendlyError(e)) } } } -} \ No newline at end of file + + fun loadMoreHistory() { + if (isLoadingMore || isLastPage) return + + isLoadingMore = true + currentPage += 1 + + viewModelScope.launch { + val token = userPreferences.tokenFlow.first() + val authHeader = "Bearer $token" + try { + val response = repository.getActuatorHistory( + authHeader = authHeader, + page = currentPage, + limit = limitPerPage + ) + if (response.isSuccessful) { + val body = response.body() + val safeData = body?.data?.filterNotNull() ?: emptyList() + _historyList.addAll(safeData) + isLastPage = currentPage >= (body?.lastPage ?: 1) + _historyState.value = ControlState.Success(body) + } else { + currentPage -= 1 + _historyState.value = ControlState.Error(response.message()) + } + } catch (e: Exception) { + currentPage -= 1 + _historyState.value = ControlState.Error(mapToUserFriendlyError(e)) + } finally { + isLoadingMore = false + } + } + } +} + diff --git a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthRecipeFeatureBanner.kt b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthRecipeFeatureBanner.kt index 56b8b9e..1b963f4 100644 --- a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthRecipeFeatureBanner.kt +++ b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/core/component/GrowthRecipeFeatureBanner.kt @@ -39,7 +39,7 @@ fun GrowthRecipeFeatureBanner(){ style = MaterialTheme.typography.titleMedium.copy(color = Color.White) ) Text( - "Lihat rekomendasi perawatan optimal untuk setiap jenis tanaman", + "Lihat rekomendasi perawatan optimal", style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f)) ) } diff --git a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeScreen.kt b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeScreen.kt index c766dd0..fa12270 100644 --- a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeScreen.kt +++ b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeScreen.kt @@ -14,10 +14,13 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -28,6 +31,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -52,21 +56,25 @@ fun GrowthRecipeScreen( navController: NavController, viewModel: GrowthRecipeViewModel = koinViewModel() ) { - val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry") + val sensorIdOptions: Map = mapOf("Bed 1" to "npk1", "Bed 2" to "npk2") val sensorOptions = listOf("Nitrogen", "Pospor", "Kalium") + val selectedSensor = remember { mutableStateOf("Nitrogen") } + val selectedSensorId = remember { mutableStateOf(sensorIdOptions.values.first()) } val graphicState by viewModel.getGraphicState.collectAsState() val isRefreshing = remember { mutableStateOf(false) } + var expanded by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { - viewModel.getGraphicData("npk1") + viewModel.getGraphicData(selectedSensorId.value) } PullToRefreshBox( isRefreshing = isRefreshing.value, onRefresh = { isRefreshing.value = true - viewModel.getGraphicData("npk1") + viewModel.getGraphicData(selectedSensorId.value) }, ) { Scaffold( @@ -104,10 +112,20 @@ fun GrowthRecipeScreen( }, title = "✨ Lihat saran perawatan" ) - Text( - "🪴 Grafik nutrisi dalam 10 hari", - style = MaterialTheme.typography.titleMedium - ) + Row( + modifier = Modifier.fillMaxWidth() + , + horizontalArrangement = Arrangement.SpaceBetween + ){ + Text( + "🪴 Grafik nutrisi dalam 10 hari", + style = MaterialTheme.typography.titleMedium + ) + Text( + selectedSensorId.value, + style = MaterialTheme.typography.titleMedium + ) + } Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically @@ -119,7 +137,7 @@ fun GrowthRecipeScreen( color = MainGreen.copy(alpha = 0.1f) ) .size(40.dp), - onClick = { }, + onClick = {expanded = !expanded}, ) { Icon( modifier = Modifier.size(24.dp), @@ -127,6 +145,27 @@ fun GrowthRecipeScreen( tint = MainGreen, contentDescription = "Choose bet" ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + sensorIdOptions.forEach { item -> + DropdownMenuItem( + text = { + Text( + item.key, + modifier = Modifier.fillMaxWidth() + ) + }, + colors = MenuDefaults.itemColors(MaterialTheme.colorScheme.onBackground), + onClick = { + selectedSensorId.value = item.value + expanded = false + viewModel.getGraphicData(selectedSensorId.value) + } + ) + } + } } Spacer(modifier = Modifier.width(8.dp)) DynamicBottomSheet( @@ -163,7 +202,7 @@ fun GrowthRecipeScreen( isRefreshing.value = false val dataList = (graphicState as ResultState.Success).data?.data?.get( - "npk1" + selectedSensorId.value ).orEmpty() val days = dataList.mapNotNull { it.day?.toInt() } @@ -191,7 +230,7 @@ fun GrowthRecipeScreen( } } Text( - "🌱 Saran Nutrisi Optimal", + "🌱 Saran Perbandingan Nutrisi Optimal", style = MaterialTheme.typography.titleMedium ) Row( @@ -199,26 +238,34 @@ fun GrowthRecipeScreen( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Column(horizontalAlignment = Alignment.Start) { + Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Nutrisi") Text("N") Text("P") Text("K") } - Column(horizontalAlignment = Alignment.Start) { + Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Vegetatif") - Text("30 ppm") - Text("40 ppm") - Text("50 ppm") + NutrientStandartBox("30") + NutrientStandartBox("10") + NutrientStandartBox("10") } - Column(horizontalAlignment = Alignment.Start) { + Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Generatif") - Text("50 ppm") - Text("60 ppm") - Text("70 ppm") + NutrientStandartBox("10") + NutrientStandartBox("20") + NutrientStandartBox("20") } } } } } +} + +@Composable +private fun NutrientStandartBox( + label: String +){ + Box(modifier = Modifier.background(shape = RoundedCornerShape(8.dp), color = MaterialTheme.colorScheme.surfaceContainer).padding(horizontal = 8.dp, vertical = 4.dp)){ Text(label, style = MaterialTheme.typography.labelSmall) } + } \ No newline at end of file diff --git a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeViewModel.kt b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeViewModel.kt index 87f4bb1..8b778f0 100644 --- a/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeViewModel.kt +++ b/agrilinkvocpro/growth_recipe_feature/src/main/java/com/syaroful/agrilinkvocpro/growth_recipe_feature/presentation/recipe/GrowthRecipeViewModel.kt @@ -51,6 +51,7 @@ class GrowthRecipeViewModel( sensor = sensor, ) if (response.isSuccessful) { + Log.d(TAG, "Response: ${response.body()}") response.body()?.let { body -> _getGraphicState.value = ResultState.Success(body) } ?: run { diff --git a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt index 21aefa9..e1b911c 100644 --- a/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt +++ b/agrilinkvocpro/plant_disease_detection_feature/src/main/java/com/syaroful/agrilinkvocpro/plant_disease_detection_feature/presentation/history/DetailHistoryScreen.kt @@ -40,6 +40,7 @@ import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBitmap import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.TextCardComponent +import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen @OptIn(ExperimentalMaterial3Api::class) @@ -121,13 +122,13 @@ fun DetailHistoryScreen( Box( modifier = Modifier .background( - color = Color.Red, + color = if (plantDiagnosis?.diagnosis == "Sehat") MainGreen else Color.Red, shape = RoundedCornerShape(4.dp) ) .padding(vertical = 4.dp, horizontal = 8.dp) ) { Text( - text = "Terkena Penyakit", + text = if (plantDiagnosis?.diagnosis == "Sehat") "Sehat" else "Terkena Penyakit", color = Color.White, style = MaterialTheme.typography.labelMedium ) diff --git a/skripsi/Draft Skripsi - Muhamad Syaroful Anam.docx b/skripsi/Draft Skripsi - Muhamad Syaroful Anam.docx deleted file mode 100644 index 84fc0ab..0000000 Binary files a/skripsi/Draft Skripsi - Muhamad Syaroful Anam.docx and /dev/null differ diff --git a/skripsi/Proposal Skripsi - Muhamad Syaroful Anam.pdf b/skripsi/Proposal Skripsi - Muhamad Syaroful Anam.pdf deleted file mode 100644 index c50be43..0000000 Binary files a/skripsi/Proposal Skripsi - Muhamad Syaroful Anam.pdf and /dev/null differ diff --git a/snapshoot/Screenshot_20250611_214911.png b/snapshoot/Screenshot_20250611_214911.png deleted file mode 100644 index 915a955..0000000 Binary files a/snapshoot/Screenshot_20250611_214911.png and /dev/null differ