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