Compare commits

..

No commits in common. "4dab797cb9dad0508785310f7b19525a76806dd4" and "30c751be7a508fdd16ebab3bb0acde41b9824fe2" have entirely different histories.

66 changed files with 399 additions and 1859 deletions

View File

@ -13,7 +13,7 @@ android {
applicationId = "com.syaroful.agrilinkvocpro"
minSdk = 29
targetSdk = 35
versionCode = 15
versionCode = 13
versionName = "1.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@ -23,8 +23,8 @@ fun DownloadModuleConfirmationDialog(
val message = when (moduleName) {
"control_feature" -> "Apakah Anda ingin mendownload fitur Kontrol Aktuator?"
"growth_recipe_feature" -> "Apakah Anda ingin mendownload fitur Formula Pertumbuhan Optimal?"
"commodity_price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?"
"recipe_feature" -> "Apakah Anda ingin mendownload fitur Resep Pertumbuhan?"
"price_prediction_feature" -> "Apakah Anda ingin mendownload fitur Prediksi Harga Komoditas?"
"plant_disease_detection_feature" -> "Apakah Anda ingin mendownload fitur Deteksi Penyakit Tanaman?"
else -> "Apakah Anda ingin mendownload modul ini?"
}
@ -69,7 +69,7 @@ fun DownloadModuleConfirmationDialog(
fun PreviewDownloadModuleConfirmationDialog() {
DownloadModuleConfirmationDialog(
moduleName = "commodity_price_prediction_feature",
moduleName = "price_prediction_feature",
onClickConfirm = {},
onDismiss = {}
)

View File

@ -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?
)

View File

@ -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?
)

View File

@ -1,18 +1,15 @@
package com.syaroful.agrilinkvocpro.data.network
import com.syaroful.agrilinkvocpro.data.model.BedLocationResponse
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.RegisterResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
@ -64,16 +61,4 @@ interface ApiService {
@Query("sensor") sensor: String
): Response<DhtGraphicDataResponse>
@GET("api/sensors/{sensorId}")
suspend fun getSensorInformation(
@Header("Authorization") authHeader: String,
@Path("sensorId") sensorId: Int
): Response<SensorInformationResponse>
@GET("api/bed-locations/{bedId}")
suspend fun getBedLocation(
@Header("Authorization") authHeader: String,
@Path("bedId") bedId: Int
): Response<BedLocationResponse>
}

View File

@ -1,10 +1,8 @@
package com.syaroful.agrilinkvocpro.data.repository
import com.syaroful.agrilinkvocpro.data.model.BedLocationResponse
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse
import com.syaroful.agrilinkvocpro.data.network.ApiService
import retrofit2.Response
@ -40,19 +38,4 @@ class SensorDataRepository(private val apiService: ApiService) {
): Response<DhtGraphicDataResponse> {
return apiService.getDhtDataSensor(authHeader, startDate, endDate, timeRange, sensor)
}
suspend fun getSensorInformation(
authHeader: String,
sensorId: Int
): Response<SensorInformationResponse>{
return apiService.getSensorInformation(authHeader, sensorId = sensorId)
}
suspend fun getBedLocation(
authHeader: String,
bedId: Int
): Response<BedLocationResponse>{
return apiService.getBedLocation(authHeader, bedId = bedId)
}
}

View File

