feat: add cluster screen for displaying recommendation

This commit introduces the `ClusterScreen.kt` composable, which displays plant care recommendations.

The screen includes:
- A top app bar with the title "Rekomendasi Perawatan".
- Pull-to-refresh functionality to update recommendations.
- Display of an image based on the cluster number (1, 2, or 3).
- "Diperbarui Pada" (Updated On) timestamp, formatted to Indonesian locale.
- A "Rekomendasi" section showing general advice and specific care instructions.
- An image of a plant.
- `ListItemClustering` to display humidity, temperature, and pH values.
- Loading and error states handling.

A private `formatDate` utility function is included to format timestamps to "dd MMMM yyyy, HH:mm" in Indonesian.
This commit is contained in:
Cutiful 2025-07-10 18:17:55 +07:00
parent acc732871f
commit e274873e43

View File

@ -0,0 +1,222 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.presentation.recomendation
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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.growth_recipe_feature.R
import com.syaroful.agrilinkvocpro.growth_recipe_feature.core.component.ListItemClustering
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import org.koin.androidx.compose.koinViewModel
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ClusterScreen(
navController: NavController,
viewModel: ClusterViewModel = koinViewModel()
) {
val state by viewModel.state.collectAsState()
val isRefreshing = remember { mutableStateOf(false) }
LaunchedEffect(state) {
if (state == ResultState.Idle) {
viewModel.getRecommendation()
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
) {
Text(
"Rekomendasi Perawatan",
style = MaterialTheme.typography.titleMedium
)
}
},
navigationIcon = {
}
)
}
) { innerPadding ->
PullToRefreshBox(
isRefreshing = isRefreshing.value,
onRefresh = {
isRefreshing.value = true
viewModel.getRecommendation()
},
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(innerPadding)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
when (state) {
is ResultState.Loading -> {
CircularProgressIndicator(
color = MainGreen
)
}
is ResultState.Error -> {
isRefreshing.value = false
DefaultErrorComponent(
label = "Oops!",
message = (state as ResultState.Error).message
)
}
is ResultState.Success -> {
isRefreshing.value = false
val data = (state as ResultState.Success).data
Image(
modifier = Modifier.fillMaxWidth(0.7f),
painter = painterResource(
id =
when (data?.cluster) {
1 -> R.drawable.cluster_1
2 -> R.drawable.cluster_2
3 -> R.drawable.cluster_3
else -> R.drawable.cluster_2
}
),
contentScale = ContentScale.FillWidth,
contentDescription = "Recommendation action"
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
"Diperbarui Pada",
modifier = Modifier
.alpha(0.6f),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Start
)
Text(
formatDate(data?.createdAt ?: "-"),
style = MaterialTheme.typography.bodySmall.copy(fontStyle = FontStyle.Italic),
textAlign = TextAlign.Start
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.fillMaxWidth(0.7f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
"Rekomendasi:",
modifier = Modifier.alpha(0.6f),
style = MaterialTheme.typography.labelMedium
)
Text(
data?.rekomendasi ?: "-",
style = MaterialTheme.typography.headlineSmall
)
Text(
"Rekomendasi Perawatan:",
modifier = Modifier.alpha(0.6f),
style = MaterialTheme.typography.labelMedium
)
Text(
data?.rekomendasiPerawatan ?: "-",
style = MaterialTheme.typography.titleMedium
)
}
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.vegeatatif),
contentDescription = "plant"
)
}
}
ListItemClustering(
humidity = data?.humidity ?: 0.0,
temp = data?.temperature ?: 0.0,
pH = data?.ph ?: 0.0
)
}
else -> {}
}
}
}
}
}
private fun formatDate(time: String): String {
val inputFormatter = DateTimeFormatter.RFC_1123_DATE_TIME
val zonedDateTime = ZonedDateTime.parse(time, inputFormatter)
.withZoneSameInstant(ZoneId.of("Asia/Jakarta"))
val dateFormatter = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale("id", "ID"))
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm")
val date = zonedDateTime.format(dateFormatter)
val jam = zonedDateTime.format(timeFormatter)
return "$date, $jam"
}