From a822c1782e8694ee29de23d4e8d6199c69f22da0 Mon Sep 17 00:00:00 2001 From: Cutiful <113351087+Syaroful@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:55:25 +0700 Subject: [PATCH] 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. --- .../core/component/FeatureBanner.kt | 54 +++ .../core/utils/RupiahFormatter.kt | 16 + .../data/model/CommodityPredictionResponse.kt | 32 ++ .../data/model/CommodityPriceResponseItem.kt | 10 + .../data/network/CommodityApiService.kt | 22 ++ .../data/repository/CommodityRepository.kt | 15 + .../di/AppModule.kt | 8 + .../di/NetworkModule.kt | 26 ++ .../di/ViewModelModule.kt | 11 + .../navigation/NavGraph.kt | 20 ++ .../commodity/CommodityViewModel.kt | 51 +++ .../commodity/component/CommodityListItem.kt | 51 +++ .../prediction/PredictionScreen.kt | 329 ++++++++++++++++++ .../prediction/PredictionViewModel.kt | 40 +++ .../core/components/FeatureBanner.kt | 53 +++ .../control_feature/data/model/Data.kt | 6 + 16 files changed, 744 insertions(+) create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/component/FeatureBanner.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/utils/RupiahFormatter.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPredictionResponse.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPriceResponseItem.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/network/CommodityApiService.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/repository/CommodityRepository.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/AppModule.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/NetworkModule.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/ViewModelModule.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/navigation/NavGraph.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityViewModel.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/CommodityListItem.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionScreen.kt create mode 100644 agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionViewModel.kt create mode 100644 agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/FeatureBanner.kt create mode 100644 agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/Data.kt diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/component/FeatureBanner.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/component/FeatureBanner.kt new file mode 100644 index 0000000..c5529d7 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/component/FeatureBanner.kt @@ -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", + ) + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/utils/RupiahFormatter.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/utils/RupiahFormatter.kt new file mode 100644 index 0000000..73aabdd --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/core/utils/RupiahFormatter.kt @@ -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 +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPredictionResponse.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPredictionResponse.kt new file mode 100644 index 0000000..0b4eb8a --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPredictionResponse.kt @@ -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 { +// 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() +// jsonObject.entrySet().forEach { entry -> +// if (entry.key.startsWith("val_")) { +// predictions[entry.key] = entry.value.asDouble +// } +// } +// +// return CommodityPredictionResponse(name, date, predictions) +// } +//} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPriceResponseItem.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPriceResponseItem.kt new file mode 100644 index 0000000..694237a --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/model/CommodityPriceResponseItem.kt @@ -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? +) \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/network/CommodityApiService.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/network/CommodityApiService.kt new file mode 100644 index 0000000..a1afa8d --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/network/CommodityApiService.kt @@ -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 + + @GET("api/harga") + suspend fun getCommodityPrice( + @Query("komoditas") komoditas: String, + @Query("pasar") pasar: String, + @Query("tanggal") tanggal: String, + @Query("kab_kota") kabKota: String + ): List +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/repository/CommodityRepository.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/repository/CommodityRepository.kt new file mode 100644 index 0000000..71cfe9a --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/data/repository/CommodityRepository.kt @@ -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 { + return apiService.getCommodityPredictions(date) + } + + suspend fun fetchCommodityPrice(commodityName: String, marketName: String, date: String, city: String): List { + return apiService.getCommodityPrice(komoditas = commodityName, pasar = marketName, tanggal = date, kabKota = city) + } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/AppModule.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/AppModule.kt new file mode 100644 index 0000000..1a1b231 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/AppModule.kt @@ -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()) } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/NetworkModule.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/NetworkModule.kt new file mode 100644 index 0000000..ddaa58a --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/NetworkModule.kt @@ -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().create(CommodityApiService::class.java) } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/ViewModelModule.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/ViewModelModule.kt new file mode 100644 index 0000000..5b9d189 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/di/ViewModelModule.kt @@ -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()) } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/navigation/NavGraph.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/navigation/NavGraph.kt new file mode 100644 index 0000000..0f9f9cf --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/navigation/NavGraph.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityViewModel.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityViewModel.kt new file mode 100644 index 0000000..c847bac --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/CommodityViewModel.kt @@ -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.Idle + ) + val state: StateFlow>> = _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) + } + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/CommodityListItem.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/CommodityListItem.kt new file mode 100644 index 0000000..060c877 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/commodity/component/CommodityListItem.kt @@ -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 + ) + } + } + ) +} diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionScreen.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionScreen.kt new file mode 100644 index 0000000..d50aa87 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionScreen.kt @@ -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>).data + + if (!data.isNullOrEmpty()) { + var expanded by remember { mutableStateOf(false) } + var selectedCommodity by remember { + mutableStateOf( + 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) + ) + } +} \ No newline at end of file diff --git a/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionViewModel.kt b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionViewModel.kt new file mode 100644 index 0000000..f12d7e0 --- /dev/null +++ b/agrilinkvocpro/commodity_price_prediction_feature/src/main/java/com/syaroful/agrilinkvocpro/commodity_price_prediction_feature/presentation/prediction/PredictionViewModel.kt @@ -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.Idle + ) + val commodityData: StateFlow>> = _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") + } + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/FeatureBanner.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/FeatureBanner.kt new file mode 100644 index 0000000..43eef19 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/FeatureBanner.kt @@ -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", + ) + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/Data.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/Data.kt new file mode 100644 index 0000000..8139802 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/Data.kt @@ -0,0 +1,6 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.model + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +