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