@ -1,16 +1,9 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@ -24,10 +17,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.getCurrentDate
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DetailDhtContent
@ -44,27 +33,23 @@ fun DetailScreen(
detailViewModel: DetailViewModel = koinViewModel(),
) {
val date = remember { mutableStateOf(Date().toFormattedString()) }
val id = if (sensorId == "dht") 1 else if (sensorId == "npk1") 2 else 3
LaunchedEffect(sensorId) {
if (sensorId == "dht") {
detailViewModel.fetchDhtData(date = date.value, sensor = sensorId)
detailViewModel.fetchSensorInformation(id)
} else {
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
detailViewModel.fetchSensorInformation(id)
}
}
val npkDataState by detailViewModel.npkDataState.collectAsState()
val dhtDataState by detailViewModel.dhtDataState.collectAsState()
val bedState by detailViewModel.bedState.collectAsState()
val currentData = detailViewModel.currentSensorData
val isRefreshing = remember { mutableStateOf(false) }
val currentDate = remember { mutableStateOf(getCurrentDate()) }
Scaffold(
topBar = { DetailTopBar() }) { innerPadding ->
PullToRefreshBox(
@ -76,72 +61,26 @@ fun DetailScreen(
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
}
}) {
Column(
modifier = modifier
.padding(innerPadding)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
when (bedState) {
is ResultState.Success -> {
val data = (bedState as ResultState.Success).data
ListItem(
modifier = Modifier
.padding(horizontal = 16.dp)
.border(
shape = RoundedCornerShape(8.dp),
width = 1.dp,
color = MaterialTheme.colorScheme.surfaceContainerHighest.copy(
alpha = 0.5f
)
),
headlineContent = {
Text(text = (data?.data?.address ?: "unknown"), style = MaterialTheme.typography.titleSmall)
},
supportingContent = {
Text(text = (data?.data?.description ?: "unknown"), style = MaterialTheme.typography.bodySmall, modifier = Modifier.alpha(0.6f))
},
trailingContent = {
Box(
modifier = Modifier.height(48.dp).padding(vertical = 16.dp),
){ Text(text = (data?.data?.name ?: "unknown")) }
}
)
}
is ResultState.Error -> {
val errorMessage = (bedState as ResultState.Error).message
Text(errorMessage)
}
ResultState.Idle -> {}
ResultState.Loading -> Box(
modifier
.fillMaxWidth()
.height(80.dp)
.shimmerEffect()
)
}
if (sensorId == "dht") DetailDhtContent(
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
)
}
if (sensorId == "dht") DetailDhtContent(
modifier = Modifier.padding(innerPadding),
viewModel = detailViewModel,
dhtDataState = dhtDataState,
currentData = currentData,
sensorId = sensorId,
isRefreshing = isRefreshing,
date = date,
currentDate = currentDate
)
else DetailNpkContent(
modifier = Modifier.padding(innerPadding),
viewModel = detailViewModel,
npkDataState = npkDataState,
currentData = currentData,
sensorId = sensorId,
isRefreshing = isRefreshing,
date = date,
currentDate = currentDate
)
}
}
}

View File

@ -7,11 +7,9 @@ import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.data.model.BedLocationResponse
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorInformationResponse
import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
@ -32,75 +30,14 @@ class DetailViewModel(
val currentSensorData: SensorDataResponse?
get() = sensorDataRepository.latestSensorData
private val _npkDataState =
MutableStateFlow<ResultState<NpkGraphicDataResponse>>(ResultState.Idle)
private val _npkDataState = MutableStateFlow<ResultState<NpkGraphicDataResponse>>(ResultState.Idle)
val npkDataState: StateFlow<ResultState<NpkGraphicDataResponse>> = _npkDataState.asStateFlow()
private val _dhtDataState =
MutableStateFlow<ResultState<DhtGraphicDataResponse>>(ResultState.Idle)
private val _dhtDataState = MutableStateFlow<ResultState<DhtGraphicDataResponse>>(ResultState.Idle)
val dhtDataState: StateFlow<ResultState<DhtGraphicDataResponse>> = _dhtDataState.asStateFlow()
private val _sensorInformationState =
MutableStateFlow<ResultState<SensorInformationResponse>>(ResultState.Idle)
val sensorInformationState: StateFlow<ResultState<SensorInformationResponse>> =
_sensorInformationState.asStateFlow()
private val _bedState =
MutableStateFlow<ResultState<BedLocationResponse>>(ResultState.Idle)
val bedState: StateFlow<ResultState<BedLocationResponse>> =
_bedState.asStateFlow()
private val today = Date()
fun fetchSensorInformation(sensorId: Int) {
_bedState.value = ResultState.Loading
Log.d(TAG, "Sensor ID: $sensorId")
viewModelScope.launch {
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
try {
delay(300L)
val response = sensorDataRepository.getSensorInformation(
authHeader = authHeader,
sensorId = sensorId
)
if (response.isSuccessful) {
Log.d(TAG, "Sensor Info: ${response.body()}")
response.body()?.let { body ->
val responseBed = sensorDataRepository.getBedLocation(
authHeader,
body.data?.bedLocationId ?: 1
)
if (responseBed.isSuccessful) {
Log.d(TAG, "Bed Location: ${responseBed.body()}")
responseBed.body()?.let { bedBody ->
_bedState.value = ResultState.Success(bedBody)
} ?: run {
_bedState.value =
ResultState.Error("Informasi Bed tidak ditemukan")
}
} else {
_bedState.value =
ResultState.Error("Error: ${responseBed.code()} - ${responseBed.message()}")
}
} ?: run {
_sensorInformationState.value =
ResultState.Error("Informasi Sensor tidak ditemukan")
}
} else {
_sensorInformationState.value =
ResultState.Error("Error: ${response.code()} - ${response.message()}")
}
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
_sensorInformationState.value = ResultState.Error(errorMessage)
_bedState.value = ResultState.Error(errorMessage)
Log.d(TAG, "Failed to fetch data: ${e.message}")
}
}
}
fun fetchNpkData(
date: String = today.toFormattedString(),
sensor: String,
@ -125,8 +62,7 @@ class DetailViewModel(
_npkDataState.value = ResultState.Error("Data tidak ditemukan")
}
} else {
_npkDataState.value =
ResultState.Error("Error: ${response.code()} - ${response.message()}")
_npkDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}")
}
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
@ -160,8 +96,7 @@ class DetailViewModel(
_dhtDataState.value = ResultState.Error("Data tidak ditemukan")
}
} else {
_dhtDataState.value =
ResultState.Error("Error: ${response.code()} - ${response.message()}")
_dhtDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}")
}
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)

