feat: implement commodity price prediction feature
This commit introduces the commodity price prediction feature, including:
- **Data Layer:**
- Models for commodity prediction and price responses.
- `CommodityApiService` for fetching data from the backend.
- `CommodityRepository` to abstract data fetching logic.
- **DI Modules:**
- `appModule` for providing `CommodityRepository`.
- `networkModule` for setting up Retrofit and OkHttp.
- `viewModelModule` for providing `PredictionViewModel` and `CommodityViewModel`.
- **Presentation Layer:**
- `PredictionViewModel` and `CommodityViewModel` to handle business logic and expose data to UI.
- `PredictionScreen` to display commodity price predictions with a dropdown to select commodities and detailed breakdown of future prices (tomorrow, next week, next month, next 3 months).
- `CommodityPriceScreen` (initial setup, details to be implemented) to display current commodity prices.
- `CommodityListItem` composable for displaying individual commodity price information.
- `FeatureBanner` composable for both commodity price prediction and control features.
- **Navigation:**
- `SetupNavigation` to define navigation routes for `CommodityPriceScreen` and `PredictionScreen`.
- **Core Utilities:**
- `toRupiahFormat()` extension function to format numbers as Indonesian Rupiah.
- `percentageChange()` function to calculate percentage difference.
This commit is contained in:
parent
30c751be7a
commit
a822c1782e
|
|
@ -0,0 +1,54 @@
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
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?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
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()) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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()) }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,329 @@
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.control_feature.data.model
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user