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