View File

@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -52,6 +54,7 @@ fun DetailDhtContent(
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
when (dhtDataState) {
is ResultState.Loading -> {

View File

@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -55,6 +57,7 @@ fun DetailNpkContent(
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
when (npkDataState) {
is ResultState.Loading -> {

View File

@ -4,10 +4,8 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
@ -55,7 +53,6 @@ fun DynamicBottomSheet(
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.Filled.ArrowDropDown,
tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f),
contentDescription = "Dropdown Arrow"
)
}
@ -65,24 +62,19 @@ fun DynamicBottomSheet(
onDismissRequest = { setShowSheet(false) },
sheetState = sheetState
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(options.size) { index ->
val option = options[index]
Text(
text = option,
modifier = Modifier
.fillMaxWidth()
.clickable {
onValueSelected(option)
setSelectedOption(option)
setShowSheet(false)
}
.padding(16.dp)
)
}
options.forEach { option ->
Text(
text = option,
modifier = Modifier
.fillMaxWidth()
.clickable {
onValueSelected(option)
setSelectedOption(option)
setShowSheet(false)
}
.padding(16.dp)
)
}
}
}
}
}

View File

@ -324,22 +324,22 @@ fun DynamicFeatureSection(
horizontalArrangement = Arrangement.SpaceBetween
) {
MenuItemButton(
label = "Control\nActuator",
label = "Kontrol\nAktuator",
icon = painterResource(id = R.drawable.control_actuator_icon),
onClick = { onFeatureClick("control_feature") },
)
MenuItemButton(
label = "Growth\nFormula",
label = "Resep\nPertumbuhan",
icon = painterResource(id = R.drawable.growth_recipe_icon),
onClick = { onFeatureClick("growth_recipe_feature") },
)
MenuItemButton(
label = "Commodity\nPrice",
label = "Harga\nKomoditas",
icon = painterResource(id = R.drawable.commodity_price_prediction_icon),
onClick = { onFeatureClick("commodity_price_prediction_feature") },
)
MenuItemButton(
label = "Disease\nDetection",
label = "Deteksi\nPenyakit",
icon = painterResource(id = R.drawable.plant_disease_detection_icon),
onClick = { onFeatureClick("plant_disease_detection_feature") },
)
@ -378,7 +378,7 @@ fun GreenHouseInformationSection(navController: NavController) {
modifier = Modifier.weight(1f)
) {
Text(text = "2 Commodities", color = MainGreen, style = textTheme.bodyMedium)
Text(text = "2 Komoditas", color = MainGreen, style = textTheme.bodyMedium)
Spacer(modifier = Modifier.height(24.dp))
Text(text = "Green House Bumiaji", style = textTheme.bodyLarge)
Text(
@ -397,4 +397,15 @@ fun GreenHouseInformationSection(navController: NavController) {
.clip(RoundedCornerShape(10.dp))
)
}
}
}
//@Preview(showBackground = true, name = "Light Mode")
//@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
//@Composable
//fun HomePreview() {
// val navController = rememberNavController()
// HomeScreen(
// navController = navController,
// onFeatureClick = {},
// )
//}

View File

@ -24,6 +24,9 @@ class HomeViewModel(
private val _homeState = MutableStateFlow<ResultState<SensorDataResponse>>(ResultState.Idle)
val homeState: StateFlow<ResultState<SensorDataResponse>> = _homeState
// var currentDataSensor: SensorDataResponse? = null
// private set
init {
getGreenHouseData()
}

View File

@ -94,11 +94,11 @@ fun RegisterScreen(
)
Text(
modifier = Modifier.fillMaxWidth(),
text = "Register", style = textTheme.titleMedium, textAlign = TextAlign.Center
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
)
Text(
modifier = Modifier.fillMaxWidth(),
text = "Halo! yuk daftarkan akun ke aplikasi",
text = "Halo! yuk masuk ke dalam akunmu",
style = textTheme.titleSmall.copy(color = DarkGrey),
textAlign = TextAlign.Center
)

View File

@ -7,7 +7,7 @@
<string name="control_feature_label">Kontrol Aktuator</string>
<string name="play_store_icon_desc">Google Play Store</string>
<string name="download_module_title">Unduh Fitur</string>
<string name="download_module_title">Unduh Modul Fitur Dinamis</string>
<string name="download_module_message">anda perlu mengunduh modul fitur dinamis agar fitur ini dapat digunakan</string>
<string name="download">Download</string>
<string name="cancel">Cancel</string>

View File

@ -4,31 +4,17 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.navigation.compose.rememberNavController
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di.appModule
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di.networkModule
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.di.viewModelModule
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.navigation.SetupNavigation
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PricePredictionScreen
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
import org.koin.core.context.loadKoinModules
import org.koin.core.context.unloadKoinModules
class PricePredictionActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
loadKoinModules(listOf(appModule, viewModelModule, networkModule))
setContent {
AgrilinkVocproTheme {
val navController = rememberNavController()
SetupNavigation(navController)
PricePredictionScreen()
}
}
}
override fun onDestroy() {
super.onDestroy()
unloadKoinModules(listOf(appModule, viewModelModule, networkModule))
}
}

