refactor: fix some feature
This commit is contained in:
parent
ac5f7c1619
commit
faa8f2ea76
|
|
@ -13,7 +13,7 @@ android {
|
||||||
applicationId = "com.syaroful.agrilinkvocpro"
|
applicationId = "com.syaroful.agrilinkvocpro"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 13
|
versionCode = 15
|
||||||
versionName = "1.0.1"
|
versionName = "1.0.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ fun DownloadModuleConfirmationDialog(
|
||||||
|
|
||||||
val message = when (moduleName) {
|
val message = when (moduleName) {
|
||||||
"control_feature" -> "Apakah Anda ingin mendownload fitur Kontrol Aktuator?"
|
"control_feature" -> "Apakah Anda ingin mendownload fitur Kontrol Aktuator?"
|
||||||
"recipe_feature" -> "Apakah Anda ingin mendownload fitur Resep Pertumbuhan?"
|
"growth_recipe_feature" -> "Apakah Anda ingin mendownload fitur Formula Pertumbuhan Optimal?"
|
||||||
"price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?"
|
"commodity_price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?"
|
||||||
"plant_disease_detection_feature" -> "Apakah Anda ingin mendownload fitur Deteksi Penyakit Tanaman?"
|
"plant_disease_detection_feature" -> "Apakah Anda ingin mendownload fitur Deteksi Penyakit Tanaman?"
|
||||||
else -> "Apakah Anda ingin mendownload modul ini?"
|
else -> "Apakah Anda ingin mendownload modul ini?"
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +69,7 @@ fun DownloadModuleConfirmationDialog(
|
||||||
fun PreviewDownloadModuleConfirmationDialog() {
|
fun PreviewDownloadModuleConfirmationDialog() {
|
||||||
|
|
||||||
DownloadModuleConfirmationDialog(
|
DownloadModuleConfirmationDialog(
|
||||||
moduleName = "price_prediction_feature",
|
moduleName = "commodity_price_prediction_feature",
|
||||||
onClickConfirm = {},
|
onClickConfirm = {},
|
||||||
onDismiss = {}
|
onDismiss = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
)
|
||||||
|
|
@ -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?
|
||||||
|
)
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
package com.syaroful.agrilinkvocpro.data.network
|
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.DhtGraphicDataResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
|
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
|
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.RegisterResponse
|
import com.syaroful.agrilinkvocpro.data.model.RegisterResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
|
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
|
||||||
|
import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Header
|
import retrofit2.http.Header
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,4 +64,16 @@ interface ApiService {
|
||||||
@Query("sensor") sensor: String
|
@Query("sensor") sensor: String
|
||||||
): Response<DhtGraphicDataResponse>
|
): Response<DhtGraphicDataResponse>
|
||||||
|
|
||||||
|
@GET("api/sensors/{sensorId}")
|
||||||
|
suspend fun getSensorInformation(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
@Path("sensorId") sensorId: Int
|
||||||
|
): Response<SensorInformationResponse>
|
||||||
|
|
||||||
|
@GET("api/bed-locations/{bedId}")
|
||||||
|
suspend fun getBedLocation(
|
||||||
|
@Header("Authorization") authHeader: String,
|
||||||
|
@Path("bedId") bedId: Int
|
||||||
|
): Response<BedLocationResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.syaroful.agrilinkvocpro.data.repository
|
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.DhtGraphicDataResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
|
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
|
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
|
||||||
|
import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.network.ApiService
|
import com.syaroful.agrilinkvocpro.data.network.ApiService
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
|
|
@ -38,4 +40,19 @@ class SensorDataRepository(private val apiService: ApiService) {
|
||||||
): Response<DhtGraphicDataResponse> {
|
): Response<DhtGraphicDataResponse> {
|
||||||
return apiService.getDhtDataSensor(authHeader, startDate, endDate, timeRange, sensor)
|
return apiService.getDhtDataSensor(authHeader, startDate, endDate, timeRange, sensor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getSensorInformation(
|
||||||
|
authHeader: String,
|
||||||
|
sensorId: Int
|
||||||
|
): Response<SensorInformationResponse>{
|
||||||
|
return apiService.getSensorInformation(authHeader, sensorId = sensorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getBedLocation(
|
||||||
|
authHeader: String,
|
||||||
|
bedId: Int
|
||||||
|
): Response<BedLocationResponse>{
|
||||||
|
return apiService.getBedLocation(authHeader, bedId = bedId)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
package com.syaroful.agrilinkvocpro.presentation.screen.detail
|
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.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -17,6 +24,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.getCurrentDate
|
||||||
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
|
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
|
||||||
import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DetailDhtContent
|
import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DetailDhtContent
|
||||||
|
|
@ -33,23 +44,27 @@ fun DetailScreen(
|
||||||
detailViewModel: DetailViewModel = koinViewModel(),
|
detailViewModel: DetailViewModel = koinViewModel(),
|
||||||
) {
|
) {
|
||||||
val date = remember { mutableStateOf(Date().toFormattedString()) }
|
val date = remember { mutableStateOf(Date().toFormattedString()) }
|
||||||
|
|
||||||
|
val id = if (sensorId == "dht") 1 else if (sensorId == "npk1") 2 else 3
|
||||||
LaunchedEffect(sensorId) {
|
LaunchedEffect(sensorId) {
|
||||||
if (sensorId == "dht") {
|
if (sensorId == "dht") {
|
||||||
detailViewModel.fetchDhtData(date = date.value, sensor = sensorId)
|
detailViewModel.fetchDhtData(date = date.value, sensor = sensorId)
|
||||||
|
detailViewModel.fetchSensorInformation(id)
|
||||||
} else {
|
} else {
|
||||||
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
|
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
|
||||||
|
detailViewModel.fetchSensorInformation(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val npkDataState by detailViewModel.npkDataState.collectAsState()
|
val npkDataState by detailViewModel.npkDataState.collectAsState()
|
||||||
val dhtDataState by detailViewModel.dhtDataState.collectAsState()
|
val dhtDataState by detailViewModel.dhtDataState.collectAsState()
|
||||||
|
val bedState by detailViewModel.bedState.collectAsState()
|
||||||
|
|
||||||
val currentData = detailViewModel.currentSensorData
|
val currentData = detailViewModel.currentSensorData
|
||||||
val isRefreshing = remember { mutableStateOf(false) }
|
val isRefreshing = remember { mutableStateOf(false) }
|
||||||
val currentDate = remember { mutableStateOf(getCurrentDate()) }
|
val currentDate = remember { mutableStateOf(getCurrentDate()) }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { DetailTopBar() }) { innerPadding ->
|
topBar = { DetailTopBar() }) { innerPadding ->
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
|
@ -61,8 +76,53 @@ fun DetailScreen(
|
||||||
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
|
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
|
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(
|
if (sensorId == "dht") DetailDhtContent(
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
viewModel = detailViewModel,
|
viewModel = detailViewModel,
|
||||||
dhtDataState = dhtDataState,
|
dhtDataState = dhtDataState,
|
||||||
currentData = currentData,
|
currentData = currentData,
|
||||||
|
|
@ -72,7 +132,6 @@ fun DetailScreen(
|
||||||
currentDate = currentDate
|
currentDate = currentDate
|
||||||
)
|
)
|
||||||
else DetailNpkContent(
|
else DetailNpkContent(
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
viewModel = detailViewModel,
|
viewModel = detailViewModel,
|
||||||
npkDataState = npkDataState,
|
npkDataState = npkDataState,
|
||||||
currentData = currentData,
|
currentData = currentData,
|
||||||
|
|
@ -81,6 +140,8 @@ fun DetailScreen(
|
||||||
date = date,
|
date = date,
|
||||||
currentDate = currentDate
|
currentDate = currentDate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.mapToUserFriendlyError
|
||||||
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
|
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
|
||||||
import com.syaroful.agrilinkvocpro.data.UserPreferences
|
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.DhtGraphicDataResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
|
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
|
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
|
||||||
|
import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse
|
||||||
import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository
|
import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -30,14 +32,75 @@ class DetailViewModel(
|
||||||
val currentSensorData: SensorDataResponse?
|
val currentSensorData: SensorDataResponse?
|
||||||
get() = sensorDataRepository.latestSensorData
|
get() = sensorDataRepository.latestSensorData
|
||||||
|
|
||||||
private val _npkDataState = MutableStateFlow<ResultState<NpkGraphicDataResponse>>(ResultState.Idle)
|
private val _npkDataState =
|
||||||
|
MutableStateFlow<ResultState<NpkGraphicDataResponse>>(ResultState.Idle)
|
||||||
val npkDataState: StateFlow<ResultState<NpkGraphicDataResponse>> = _npkDataState.asStateFlow()
|
val npkDataState: StateFlow<ResultState<NpkGraphicDataResponse>> = _npkDataState.asStateFlow()
|
||||||
|
|
||||||
private val _dhtDataState = MutableStateFlow<ResultState<DhtGraphicDataResponse>>(ResultState.Idle)
|
private val _dhtDataState =
|
||||||
|
MutableStateFlow<ResultState<DhtGraphicDataResponse>>(ResultState.Idle)
|
||||||
val dhtDataState: StateFlow<ResultState<DhtGraphicDataResponse>> = _dhtDataState.asStateFlow()
|
val dhtDataState: StateFlow<ResultState<DhtGraphicDataResponse>> = _dhtDataState.asStateFlow()
|
||||||
|
|
||||||
|
private val _sensorInformationState =
|
||||||
|
MutableStateFlow<ResultState<SensorInformationResponse>>(ResultState.Idle)
|
||||||
|
val sensorInformationState: StateFlow<ResultState<SensorInformationResponse>> =
|
||||||
|
_sensorInformationState.asStateFlow()
|
||||||
|
|
||||||
|
private val _bedState =
|
||||||
|
MutableStateFlow<ResultState<BedLocationResponse>>(ResultState.Idle)
|
||||||
|
val bedState: StateFlow<ResultState<BedLocationResponse>> =
|
||||||
|
_bedState.asStateFlow()
|
||||||
|
|
||||||
private val today = Date()
|
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(
|
fun fetchNpkData(
|
||||||
date: String = today.toFormattedString(),
|
date: String = today.toFormattedString(),
|
||||||
sensor: String,
|
sensor: String,
|
||||||
|
|
@ -62,7 +125,8 @@ class DetailViewModel(
|
||||||
_npkDataState.value = ResultState.Error("Data tidak ditemukan")
|
_npkDataState.value = ResultState.Error("Data tidak ditemukan")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_npkDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}")
|
_npkDataState.value =
|
||||||
|
ResultState.Error("Error: ${response.code()} - ${response.message()}")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val errorMessage = mapToUserFriendlyError(e)
|
val errorMessage = mapToUserFriendlyError(e)
|
||||||
|
|
@ -96,7 +160,8 @@ class DetailViewModel(
|
||||||
_dhtDataState.value = ResultState.Error("Data tidak ditemukan")
|
_dhtDataState.value = ResultState.Error("Data tidak ditemukan")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_dhtDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}")
|
_dhtDataState.value =
|
||||||
|
ResultState.Error("Error: ${response.code()} - ${response.message()}")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val errorMessage = mapToUserFriendlyError(e)
|
val errorMessage = mapToUserFriendlyError(e)
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -54,7 +52,6 @@ fun DetailDhtContent(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
) {
|
||||||
when (dhtDataState) {
|
when (dhtDataState) {
|
||||||
is ResultState.Loading -> {
|
is ResultState.Loading -> {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -57,7 +55,6 @@ fun DetailNpkContent(
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
) {
|
||||||
when (npkDataState) {
|
when (npkDataState) {
|
||||||
is ResultState.Loading -> {
|
is ResultState.Loading -> {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
|
@ -53,6 +55,7 @@ fun DynamicBottomSheet(
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowDropDown,
|
imageVector = Icons.Filled.ArrowDropDown,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f),
|
||||||
contentDescription = "Dropdown Arrow"
|
contentDescription = "Dropdown Arrow"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +65,11 @@ fun DynamicBottomSheet(
|
||||||
onDismissRequest = { setShowSheet(false) },
|
onDismissRequest = { setShowSheet(false) },
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
) {
|
) {
|
||||||
options.forEach { option ->
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
items(options.size) { index ->
|
||||||
|
val option = options[index]
|
||||||
Text(
|
Text(
|
||||||
text = option,
|
text = option,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -77,4 +84,5 @@ fun DynamicBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -324,22 +324,22 @@ fun DynamicFeatureSection(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
label = "Kontrol\nAktuator",
|
label = "Control\nActuator",
|
||||||
icon = painterResource(id = R.drawable.control_actuator_icon),
|
icon = painterResource(id = R.drawable.control_actuator_icon),
|
||||||
onClick = { onFeatureClick("control_feature") },
|
onClick = { onFeatureClick("control_feature") },
|
||||||
)
|
)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
label = "Resep\nPertumbuhan",
|
label = "Growth\nFormula",
|
||||||
icon = painterResource(id = R.drawable.growth_recipe_icon),
|
icon = painterResource(id = R.drawable.growth_recipe_icon),
|
||||||
onClick = { onFeatureClick("growth_recipe_feature") },
|
onClick = { onFeatureClick("growth_recipe_feature") },
|
||||||
)
|
)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
label = "Harga\nKomoditas",
|
label = "Commodity\nPrice",
|
||||||
icon = painterResource(id = R.drawable.commodity_price_prediction_icon),
|
icon = painterResource(id = R.drawable.commodity_price_prediction_icon),
|
||||||
onClick = { onFeatureClick("commodity_price_prediction_feature") },
|
onClick = { onFeatureClick("commodity_price_prediction_feature") },
|
||||||
)
|
)
|
||||||
MenuItemButton(
|
MenuItemButton(
|
||||||
label = "Deteksi\nPenyakit",
|
label = "Disease\nDetection",
|
||||||
icon = painterResource(id = R.drawable.plant_disease_detection_icon),
|
icon = painterResource(id = R.drawable.plant_disease_detection_icon),
|
||||||
onClick = { onFeatureClick("plant_disease_detection_feature") },
|
onClick = { onFeatureClick("plant_disease_detection_feature") },
|
||||||
)
|
)
|
||||||
|
|
@ -378,7 +378,7 @@ fun GreenHouseInformationSection(navController: NavController) {
|
||||||
modifier = Modifier.weight(1f)
|
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))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(text = "Green House Bumiaji", style = textTheme.bodyLarge)
|
Text(text = "Green House Bumiaji", style = textTheme.bodyLarge)
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -398,14 +398,3 @@ fun GreenHouseInformationSection(navController: NavController) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//@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 = {},
|
|
||||||
// )
|
|
||||||
//}
|
|
||||||
|
|
@ -24,9 +24,6 @@ class HomeViewModel(
|
||||||
private val _homeState = MutableStateFlow<ResultState<SensorDataResponse>>(ResultState.Idle)
|
private val _homeState = MutableStateFlow<ResultState<SensorDataResponse>>(ResultState.Idle)
|
||||||
val homeState: StateFlow<ResultState<SensorDataResponse>> = _homeState
|
val homeState: StateFlow<ResultState<SensorDataResponse>> = _homeState
|
||||||
|
|
||||||
// var currentDataSensor: SensorDataResponse? = null
|
|
||||||
// private set
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getGreenHouseData()
|
getGreenHouseData()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,11 +94,11 @@ fun RegisterScreen(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
text = "Register", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
text = "Halo! yuk masuk ke dalam akunmu",
|
text = "Halo! yuk daftarkan akun ke aplikasi",
|
||||||
style = textTheme.titleSmall.copy(color = DarkGrey),
|
style = textTheme.titleSmall.copy(color = DarkGrey),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<string name="control_feature_label">Kontrol Aktuator</string>
|
<string name="control_feature_label">Kontrol Aktuator</string>
|
||||||
<string name="play_store_icon_desc">Google Play Store</string>
|
<string name="play_store_icon_desc">Google Play Store</string>
|
||||||
<string name="download_module_title">Unduh Modul Fitur Dinamis</string>
|
<string name="download_module_title">Unduh Fitur</string>
|
||||||
<string name="download_module_message">anda perlu mengunduh modul fitur dinamis agar fitur ini dapat digunakan</string>
|
<string name="download_module_message">anda perlu mengunduh modul fitur dinamis agar fitur ini dapat digunakan</string>
|
||||||
<string name="download">Download</string>
|
<string name="download">Download</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,31 @@ import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
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 com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
|
||||||
|
import org.koin.core.context.loadKoinModules
|
||||||
|
import org.koin.core.context.unloadKoinModules
|
||||||
|
|
||||||
class PricePredictionActivity : ComponentActivity() {
|
class PricePredictionActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
loadKoinModules(listOf(appModule, viewModelModule, networkModule))
|
||||||
setContent {
|
setContent {
|
||||||
AgrilinkVocproTheme {
|
AgrilinkVocproTheme {
|
||||||
PricePredictionScreen()
|
val navController = rememberNavController()
|
||||||
|
SetupNavigation(navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
unloadKoinModules(listOf(appModule, viewModelModule, networkModule))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.constant
|
||||||
|
|
||||||
|
class AppConstant {
|
||||||
|
val locationOption: List<String> = 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<String> = 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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
@ -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<String>,
|
||||||
|
locationOptions: List<String>,
|
||||||
|
selectedCommodity: MutableState<String>,
|
||||||
|
selectedLocation: MutableState<String>,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -138,7 +138,7 @@ fun PriceChart(
|
||||||
"Harga",
|
"Harga",
|
||||||
spacing / 2f,
|
spacing / 2f,
|
||||||
labelPadding - 100,
|
labelPadding - 100,
|
||||||
textPaint.apply { color = android.graphics.Color.GREEN }
|
textPaint
|
||||||
)
|
)
|
||||||
|
|
||||||
horizontalValue?.let { value ->
|
horizontalValue?.let { value ->
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,69 @@
|
||||||
package com.syaroful.agrilinkvocpro.control_feature.data.model
|
package com.syaroful.agrilinkvocpro.control_feature.data.model
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class ActuatorHistoryResponse(
|
data class ActuatorHistoryResponse(
|
||||||
val `data`: List<ActuatorLog?>?,
|
@SerialName("data")
|
||||||
|
val `data`: List<ActuatorHistoryData?>?,
|
||||||
|
@SerialName("lastPage")
|
||||||
val lastPage: Int?,
|
val lastPage: Int?,
|
||||||
|
@SerialName("message")
|
||||||
val message: String?,
|
val message: String?,
|
||||||
|
@SerialName("nextPage")
|
||||||
val nextPage: Any?,
|
val nextPage: Any?,
|
||||||
|
@SerialName("page")
|
||||||
val page: Int?,
|
val page: Int?,
|
||||||
|
@SerialName("perPage")
|
||||||
val perPage: Int?,
|
val perPage: Int?,
|
||||||
|
@SerialName("previousPage")
|
||||||
val previousPage: Any?,
|
val previousPage: Any?,
|
||||||
|
@SerialName("statusCode")
|
||||||
val statusCode: Int?,
|
val statusCode: Int?,
|
||||||
|
@SerialName("total")
|
||||||
val total: Int?
|
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?
|
||||||
|
)
|
||||||
|
|
@ -10,6 +10,7 @@ import retrofit2.http.Header
|
||||||
import retrofit2.http.Multipart
|
import retrofit2.http.Multipart
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Part
|
import retrofit2.http.Part
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface ControlService {
|
interface ControlService {
|
||||||
// get all actuator status
|
// get all actuator status
|
||||||
|
|
@ -43,8 +44,17 @@ interface ControlService {
|
||||||
): Response<ControlLogResponse>
|
): Response<ControlLogResponse>
|
||||||
|
|
||||||
// get controls log
|
// get controls log
|
||||||
@GET("api//actuator-control-logs")
|
@GET("api/actuator-control-logs")
|
||||||
suspend fun getActuatorsControlLog(
|
suspend fun getActuatorsControlLog(
|
||||||
@Header("Authorization") authHeader: String,
|
@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<ActuatorHistoryResponse>
|
): Response<ActuatorHistoryResponse>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -37,8 +37,29 @@ class ControlRepository(
|
||||||
return controlService.getActuatorStatus(authHeader)
|
return controlService.getActuatorStatus(authHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getActuatorHistory(authHeader: String): Response<ActuatorHistoryResponse> {
|
suspend fun getActuatorHistory(
|
||||||
return controlService.getActuatorsControlLog(authHeader)
|
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<ActuatorHistoryResponse> {
|
||||||
|
return controlService.getActuatorsControlLog(
|
||||||
|
authHeader = authHeader,
|
||||||
|
actuatorId = actuatorId,
|
||||||
|
status = status,
|
||||||
|
startDate = startDate,
|
||||||
|
endDate = endDate,
|
||||||
|
page = page,
|
||||||
|
limit = limit,
|
||||||
|
sortField = sortField,
|
||||||
|
sortDirection = sortDirection
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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 = {}
|
|
||||||
// )
|
|
||||||
//}
|
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
package com.syaroful.agrilinkvocpro.control_feature.presentation.control
|
package com.syaroful.agrilinkvocpro.control_feature.presentation.control
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.Check
|
||||||
import androidx.compose.material.icons.filled.Clear
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
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.ControlCard
|
||||||
import com.syaroful.agrilinkvocpro.control_feature.core.components.CustomSnackBar
|
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.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.core.state.ControlState
|
||||||
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType
|
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType
|
||||||
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
|
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
|
||||||
|
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -88,14 +93,6 @@ fun ControlActuatorScreen(
|
||||||
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
|
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
navController.navigate("control_history")
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Filled.Refresh, contentDescription = "History")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
|
|
@ -115,9 +112,11 @@ fun ControlActuatorScreen(
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
FeatureBanner()
|
||||||
|
HistoryButton {
|
||||||
|
navController.navigate("control_history")
|
||||||
|
}
|
||||||
Text("Daftar Actuator", style = MaterialTheme.typography.titleMedium)
|
Text("Daftar Actuator", style = MaterialTheme.typography.titleMedium)
|
||||||
Spacer(modifier = Modifier.height(1.dp))
|
|
||||||
|
|
||||||
when (allActuators) {
|
when (allActuators) {
|
||||||
is ControlState.Loading -> {
|
is ControlState.Loading -> {
|
||||||
isRefreshing.value = false
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
package com.syaroful.agrilinkvocpro.control_feature.presentation.history
|
package com.syaroful.agrilinkvocpro.control_feature.presentation.history
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
|
@ -19,15 +25,19 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
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.state.ControlState
|
||||||
import com.syaroful.agrilinkvocpro.control_feature.core.utils.getRelativeTime
|
import com.syaroful.agrilinkvocpro.control_feature.core.utils.getRelativeTime
|
||||||
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryResponse
|
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryResponse
|
||||||
|
|
@ -53,7 +63,10 @@ fun ControlHistoryScreen(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Riwayat Kontrol Aktuator", style = MaterialTheme.typography.titleMedium)
|
Text(
|
||||||
|
"Riwayat Kontrol Aktuator",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|
@ -68,25 +81,51 @@ fun ControlHistoryScreen(
|
||||||
|
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
isRefreshing = isRefreshing.value,
|
isRefreshing = isRefreshing.value,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
isRefreshing.value = true
|
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(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
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 -> {
|
is ControlState.Error -> {
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
item {
|
item {
|
||||||
DefaultErrorComponent(
|
DefaultErrorComponent(
|
||||||
message = (historyState as ControlState.Error).message,
|
message = state.message,
|
||||||
label = "Oops!",
|
label = "Oops!",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -94,52 +133,95 @@ fun ControlHistoryScreen(
|
||||||
|
|
||||||
is ControlState.Success -> {
|
is ControlState.Success -> {
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
val data =
|
if (historyList.isEmpty()) {
|
||||||
(historyState as ControlState.Success<ActuatorHistoryResponse>).data?.data
|
item {
|
||||||
if (!data.isNullOrEmpty()) {
|
DefaultErrorComponent(
|
||||||
val reversedData = data.reversed()
|
label = "Data kosong",
|
||||||
items(reversedData) { historyData ->
|
message = "Tidak ada riwayat kontrol yang tersedia"
|
||||||
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 {
|
} else {
|
||||||
item {
|
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<ActuatorHistoryResponse>)?.data?.let { response ->
|
||||||
|
Text("Total: ${response.total}", style = MaterialTheme.typography.titleSmall)
|
||||||
|
} ?: run {
|
||||||
|
Text("Total: -", style = MaterialTheme.typography.titleSmall)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ControlState.Loading -> {
|
// IconButton(
|
||||||
item {
|
// modifier = Modifier
|
||||||
CircularProgressIndicator(
|
// .background(
|
||||||
color = MainGreen
|
// 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)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
// Infinite scroll loading indicator
|
||||||
|
if (!controlHistoryViewModel.isLastPage) {
|
||||||
|
item {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MainGreen,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
package com.syaroful.agrilinkvocpro.control_feature.presentation.history
|
package com.syaroful.agrilinkvocpro.control_feature.presentation.history
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState
|
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.model.ActuatorHistoryResponse
|
||||||
import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository
|
import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository
|
||||||
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
|
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
|
||||||
import com.syaroful.agrilinkvocpro.data.UserPreferences
|
import com.syaroful.agrilinkvocpro.data.UserPreferences
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -22,35 +23,88 @@ class ControlHistoryViewModel(
|
||||||
|
|
||||||
private val _historyState =
|
private val _historyState =
|
||||||
MutableStateFlow<ControlState<ActuatorHistoryResponse>>(ControlState.Idle)
|
MutableStateFlow<ControlState<ActuatorHistoryResponse>>(ControlState.Idle)
|
||||||
val historyState: MutableStateFlow<ControlState<ActuatorHistoryResponse>> = _historyState
|
val historyState: StateFlow<ControlState<ActuatorHistoryResponse>> = _historyState
|
||||||
|
|
||||||
|
private val _historyList = mutableListOf<ActuatorHistoryData>()
|
||||||
|
val historyList: List<ActuatorHistoryData> get() = _historyList
|
||||||
|
|
||||||
|
private var currentPage = 1
|
||||||
|
private val limitPerPage = 10
|
||||||
|
private var isLoadingMore = false
|
||||||
|
var isLastPage = false
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getActuatorHistory()
|
loadInitialHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActuatorHistory() {
|
fun loadInitialHistory() {
|
||||||
_historyState.value = ControlState.Loading
|
_historyState.value = ControlState.Loading
|
||||||
|
|
||||||
|
// Reset pagination state
|
||||||
|
currentPage = 1
|
||||||
|
isLastPage = false
|
||||||
|
isLoadingMore = false
|
||||||
|
_historyList.clear()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val token = userPreferences.tokenFlow.first()
|
val token = userPreferences.tokenFlow.first()
|
||||||
val authHeader = "Bearer $token"
|
val authHeader = "Bearer $token"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
delay(500L)
|
delay(500L)
|
||||||
val response = repository.getActuatorHistory(authHeader = authHeader)
|
val response = repository.getActuatorHistory(
|
||||||
|
authHeader = authHeader,
|
||||||
|
page = currentPage,
|
||||||
|
limit = limitPerPage
|
||||||
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val data = response.body()
|
val body = response.body()
|
||||||
_historyState.value = ControlState.Success(data)
|
_historyList.clear()
|
||||||
Log.d(TAG, "Successfully get Actuator History ${response.body()}")
|
val safeData = body?.data?.filterNotNull() ?: emptyList()
|
||||||
|
_historyList.addAll(safeData)
|
||||||
|
isLastPage = currentPage >= (body?.lastPage ?: 1)
|
||||||
|
_historyState.value = ControlState.Success(body)
|
||||||
} else {
|
} else {
|
||||||
val errorBody = response.errorBody()?.string()
|
_historyState.value = ControlState.Error(response.message())
|
||||||
val errorMessage = errorBody ?: "Error ${response.code()}"
|
|
||||||
_historyState.value = ControlState.Error(errorMessage)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val errorMessage = mapToUserFriendlyError(e)
|
_historyState.value = ControlState.Error(mapToUserFriendlyError(e))
|
||||||
_historyState.value = ControlState.Error(errorMessage)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ fun GrowthRecipeFeatureBanner(){
|
||||||
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
|
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Lihat rekomendasi perawatan optimal untuk setiap jenis tanaman",
|
"Lihat rekomendasi perawatan optimal",
|
||||||
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
|
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,13 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MenuDefaults
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
|
@ -28,6 +31,7 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
@ -52,21 +56,25 @@ fun GrowthRecipeScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: GrowthRecipeViewModel = koinViewModel()
|
viewModel: GrowthRecipeViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry")
|
val sensorIdOptions: Map<String, String> = mapOf("Bed 1" to "npk1", "Bed 2" to "npk2")
|
||||||
val sensorOptions = listOf("Nitrogen", "Pospor", "Kalium")
|
val sensorOptions = listOf("Nitrogen", "Pospor", "Kalium")
|
||||||
|
|
||||||
|
|
||||||
val selectedSensor = remember { mutableStateOf("Nitrogen") }
|
val selectedSensor = remember { mutableStateOf("Nitrogen") }
|
||||||
|
val selectedSensorId = remember { mutableStateOf(sensorIdOptions.values.first()) }
|
||||||
val graphicState by viewModel.getGraphicState.collectAsState()
|
val graphicState by viewModel.getGraphicState.collectAsState()
|
||||||
val isRefreshing = remember { mutableStateOf(false) }
|
val isRefreshing = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.getGraphicData("npk1")
|
viewModel.getGraphicData(selectedSensorId.value)
|
||||||
}
|
}
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing.value,
|
isRefreshing = isRefreshing.value,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
isRefreshing.value = true
|
isRefreshing.value = true
|
||||||
viewModel.getGraphicData("npk1")
|
viewModel.getGraphicData(selectedSensorId.value)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -104,10 +112,20 @@ fun GrowthRecipeScreen(
|
||||||
},
|
},
|
||||||
title = "✨ Lihat saran perawatan"
|
title = "✨ Lihat saran perawatan"
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
){
|
||||||
Text(
|
Text(
|
||||||
"🪴 Grafik nutrisi dalam 10 hari",
|
"🪴 Grafik nutrisi dalam 10 hari",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
selectedSensorId.value,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|
@ -119,7 +137,7 @@ fun GrowthRecipeScreen(
|
||||||
color = MainGreen.copy(alpha = 0.1f)
|
color = MainGreen.copy(alpha = 0.1f)
|
||||||
)
|
)
|
||||||
.size(40.dp),
|
.size(40.dp),
|
||||||
onClick = { },
|
onClick = {expanded = !expanded},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
|
|
@ -127,6 +145,27 @@ fun GrowthRecipeScreen(
|
||||||
tint = MainGreen,
|
tint = MainGreen,
|
||||||
contentDescription = "Choose bet"
|
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))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
DynamicBottomSheet(
|
DynamicBottomSheet(
|
||||||
|
|
@ -163,7 +202,7 @@ fun GrowthRecipeScreen(
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
val dataList =
|
val dataList =
|
||||||
(graphicState as ResultState.Success<NpkGraphicDayResponse>).data?.data?.get(
|
(graphicState as ResultState.Success<NpkGraphicDayResponse>).data?.data?.get(
|
||||||
"npk1"
|
selectedSensorId.value
|
||||||
).orEmpty()
|
).orEmpty()
|
||||||
|
|
||||||
val days = dataList.mapNotNull { it.day?.toInt() }
|
val days = dataList.mapNotNull { it.day?.toInt() }
|
||||||
|
|
@ -191,7 +230,7 @@ fun GrowthRecipeScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"🌱 Saran Nutrisi Optimal",
|
"🌱 Saran Perbandingan Nutrisi Optimal",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -199,26 +238,34 @@ fun GrowthRecipeScreen(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("Nutrisi")
|
Text("Nutrisi")
|
||||||
Text("N")
|
Text("N")
|
||||||
Text("P")
|
Text("P")
|
||||||
Text("K")
|
Text("K")
|
||||||
}
|
}
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("Vegetatif")
|
Text("Vegetatif")
|
||||||
Text("30 ppm")
|
NutrientStandartBox("30")
|
||||||
Text("40 ppm")
|
NutrientStandartBox("10")
|
||||||
Text("50 ppm")
|
NutrientStandartBox("10")
|
||||||
}
|
}
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("Generatif")
|
Text("Generatif")
|
||||||
Text("50 ppm")
|
NutrientStandartBox("10")
|
||||||
Text("60 ppm")
|
NutrientStandartBox("20")
|
||||||
Text("70 ppm")
|
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) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ class GrowthRecipeViewModel(
|
||||||
sensor = sensor,
|
sensor = sensor,
|
||||||
)
|
)
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
|
Log.d(TAG, "Response: ${response.body()}")
|
||||||
response.body()?.let { body ->
|
response.body()?.let { body ->
|
||||||
_getGraphicState.value = ResultState.Success(body)
|
_getGraphicState.value = ResultState.Success(body)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
|
|
|
||||||
|
|
@ -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.core.extention.toBitmap
|
||||||
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
|
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.plant_disease_detection_feature.presentation.detail.TextCardComponent
|
||||||
|
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -121,13 +122,13 @@ fun DetailHistoryScreen(
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
color = Color.Red,
|
color = if (plantDiagnosis?.diagnosis == "Sehat") MainGreen else Color.Red,
|
||||||
shape = RoundedCornerShape(4.dp)
|
shape = RoundedCornerShape(4.dp)
|
||||||
)
|
)
|
||||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Terkena Penyakit",
|
text = if (plantDiagnosis?.diagnosis == "Sehat") "Sehat" else "Terkena Penyakit",
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
style = MaterialTheme.typography.labelMedium
|
style = MaterialTheme.typography.labelMedium
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 161 KiB |
Loading…
Reference in New Issue
Block a user