Compare commits
No commits in common. "4dab797cb9dad0508785310f7b19525a76806dd4" and "30c751be7a508fdd16ebab3bb0acde41b9824fe2" have entirely different histories.
4dab797cb9
...
30c751be7a
|
|
@ -13,7 +13,7 @@ android {
|
|||
applicationId = "com.syaroful.agrilinkvocpro"
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 15
|
||||
versionCode = 13
|
||||
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?"
|
||||
"growth_recipe_feature" -> "Apakah Anda ingin mendownload fitur Formula Pertumbuhan Optimal?"
|
||||
"commodity_price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?"
|
||||
"recipe_feature" -> "Apakah Anda ingin mendownload fitur Resep Pertumbuhan?"
|
||||
"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 = "commodity_price_prediction_feature",
|
||||
moduleName = "price_prediction_feature",
|
||||
onClickConfirm = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
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?
|
||||
)
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
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,18 +1,15 @@
|
|||
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
|
||||
|
||||
|
||||
|
|
@ -64,16 +61,4 @@ 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,10 +1,8 @@
|
|||
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
|
||||
|
||||
|
|
@ -40,19 +38,4 @@ 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,16 +1,9 @@
|
|||
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
|
||||
|
|
@ -24,10 +17,6 @@ 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
|
||||
|
|
@ -44,27 +33,23 @@ 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(
|
||||
|
|
@ -76,53 +61,8 @@ fun DetailScreen(
|
|||
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(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
viewModel = detailViewModel,
|
||||
dhtDataState = dhtDataState,
|
||||
currentData = currentData,
|
||||
|
|
@ -132,6 +72,7 @@ fun DetailScreen(
|
|||
currentDate = currentDate
|
||||
)
|
||||
else DetailNpkContent(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
viewModel = detailViewModel,
|
||||
npkDataState = npkDataState,
|
||||
currentData = currentData,
|
||||
|
|
@ -140,8 +81,6 @@ fun DetailScreen(
|
|||
date = date,
|
||||
currentDate = currentDate
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,9 @@ 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
|
||||
|
|
@ -32,75 +30,14 @@ 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,
|
||||
|
|
@ -125,8 +62,7 @@ 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)
|
||||
|
|
@ -160,8 +96,7 @@ 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,6 +8,8 @@ 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
|
||||
|
|
@ -52,6 +54,7 @@ fun DetailDhtContent(
|
|||
Column(
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (dhtDataState) {
|
||||
is ResultState.Loading -> {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ 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
|
||||
|
|
@ -55,6 +57,7 @@ fun DetailNpkContent(
|
|||
Column(
|
||||
modifier = modifier
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (npkDataState) {
|
||||
is ResultState.Loading -> {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ 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
|
||||
|
|
@ -55,7 +53,6 @@ fun DynamicBottomSheet(
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowDropDown,
|
||||
tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f),
|
||||
contentDescription = "Dropdown Arrow"
|
||||
)
|
||||
}
|
||||
|
|
@ -65,11 +62,7 @@ fun DynamicBottomSheet(
|
|||
onDismissRequest = { setShowSheet(false) },
|
||||
sheetState = sheetState
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
items(options.size) { index ->
|
||||
val option = options[index]
|
||||
options.forEach { option ->
|
||||
Text(
|
||||
text = option,
|
||||
modifier = Modifier
|
||||
|
|
@ -84,5 +77,4 @@ fun DynamicBottomSheet(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -324,22 +324,22 @@ fun DynamicFeatureSection(
|
|||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
MenuItemButton(
|
||||
label = "Control\nActuator",
|
||||
label = "Kontrol\nAktuator",
|
||||
icon = painterResource(id = R.drawable.control_actuator_icon),
|
||||
onClick = { onFeatureClick("control_feature") },
|
||||
)
|
||||
MenuItemButton(
|
||||
label = "Growth\nFormula",
|
||||
label = "Resep\nPertumbuhan",
|
||||
icon = painterResource(id = R.drawable.growth_recipe_icon),
|
||||
onClick = { onFeatureClick("growth_recipe_feature") },
|
||||
)
|
||||
MenuItemButton(
|
||||
label = "Commodity\nPrice",
|
||||
label = "Harga\nKomoditas",
|
||||
icon = painterResource(id = R.drawable.commodity_price_prediction_icon),
|
||||
onClick = { onFeatureClick("commodity_price_prediction_feature") },
|
||||
)
|
||||
MenuItemButton(
|
||||
label = "Disease\nDetection",
|
||||
label = "Deteksi\nPenyakit",
|
||||
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 Commodities", color = MainGreen, style = textTheme.bodyMedium)
|
||||
Text(text = "2 Komoditas", color = MainGreen, style = textTheme.bodyMedium)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(text = "Green House Bumiaji", style = textTheme.bodyLarge)
|
||||
Text(
|
||||
|
|
@ -398,3 +398,14 @@ 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,6 +24,9 @@ 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 = "Register", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
||||
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Halo! yuk daftarkan akun ke aplikasi",
|
||||
text = "Halo! yuk masuk ke dalam akunmu",
|
||||
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 Fitur</string>
|
||||
<string name="download_module_title">Unduh Modul Fitur Dinamis</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,31 +4,17 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
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.commodity_price_prediction_feature.presentation.prediction.PricePredictionScreen
|
||||
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 {
|
||||
val navController = rememberNavController()
|
||||
SetupNavigation(navController)
|
||||
PricePredictionScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unloadKoinModules(listOf(appModule, viewModelModule, networkModule))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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 com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.R
|
||||
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
|
||||
|
||||
|
||||
@Composable
|
||||
fun FeatureBanner(){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MainGreen, shape = RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth(0.6f)) {
|
||||
Text(
|
||||
"Prediksi Harga",
|
||||
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
|
||||
)
|
||||
Text(
|
||||
"Agar kamu bisa memilih komoditas yang tepat",
|
||||
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
|
||||
)
|
||||
}
|
||||
Image(
|
||||
modifier = Modifier.fillMaxWidth(0.8f),
|
||||
painter = painterResource(id = R.drawable.commodity_img_banner),
|
||||
contentDescription = "Plant Image",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
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",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils
|
||||
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
fun Number.toRupiahFormat(): String {
|
||||
val localeID = Locale("in", "ID")
|
||||
val numberFormat = NumberFormat.getCurrencyInstance(localeID)
|
||||
return numberFormat.format(this.toLong()).replace(",00", "")
|
||||
}
|
||||
|
||||
fun percentageChange(from: Number, to: Number): Double {
|
||||
if (from.toDouble() == 0.0 || to.toDouble() == 0.0) return 0.0
|
||||
val change = ((to.toDouble() - from.toDouble()) / from.toDouble()) * 100
|
||||
return change
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model
|
||||
|
||||
data class CommodityPredictionResponse(
|
||||
val harga: Double? = null,
|
||||
val nama_komoditas: String,
|
||||
val tanggal: String? = null,
|
||||
val val_1: Double? = null,
|
||||
val val_7: Double? = null,
|
||||
val val_30: Double? = null,
|
||||
val val_90: Double? = null
|
||||
)
|
||||
|
||||
//class CommodityPredictionDeserializer : JsonDeserializer<CommodityPredictionResponse> {
|
||||
// override fun deserialize(
|
||||
// json: JsonElement,
|
||||
// typeOfT: Type,
|
||||
// context: JsonDeserializationContext
|
||||
// ): CommodityPredictionResponse {
|
||||
// val jsonObject = json.asJsonObject
|
||||
// val name = jsonObject.get("nama_komoditas").asString
|
||||
// val date = jsonObject.get("tanggal").asString
|
||||
//
|
||||
// val predictions = mutableMapOf<String, Double>()
|
||||
// jsonObject.entrySet().forEach { entry ->
|
||||
// if (entry.key.startsWith("val_")) {
|
||||
// predictions[entry.key] = entry.value.asDouble
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return CommodityPredictionResponse(name, date, predictions)
|
||||
// }
|
||||
//}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model
|
||||
|
||||
data class CommodityPriceResponseItem(
|
||||
val harga: Int?,
|
||||
val kab_kota: String?,
|
||||
val komoditas_nama: String?,
|
||||
val pasar: String?,
|
||||
val satuan: String?,
|
||||
val tanggal: String?
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.network
|
||||
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
|
||||
import retrofit2.http.GET
|
||||
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface CommodityApiService {
|
||||
@GET("api/periodic-data")
|
||||
suspend fun getCommodityPredictions(
|
||||
@Query("tanggal") date: String,
|
||||
): List<CommodityPredictionResponse>
|
||||
|
||||
@GET("api/harga")
|
||||
suspend fun getCommodityPrice(
|
||||
@Query("komoditas") komoditas: String,
|
||||
@Query("pasar") pasar: String,
|
||||
@Query("tanggal") tanggal: String,
|
||||
@Query("kab_kota") kabKota: String
|
||||
): List<CommodityPriceResponseItem>
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository
|
||||
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.network.CommodityApiService
|
||||
|
||||
class CommodityRepository(private val apiService: CommodityApiService) {
|
||||
suspend fun fetchCommodityPredictions(date: String): List<CommodityPredictionResponse> {
|
||||
return apiService.getCommodityPredictions(date)
|
||||
}
|
||||
|
||||
suspend fun fetchCommodityPrice(commodityName: String, marketName: String, date: String, city: String): List<CommodityPriceResponseItem> {
|
||||
return apiService.getCommodityPrice(komoditas = commodityName, pasar = marketName, tanggal = date, kabKota = city)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di
|
||||
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository.CommodityRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appModule = module {
|
||||
single { CommodityRepository(get()) }
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di
|
||||
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.network.CommodityApiService
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
val networkModule = module {
|
||||
|
||||
single {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(2, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
single {
|
||||
Retrofit.Builder()
|
||||
.baseUrl("http://labai.polinema.ac.id:50/")
|
||||
.client(get())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
single { get<Retrofit>().create(CommodityApiService::class.java) }
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di
|
||||
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.CommodityViewModel
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PredictionViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val viewModelModule = module {
|
||||
viewModel { PredictionViewModel(get()) }
|
||||
viewModel { CommodityViewModel(get()) }
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.CommodityPriceScreen
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PredictionScreen
|
||||
|
||||
@Composable
|
||||
fun SetupNavigation(navController: NavHostController) {
|
||||
NavHost(navController = navController, startDestination = "commodity_screen") {
|
||||
composable("commodity_screen") {
|
||||
CommodityPriceScreen(navController = navController)
|
||||
}
|
||||
composable("prediction_screen") {
|
||||
PredictionScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
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),
|
||||
// )
|
||||
// }
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository.CommodityRepository
|
||||
import com.syaroful.agrilinkvocpro.core.utils.ResultState
|
||||
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
private const val TAG = "CommodityViewModel"
|
||||
|
||||
class CommodityViewModel(
|
||||
private val repository: CommodityRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<ResultState<List<CommodityPriceResponseItem>>>(
|
||||
ResultState.Idle
|
||||
)
|
||||
val state: StateFlow<ResultState<List<CommodityPriceResponseItem>>> = _state
|
||||
|
||||
fun loadCommodityPrice(
|
||||
commodityName: String,
|
||||
market: String,
|
||||
date: String,
|
||||
city: String,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_state.value = ResultState.Loading
|
||||
try {
|
||||
val data = repository.fetchCommodityPrice(
|
||||
commodityName = commodityName,
|
||||
marketName = market,
|
||||
date = date,
|
||||
city = city
|
||||
)
|
||||
Log.d(TAG, "loadCommodityPrice: $commodityName, $market, $date, $city")
|
||||
_state.value = ResultState.Success(data)
|
||||
Log.d(TAG, "loadCommodityPrice: $data")
|
||||
} catch (e: Exception) {
|
||||
val errorMessage = mapToUserFriendlyError(e)
|
||||
_state.value = ResultState.Error(errorMessage)
|
||||
Log.e(TAG, "loadCommodityPrice: $errorMessage", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.commodity.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils.toRupiahFormat
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPriceResponseItem
|
||||
|
||||
@Composable
|
||||
fun CommodityListItem(item: CommodityPriceResponseItem) {
|
||||
ListItem(
|
||||
modifier = Modifier.border(
|
||||
shape = RoundedCornerShape(8.dp), width = 1.dp, color =
|
||||
MaterialTheme.colorScheme.surfaceContainer
|
||||
),
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = "${item.kab_kota} - ${item.pasar}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = item.komoditas_nama.toString(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.alpha(0.5f)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Box(
|
||||
modifier = Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = RoundedCornerShape(8.dp)).padding(vertical = 4.dp, horizontal = 8.dp)
|
||||
)
|
||||
{
|
||||
Text(
|
||||
text = "${item.harga?.toRupiahFormat()}",
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction
|
||||
|
||||
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
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.runtime.setValue
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.R
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils.percentageChange
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.core.utils.toRupiahFormat
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
|
||||
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 com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PredictionScreen(
|
||||
navController: NavHostController,
|
||||
viewModel: PredictionViewModel = koinViewModel()
|
||||
) {
|
||||
val resultState by viewModel.commodityData.collectAsState()
|
||||
|
||||
val calendar = remember { Calendar.getInstance().apply { add(Calendar.DATE, -1) } }
|
||||
val today = Date().toFormattedString()
|
||||
val dateYesterday = calendar.time.toFormattedString()
|
||||
val isRefreshing = remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (resultState == ResultState.Idle) {
|
||||
viewModel.loadCommodityPredictions(today)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"Price Prediction",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing.value,
|
||||
onRefresh = {
|
||||
viewModel.loadCommodityPredictions(today)
|
||||
}
|
||||
) {
|
||||
when (resultState) {
|
||||
is ResultState.Idle -> {
|
||||
|
||||
}
|
||||
|
||||
is ResultState.Loading -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Loader()
|
||||
}
|
||||
}
|
||||
|
||||
is ResultState.Success -> {
|
||||
isRefreshing.value = false
|
||||
val data =
|
||||
(resultState as ResultState.Success<List<CommodityPredictionResponse>>).data
|
||||
|
||||
if (!data.isNullOrEmpty()) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var selectedCommodity by remember {
|
||||
mutableStateOf<CommodityPredictionResponse?>(
|
||||
data[0]
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Box {
|
||||
OutlinedButton(
|
||||
onClick = { expanded = !expanded },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = BorderStroke(
|
||||
color = MainGreen.copy(alpha = 0.5f),
|
||||
width = 1.dp
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = selectedCommodity?.nama_komoditas
|
||||
?: "Pilih Komoditas",
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Image(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
alpha = 0.5f,
|
||||
contentDescription = "dropdown"
|
||||
)
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
data.forEach { item ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
item.nama_komoditas,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
colors = MenuDefaults.itemColors(MaterialTheme.colorScheme.onBackground),
|
||||
onClick = {
|
||||
selectedCommodity = item
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
selectedCommodity?.let { commodity ->
|
||||
CommodityPredictionCard(commodity)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Data kosong.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ResultState.Error -> {
|
||||
isRefreshing.value = false
|
||||
val message = (resultState as ResultState.Error).message
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
DefaultErrorComponent(
|
||||
label = "Oops!",
|
||||
message = message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PredictedPriceListItem(
|
||||
periode: String,
|
||||
harga: String,
|
||||
percentage: Double,
|
||||
) {
|
||||
ListItem(
|
||||
supportingContent = {
|
||||
Text(
|
||||
harga,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
periode,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.alpha(0.5f)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
val graphic: String = if (percentage >= 0) "↗" else "↘"
|
||||
Text(
|
||||
"$graphic ${"%.2f".format(percentage)}%",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = if (percentage >= 0) MainGreen else Color.Red
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CommodityPredictionCard(item: CommodityPredictionResponse) {
|
||||
val hargaHariIni = item.harga?.toInt() ?: 0
|
||||
val hargaBesok = item.val_1?.toInt() ?: 0
|
||||
val hargaMinggu = item.val_7?.toInt() ?: 0
|
||||
val hargaBulan = item.val_30?.toInt() ?: 0
|
||||
val harga3Bulan = item.val_90?.toInt() ?: 0
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
"Komoditas",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.alpha(0.5f)
|
||||
)
|
||||
Text(item.nama_komoditas, style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
"Harga Hari Ini",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
"${item.harga?.toRupiahFormat()}",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(
|
||||
when (item.nama_komoditas) {
|
||||
"Tomat Merah" -> R.drawable.tomat_merah
|
||||
"Cabe Merah Besar" -> R.drawable.cabe_merah_besar
|
||||
"Cabe Rawit Merah" -> R.drawable.cabe_rawit
|
||||
"Bawang Merah" -> R.drawable.bawang_merah
|
||||
"Bawang Putih Sinco/Honan" -> R.drawable.bawang_putih
|
||||
else -> R.drawable.bawang_merah
|
||||
}
|
||||
),
|
||||
contentDescription = "commodity",
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
PredictedPriceListItem(
|
||||
"Besok",
|
||||
hargaBesok.toRupiahFormat(),
|
||||
percentageChange(hargaHariIni, hargaBesok)
|
||||
)
|
||||
PredictedPriceListItem(
|
||||
"Minggu Depan",
|
||||
hargaMinggu.toRupiahFormat(),
|
||||
percentageChange(hargaHariIni, hargaMinggu)
|
||||
)
|
||||
PredictedPriceListItem(
|
||||
"Bulan Depan",
|
||||
hargaBulan.toRupiahFormat(),
|
||||
percentageChange(hargaHariIni, hargaBulan)
|
||||
)
|
||||
PredictedPriceListItem(
|
||||
"3 Bulan ke Depan",
|
||||
harga3Bulan.toRupiahFormat(),
|
||||
percentageChange(hargaHariIni, harga3Bulan)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.model.CommodityPredictionResponse
|
||||
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.data.repository.CommodityRepository
|
||||
import com.syaroful.agrilinkvocpro.core.utils.ResultState
|
||||
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
private const val TAG = "PredictionViewModel"
|
||||
class PredictionViewModel(
|
||||
private val repository: CommodityRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _commodityData = MutableStateFlow<ResultState<List<CommodityPredictionResponse>>>(
|
||||
ResultState.Idle
|
||||
)
|
||||
val commodityData: StateFlow<ResultState<List<CommodityPredictionResponse>>> = _commodityData
|
||||
|
||||
|
||||
fun loadCommodityPredictions(date: String) {
|
||||
viewModelScope.launch {
|
||||
_commodityData.value = ResultState.Loading
|
||||
try {
|
||||
val data = repository.fetchCommodityPredictions(date = date)
|
||||
_commodityData.value = ResultState.Success(data)
|
||||
Log.d(TAG, "loadCommodityPredictions: $data")
|
||||
} catch (e: Exception) {
|
||||
val errorMessage = mapToUserFriendlyError(e)
|
||||
_commodityData.value = ResultState.Error(errorMessage)
|
||||
Log.e(TAG, "loadCommodityPredictions: $errorMessage")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,7 +138,7 @@ fun PriceChart(
|
|||
"Harga",
|
||||
spacing / 2f,
|
||||
labelPadding - 100,
|
||||
textPaint
|
||||
textPaint.apply { color = android.graphics.Color.GREEN }
|
||||
)
|
||||
|
||||
horizontalValue?.let { value ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 37 KiB |
|
|
@ -1,53 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.control_feature.core.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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 com.syaroful.agrilinkvocpro.control_feature.R
|
||||
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
|
||||
|
||||
@Composable
|
||||
fun FeatureBanner(){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MainGreen, shape = RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth(0.6f)) {
|
||||
Text(
|
||||
"Kontrol Penyiraman",
|
||||
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
|
||||
)
|
||||
Text(
|
||||
"lakukan penyiraman hanya dengan sekali klik dari jarak jauh",
|
||||
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
|
||||
)
|
||||
}
|
||||
Image(
|
||||
modifier = Modifier.fillMaxWidth(0.8f),
|
||||
painter = painterResource(id = R.drawable.watering_img_banner),
|
||||
contentDescription = "Plant Image",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +1,13 @@
|
|||
package com.syaroful.agrilinkvocpro.control_feature.data.model
|
||||
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ActuatorHistoryResponse(
|
||||
@SerialName("data")
|
||||
val `data`: List<ActuatorHistoryData?>?,
|
||||
@SerialName("lastPage")
|
||||
val `data`: List<ActuatorLog?>?,
|
||||
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?
|
||||
)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.syaroful.agrilinkvocpro.control_feature.data.model
|
||||
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -10,7 +10,6 @@ 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
|
||||
|
|
@ -44,17 +43,8 @@ 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,29 +37,8 @@ class ControlRepository(
|
|||
return controlService.getActuatorStatus(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
|
||||
)
|
||||
suspend fun getActuatorHistory(authHeader: String): Response<ActuatorHistoryResponse> {
|
||||
return controlService.getActuatorsControlLog(authHeader)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
//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,24 +1,21 @@
|
|||
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.Row
|
||||
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.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.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
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
|
||||
|
|
@ -41,11 +38,9 @@ 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)
|
||||
|
|
@ -93,6 +88,14 @@ fun ControlActuatorScreen(
|
|||
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
},
|
||||
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
navController.navigate("control_history")
|
||||
}) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "History")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
|
|
@ -112,11 +115,9 @@ 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
|
||||
|
|
@ -180,30 +181,3 @@ 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,17 +1,11 @@
|
|||
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
|
||||
|
|
@ -25,19 +19,15 @@ 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
|
||||
|
|
@ -63,10 +53,7 @@ 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 = {
|
||||
|
|
@ -81,51 +68,25 @@ fun ControlHistoryScreen(
|
|||
|
||||
) { innerPadding ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
isRefreshing = isRefreshing.value,
|
||||
onRefresh = {
|
||||
isRefreshing.value = true
|
||||
controlHistoryViewModel.loadInitialHistory()
|
||||
controlHistoryViewModel.getActuatorHistory()
|
||||
},
|
||||
) {
|
||||
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 (val state = historyState) {
|
||||
is ControlState.Loading -> {
|
||||
if (historyList.isEmpty()) {
|
||||
item {
|
||||
CircularProgressIndicator(color = MainGreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (historyState) {
|
||||
is ControlState.Error -> {
|
||||
isRefreshing.value = false
|
||||
item {
|
||||
DefaultErrorComponent(
|
||||
message = state.message,
|
||||
message = (historyState as ControlState.Error).message,
|
||||
label = "Oops!",
|
||||
)
|
||||
}
|
||||
|
|
@ -133,95 +94,52 @@ fun ControlHistoryScreen(
|
|||
|
||||
is ControlState.Success -> {
|
||||
isRefreshing.value = false
|
||||
if (historyList.isEmpty()) {
|
||||
item {
|
||||
DefaultErrorComponent(
|
||||
label = "Data kosong",
|
||||
message = "Tidak ada riwayat kontrol yang tersedia"
|
||||
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
|
||||
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
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)
|
||||
Text("Tidak ada data riwayat kontrol")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
is ControlState.Loading -> {
|
||||
item {
|
||||
CircularProgressIndicator(
|
||||
color = MainGreen,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.size(24.dp)
|
||||
color = MainGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
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
|
||||
|
||||
|
|
@ -23,88 +22,35 @@ class ControlHistoryViewModel(
|
|||
|
||||
private val _historyState =
|
||||
MutableStateFlow<ControlState<ActuatorHistoryResponse>>(ControlState.Idle)
|
||||
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
|
||||
val historyState: MutableStateFlow<ControlState<ActuatorHistoryResponse>> = _historyState
|
||||
|
||||
init {
|
||||
loadInitialHistory()
|
||||
getActuatorHistory()
|
||||
}
|
||||
|
||||
fun loadInitialHistory() {
|
||||
fun getActuatorHistory() {
|
||||
_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,
|
||||
page = currentPage,
|
||||
limit = limitPerPage
|
||||
)
|
||||
val response = repository.getActuatorHistory(authHeader = authHeader)
|
||||
if (response.isSuccessful) {
|
||||
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)
|
||||
val data = response.body()
|
||||
_historyState.value = ControlState.Success(data)
|
||||
Log.d(TAG, "Successfully get Actuator History ${response.body()}")
|
||||
} else {
|
||||
_historyState.value = ControlState.Error(response.message())
|
||||
val errorBody = response.errorBody()?.string()
|
||||
val errorMessage = errorBody ?: "Error ${response.code()}"
|
||||
_historyState.value = ControlState.Error(errorMessage)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_historyState.value = ControlState.Error(mapToUserFriendlyError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
val errorMessage = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 705 B |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
|
@ -39,7 +39,7 @@ fun GrowthRecipeFeatureBanner(){
|
|||
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
|
||||
)
|
||||
Text(
|
||||
"Lihat rekomendasi perawatan optimal",
|
||||
"Lihat rekomendasi perawatan optimal untuk setiap jenis tanaman",
|
||||
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,10 @@ 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
|
||||
|
|
@ -31,7 +28,6 @@ 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
|
||||
|
|
@ -56,25 +52,21 @@ fun GrowthRecipeScreen(
|
|||
navController: NavController,
|
||||
viewModel: GrowthRecipeViewModel = koinViewModel()
|
||||
) {
|
||||
val sensorIdOptions: Map<String, String> = mapOf("Bed 1" to "npk1", "Bed 2" to "npk2")
|
||||
val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry")
|
||||
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(selectedSensorId.value)
|
||||
viewModel.getGraphicData("npk1")
|
||||
}
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing.value,
|
||||
onRefresh = {
|
||||
isRefreshing.value = true
|
||||
viewModel.getGraphicData(selectedSensorId.value)
|
||||
viewModel.getGraphicData("npk1")
|
||||
},
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -112,20 +104,10 @@ fun GrowthRecipeScreen(
|
|||
},
|
||||
title = "✨ Lihat saran perawatan"
|
||||
)
|
||||
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
|
||||
|
|
@ -137,7 +119,7 @@ fun GrowthRecipeScreen(
|
|||
color = MainGreen.copy(alpha = 0.1f)
|
||||
)
|
||||
.size(40.dp),
|
||||
onClick = {expanded = !expanded},
|
||||
onClick = { },
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
|
|
@ -145,27 +127,6 @@ 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(
|
||||
|
|
@ -202,7 +163,7 @@ fun GrowthRecipeScreen(
|
|||
isRefreshing.value = false
|
||||
val dataList =
|
||||
(graphicState as ResultState.Success<NpkGraphicDayResponse>).data?.data?.get(
|
||||
selectedSensorId.value
|
||||
"npk1"
|
||||
).orEmpty()
|
||||
|
||||
val days = dataList.mapNotNull { it.day?.toInt() }
|
||||
|
|
@ -230,7 +191,7 @@ fun GrowthRecipeScreen(
|
|||
}
|
||||
}
|
||||
Text(
|
||||
"🌱 Saran Perbandingan Nutrisi Optimal",
|
||||
"🌱 Saran Nutrisi Optimal",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Row(
|
||||
|
|
@ -238,34 +199,26 @@ fun GrowthRecipeScreen(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text("Nutrisi")
|
||||
Text("N")
|
||||
Text("P")
|
||||
Text("K")
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text("Vegetatif")
|
||||
NutrientStandartBox("30")
|
||||
NutrientStandartBox("10")
|
||||
NutrientStandartBox("10")
|
||||
Text("30 ppm")
|
||||
Text("40 ppm")
|
||||
Text("50 ppm")
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text("Generatif")
|
||||
NutrientStandartBox("10")
|
||||
NutrientStandartBox("20")
|
||||
NutrientStandartBox("20")
|
||||
Text("50 ppm")
|
||||
Text("60 ppm")
|
||||
Text("70 ppm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,7 +51,6 @@ class GrowthRecipeViewModel(
|
|||
sensor = sensor,
|
||||
)
|
||||
if (response.isSuccessful) {
|
||||
Log.d(TAG, "Response: ${response.body()}")
|
||||
response.body()?.let { body ->
|
||||
_getGraphicState.value = ResultState.Success(body)
|
||||
} ?: run {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ 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)
|
||||
|
|
@ -122,13 +121,13 @@ fun DetailHistoryScreen(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = if (plantDiagnosis?.diagnosis == "Sehat") MainGreen else Color.Red,
|
||||
color = Color.Red,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (plantDiagnosis?.diagnosis == "Sehat") "Sehat" else "Terkena Penyakit",
|
||||
text = "Terkena Penyakit",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
|
|
|
|||
BIN
skripsi/Draft Skripsi - Muhamad Syaroful Anam.docx
Normal file
BIN
skripsi/Proposal Skripsi - Muhamad Syaroful Anam.pdf
Normal file
BIN
snapshoot/Screenshot_20250611_214911.png
Normal file
|
After Width: | Height: | Size: 161 KiB |