View File

@ -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",
)
}
}
}

View File

@ -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",
)
}

View File

@ -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
}

View File

@ -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)
// }
//}

View File

@ -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?
)

View File

@ -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>
}

View File

@ -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)
}
}

View File

@ -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()) }
}

View File

@ -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) }
}

View File

@ -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()) }
}

View File

@ -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)
}
}
}

View File

@ -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),
// )
// }

View File

@ -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)
}
}
}
}

View File

@ -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
)
}
}
)
}

View File

@ -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
)
}
}
}

View File

@ -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)
)
}
}

View File

@ -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")
}
}
}
}

View File

@ -138,7 +138,7 @@ fun PriceChart(
"Harga",
spacing / 2f,
labelPadding - 100,
textPaint
textPaint.apply { color = android.graphics.Color.GREEN }
)
horizontalValue?.let { value ->

View File

@ -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),
)
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@ -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",
)
}
}
}

View File

@ -1,69 +1,13 @@
package com.syaroful.agrilinkvocpro.control_feature.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ActuatorHistoryResponse(
@SerialName("data")
val `data`: List<ActuatorHistoryData?>?,
@SerialName("lastPage")
val `data`: List<ActuatorLog?>?,
val lastPage: Int?,
@SerialName("message")
val message: String?,
@SerialName("nextPage")
val nextPage: Any?,
@SerialName("page")
val page: Int?,
@SerialName("perPage")
val perPage: Int?,
@SerialName("previousPage")
val previousPage: Any?,
@SerialName("statusCode")
val statusCode: Int?,
@SerialName("total")
val total: Int?
)
@Serializable
data class ActuatorHistoryData(
@SerialName("action")
val action: String?,
@SerialName("actuator")
val actuator: Actuator?,
@SerialName("actuatorId")
val actuatorId: Int?,
@SerialName("createdAt")
val createdAt: String?,
@SerialName("id")
val id: Int?,
@SerialName("triggeredBy")
val triggeredBy: String?,
@SerialName("turnOffAt")
val turnOffAt: Any?
)
@Serializable
data class Actuator(
@SerialName("actuatorTypeId")
val actuatorTypeId: Int?,
@SerialName("bedLocationId")
val bedLocationId: Any?,
@SerialName("createdAt")
val createdAt: String?,
@SerialName("deletedAt")
val deletedAt: Any?,
@SerialName("id")
val id: Int?,
@SerialName("maxDuration")
val maxDuration: Int?,
@SerialName("name")
val name: String?,
@SerialName("relayPin")
val relayPin: Int?,
@SerialName("slug")
val slug: String?,
@SerialName("updatedAt")
val updatedAt: String?
)

View File

@ -1,6 +0,0 @@
package com.syaroful.agrilinkvocpro.control_feature.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -10,7 +10,6 @@ import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query
interface ControlService {
// get all actuator status
@ -44,17 +43,8 @@ interface ControlService {
): Response<ControlLogResponse>
// get controls log
@GET("api/actuator-control-logs")
@GET("api//actuator-control-logs")
suspend fun getActuatorsControlLog(
@Header("Authorization") authHeader: String,
@Query("actuatorId") actuatorId: Int? = null,
@Query("status") status: String? = null,
@Query("startDate") startDate: String? = null,
@Query("endDate") endDate: String? = null,
@Query("page") page: Int? = null,
@Query("limit") limit: Int? = null,
@Query("sort_field") sortField: String? = null,
@Query("sort_direction") sortDirection: String? = null
): Response<ActuatorHistoryResponse>
}

View File

@ -37,29 +37,8 @@ class ControlRepository(
return controlService.getActuatorStatus(authHeader)
}
suspend fun getActuatorHistory(
authHeader: String,
actuatorId: Int? = null,
status: String? = null,
startDate: String? = null,
endDate: String? = null,
page: Int? = null,
limit: Int? = null,
sortField: String? = null,
sortDirection: String? = null
): Response<ActuatorHistoryResponse> {
return controlService.getActuatorsControlLog(
authHeader = authHeader,
actuatorId = actuatorId,
status = status,
startDate = startDate,
endDate = endDate,
page = page,
limit = limit,
sortField = sortField,
sortDirection = sortDirection
)
suspend fun getActuatorHistory(authHeader: String): Response<ActuatorHistoryResponse> {
return controlService.getActuatorsControlLog(authHeader)
}
}

View File

@ -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 = {}
// )
//}

View File

@ -1,24 +1,21 @@
package com.syaroful.agrilinkvocpro.control_feature.presentation.control
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
@ -41,11 +38,9 @@ import com.syaroful.agrilinkvocpro.control_feature.R
import com.syaroful.agrilinkvocpro.control_feature.core.components.ControlCard
import com.syaroful.agrilinkvocpro.control_feature.core.components.CustomSnackBar
import com.syaroful.agrilinkvocpro.control_feature.core.components.DisableControlCard
import com.syaroful.agrilinkvocpro.control_feature.core.components.FeatureBanner
import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import kotlinx.coroutines.flow.collectLatest
@OptIn(ExperimentalMaterial3Api::class)
@ -93,6 +88,14 @@ fun ControlActuatorScreen(
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
}
},
actions = {
IconButton(onClick = {
navController.navigate("control_history")
}) {
Icon(Icons.Filled.Refresh, contentDescription = "History")
}
}
)
}
) { innerPadding ->
@ -112,11 +115,9 @@ fun ControlActuatorScreen(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
FeatureBanner()
HistoryButton {
navController.navigate("control_history")
}
Text("Daftar Actuator", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(1.dp))
when (allActuators) {
is ControlState.Loading -> {
isRefreshing.value = false
@ -180,30 +181,3 @@ fun ControlActuatorScreen(
}
}
}
@Composable
private fun HistoryButton(onClick: () -> Unit) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MainGreen
),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(color = MainGreen, width = 1.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Control History")
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
tint = MainGreen,
contentDescription = "button history"
)
}
}
}

View File

@ -1,17 +1,11 @@
package com.syaroful.agrilinkvocpro.control_feature.presentation.history
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
@ -25,19 +19,15 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.syaroful.agrilinkvocpro.control_feature.R
import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState
import com.syaroful.agrilinkvocpro.control_feature.core.utils.getRelativeTime
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryResponse
@ -63,10 +53,7 @@ fun ControlHistoryScreen(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Riwayat Kontrol Aktuator",
style = MaterialTheme.typography.titleMedium
)
Text("Riwayat Kontrol Aktuator", style = MaterialTheme.typography.titleMedium)
}
},
navigationIcon = {
@ -79,53 +66,27 @@ fun ControlHistoryScreen(
)
},
) { innerPadding ->
) { innerPadding ->
PullToRefreshBox(
modifier = Modifier
.fillMaxSize(),
isRefreshing = isRefreshing.value,
onRefresh = {
isRefreshing.value = true
controlHistoryViewModel.loadInitialHistory()
controlHistoryViewModel.getActuatorHistory()
},
) {
val historyList = controlHistoryViewModel.historyList
val listState = rememberLazyListState()
LaunchedEffect(listState) {
snapshotFlow {
val layoutInfo = listState.layoutInfo
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
val totalItems = layoutInfo.totalItemsCount
lastVisibleItem to totalItems
}.collect { (lastVisible, totalItems) ->
if (lastVisible >= totalItems - 1 && !controlHistoryViewModel.isLastPage) {
controlHistoryViewModel.loadMoreHistory()
}
}
}
LazyColumn(
state = listState,
modifier = Modifier
.padding(innerPadding)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when (val state = historyState) {
is ControlState.Loading -> {
if (historyList.isEmpty()) {
item {
CircularProgressIndicator(color = MainGreen)
}
}
}
when (historyState) {
is ControlState.Error -> {
isRefreshing.value = false
item {
DefaultErrorComponent(
message = state.message,
message = (historyState as ControlState.Error).message,
label = "Oops!",
)
}
@ -133,95 +94,52 @@ fun ControlHistoryScreen(
is ControlState.Success -> {
isRefreshing.value = false
if (historyList.isEmpty()) {
item {
DefaultErrorComponent(
label = "Data kosong",
message = "Tidak ada riwayat kontrol yang tersedia"
val data =
(historyState as ControlState.Success<ActuatorHistoryResponse>).data?.data
if (!data.isNullOrEmpty()) {
val reversedData = data.reversed()
items(reversedData) { historyData ->
ListItem(
leadingContent = {
Text("${historyData?.actuatorId}")
},
headlineContent = {
val state =
if (historyData?.action == "ON") "Diaktifkan oleh " else "Dinonaktifkan oleh "
Text(text = "$state ${historyData?.triggeredBy}")
},
supportingContent = {
Text(
text = getRelativeTime(historyData?.createdAt.toString())
)
},
trailingContent = {
Text("${historyData?.action}",
color = if (historyData?.action == "ON") MainGreen else Color.Red
)
},
shadowElevation = 2.dp
)
}
} else {
item {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
(historyState as? ControlState.Success<ActuatorHistoryResponse>)?.data?.let { response ->
Text("Total: ${response.total}", style = MaterialTheme.typography.titleSmall)
} ?: run {
Text("Total: -", style = MaterialTheme.typography.titleSmall)
}
// 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)
)
}
Text("Tidak ada data riwayat kontrol")
}
}
}
else -> Unit
is ControlState.Loading -> {
item {
CircularProgressIndicator(
color = MainGreen
)
}
}
else -> {}
}
}
}
}
}

View File

@ -1,16 +1,15 @@
package com.syaroful.agrilinkvocpro.control_feature.presentation.history
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryData
import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorHistoryResponse
import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.data.UserPreferences
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -23,88 +22,35 @@ class ControlHistoryViewModel(
private val _historyState =
MutableStateFlow<ControlState<ActuatorHistoryResponse>>(ControlState.Idle)
val historyState: StateFlow<ControlState<ActuatorHistoryResponse>> = _historyState
private val _historyList = mutableListOf<ActuatorHistoryData>()
val historyList: List<ActuatorHistoryData> get() = _historyList
private var currentPage = 1
private val limitPerPage = 10
private var isLoadingMore = false
var isLastPage = false
private set
val historyState: MutableStateFlow<ControlState<ActuatorHistoryResponse>> = _historyState
init {
loadInitialHistory()
getActuatorHistory()
}
fun loadInitialHistory() {
fun getActuatorHistory() {
_historyState.value = ControlState.Loading
// Reset pagination state
currentPage = 1
isLastPage = false
isLoadingMore = false
_historyList.clear()
viewModelScope.launch {
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
try {
delay(500L)
val response = repository.getActuatorHistory(
authHeader = authHeader,
page = currentPage,
limit = limitPerPage
)
val response = repository.getActuatorHistory(authHeader = authHeader)
if (response.isSuccessful) {
val body = response.body()
_historyList.clear()
val safeData = body?.data?.filterNotNull() ?: emptyList()
_historyList.addAll(safeData)
isLastPage = currentPage >= (body?.lastPage ?: 1)
_historyState.value = ControlState.Success(body)
val data = response.body()
_historyState.value = ControlState.Success(data)
Log.d(TAG, "Successfully get Actuator History ${response.body()}")
} else {
_historyState.value = ControlState.Error(response.message())
val errorBody = response.errorBody()?.string()
val errorMessage = errorBody ?: "Error ${response.code()}"
_historyState.value = ControlState.Error(errorMessage)
}
} catch (e: Exception) {
_historyState.value = ControlState.Error(mapToUserFriendlyError(e))
val errorMessage = mapToUserFriendlyError(e)
_historyState.value = ControlState.Error(errorMessage)
}
}
}
fun loadMoreHistory() {
if (isLoadingMore || isLastPage) return
isLoadingMore = true
currentPage += 1
viewModelScope.launch {
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
try {
val response = repository.getActuatorHistory(
authHeader = authHeader,
page = currentPage,
limit = limitPerPage
)
if (response.isSuccessful) {
val body = response.body()
val safeData = body?.data?.filterNotNull() ?: emptyList()
_historyList.addAll(safeData)
isLastPage = currentPage >= (body?.lastPage ?: 1)
_historyState.value = ControlState.Success(body)
} else {
currentPage -= 1
_historyState.value = ControlState.Error(response.message())
}
} catch (e: Exception) {
currentPage -= 1
_historyState.value = ControlState.Error(mapToUserFriendlyError(e))
} finally {
isLoadingMore = false
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -39,7 +39,7 @@ fun GrowthRecipeFeatureBanner(){
style = MaterialTheme.typography.titleMedium.copy(color = Color.White)
)
Text(
"Lihat rekomendasi perawatan optimal",
"Lihat rekomendasi perawatan optimal untuk setiap jenis tanaman",
style = MaterialTheme.typography.bodySmall.copy(Color.White.copy(alpha = 0.5f))
)
}

View File

@ -14,13 +14,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@ -31,7 +28,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -56,25 +52,21 @@ fun GrowthRecipeScreen(
navController: NavController,
viewModel: GrowthRecipeViewModel = koinViewModel()
) {
val sensorIdOptions: Map<String, String> = mapOf("Bed 1" to "npk1", "Bed 2" to "npk2")
val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry")
val sensorOptions = listOf("Nitrogen", "Pospor", "Kalium")
val selectedSensor = remember { mutableStateOf("Nitrogen") }
val selectedSensorId = remember { mutableStateOf(sensorIdOptions.values.first()) }
val graphicState by viewModel.getGraphicState.collectAsState()
val isRefreshing = remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.getGraphicData(selectedSensorId.value)
viewModel.getGraphicData("npk1")
}
PullToRefreshBox(
isRefreshing = isRefreshing.value,
onRefresh = {
isRefreshing.value = true
viewModel.getGraphicData(selectedSensorId.value)
viewModel.getGraphicData("npk1")
},
) {
Scaffold(
@ -112,20 +104,10 @@ fun GrowthRecipeScreen(
},
title = "✨ Lihat saran perawatan"
)
Row(
modifier = Modifier.fillMaxWidth()
,
horizontalArrangement = Arrangement.SpaceBetween
){
Text(
"🪴 Grafik nutrisi dalam 10 hari",
style = MaterialTheme.typography.titleMedium
)
Text(
selectedSensorId.value,
style = MaterialTheme.typography.titleMedium
)
}
Text(
"🪴 Grafik nutrisi dalam 10 hari",
style = MaterialTheme.typography.titleMedium
)
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
@ -137,7 +119,7 @@ fun GrowthRecipeScreen(
color = MainGreen.copy(alpha = 0.1f)
)
.size(40.dp),
onClick = {expanded = !expanded},
onClick = { },
) {
Icon(
modifier = Modifier.size(24.dp),
@ -145,27 +127,6 @@ fun GrowthRecipeScreen(
tint = MainGreen,
contentDescription = "Choose bet"
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
sensorIdOptions.forEach { item ->
DropdownMenuItem(
text = {
Text(
item.key,
modifier = Modifier.fillMaxWidth()
)
},
colors = MenuDefaults.itemColors(MaterialTheme.colorScheme.onBackground),
onClick = {
selectedSensorId.value = item.value
expanded = false
viewModel.getGraphicData(selectedSensorId.value)
}
)
}
}
}
Spacer(modifier = Modifier.width(8.dp))
DynamicBottomSheet(
@ -202,7 +163,7 @@ fun GrowthRecipeScreen(
isRefreshing.value = false
val dataList =
(graphicState as ResultState.Success<NpkGraphicDayResponse>).data?.data?.get(
selectedSensorId.value
"npk1"
).orEmpty()
val days = dataList.mapNotNull { it.day?.toInt() }
@ -230,7 +191,7 @@ fun GrowthRecipeScreen(
}
}
Text(
"🌱 Saran Perbandingan Nutrisi Optimal",
"🌱 Saran Nutrisi Optimal",
style = MaterialTheme.typography.titleMedium
)
Row(
@ -238,34 +199,26 @@ fun GrowthRecipeScreen(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(horizontalAlignment = Alignment.Start) {
Text("Nutrisi")
Text("N")
Text("P")
Text("K")
}
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(horizontalAlignment = Alignment.Start) {
Text("Vegetatif")
NutrientStandartBox("30")
NutrientStandartBox("10")
NutrientStandartBox("10")
Text("30 ppm")
Text("40 ppm")
Text("50 ppm")
}
Column(horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp)) {
Column(horizontalAlignment = Alignment.Start) {
Text("Generatif")
NutrientStandartBox("10")
NutrientStandartBox("20")
NutrientStandartBox("20")
Text("50 ppm")
Text("60 ppm")
Text("70 ppm")
}
}
}
}
}
}
@Composable
private fun NutrientStandartBox(
label: String
){
Box(modifier = Modifier.background(shape = RoundedCornerShape(8.dp), color = MaterialTheme.colorScheme.surfaceContainer).padding(horizontal = 8.dp, vertical = 4.dp)){ Text(label, style = MaterialTheme.typography.labelSmall) }
}

View File

@ -51,7 +51,6 @@ class GrowthRecipeViewModel(
sensor = sensor,
)
if (response.isSuccessful) {
Log.d(TAG, "Response: ${response.body()}")
response.body()?.let { body ->
_getGraphicState.value = ResultState.Success(body)
} ?: run {

View File

@ -40,7 +40,6 @@ import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBitmap
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.TextCardComponent
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@OptIn(ExperimentalMaterial3Api::class)
@ -122,13 +121,13 @@ fun DetailHistoryScreen(
Box(
modifier = Modifier
.background(
color = if (plantDiagnosis?.diagnosis == "Sehat") MainGreen else Color.Red,
color = Color.Red,
shape = RoundedCornerShape(4.dp)
)
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(
text = if (plantDiagnosis?.diagnosis == "Sehat") "Sehat" else "Terkena Penyakit",
text = "Terkena Penyakit",
color = Color.White,
style = MaterialTheme.typography.labelMedium
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB