add draft report

This commit is contained in:
Cutiful 2025-06-20 22:35:00 +07:00
parent 42d9ff8510
commit 5151379922
23 changed files with 1208 additions and 28 deletions

View File

@ -0,0 +1,2 @@
package com.syaroful.agrilinkvocpro.core.utils.extention

View File

@ -0,0 +1,7 @@
package com.syaroful.agrilinkvocpro.data.model
data class DhtGraphicDataResponse(
val `data`: Data?,
val message: String?,
val statusCode: Int?
)

View File

@ -0,0 +1,4 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
class DatePickerComponent {
}

View File

@ -0,0 +1,131 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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 com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.getValuesForSensor
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
@Composable
fun DetailContent(
modifier: Modifier = Modifier,
npkDataState: ResultState<NpkGraphicDataResponse?>,
currentData: SensorDataResponse?,
sensorId: String,
isRefreshing: MutableState<Boolean>,
currentDate: String,
) {
val selectedSensor = remember { mutableStateOf("Nitrogen") }
val options = listOf(
"Nitrogen",
"Pospor",
"Kalium",
"Suhu Tanah",
"PH Tanah",
"Kelembapan",
"Konduktivitas"
)
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
when (npkDataState) {
is ResultState.Loading -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.shimmerEffect()
)
}
is ResultState.Error -> {
isRefreshing.value = false
DefaultErrorComponent(
modifier = Modifier.padding(vertical = 20.dp),
label = "Oops!",
message = npkDataState.message,
painter = painterResource(id = R.drawable.mascot_confused)
)
}
is ResultState.Success -> {
isRefreshing.value = false
val dataList = npkDataState.data?.data?.get(sensorId).orEmpty()
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
}
Spacer(modifier = Modifier.height(16.dp))
val hours = dataList.mapNotNull { it.hour?.toInt() }
val values = getValuesForSensor(selectedSensor.value, dataList)
LineChart(
hours = hours,
values = values,
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.align(CenterHorizontally)
)
}
ResultState.Idle -> {}
}
Text(
"Grafik ini adalah grafik per hari ini tanggal $currentDate",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,
modifier = Modifier
.fillMaxWidth()
.alpha(0.5f)
.padding(vertical = 8.dp)
)
DetailSensorData(currentData = currentData, sensorId = sensorId)
}
}
@Composable
fun SensorSelector(
selectedSensor: String,
onSensorSelected: (String) -> Unit,
options: List<String>
) {
DynamicBottomSheet(
options = options,
) {
onSensorSelected
}
}

View File

@ -0,0 +1,118 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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 com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.getValuesForSensorNpk
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
@Composable
fun DetailNpkContent(
modifier: Modifier = Modifier,
npkDataState: ResultState<NpkGraphicDataResponse?>,
currentData: SensorDataResponse?,
sensorId: String,
isRefreshing: MutableState<Boolean>,
currentDate: String,
) {
val selectedSensor = remember { mutableStateOf("Nitrogen") }
val options = listOf(
"Nitrogen",
"Pospor",
"Kalium",
"Suhu Tanah",
"PH Tanah",
"Kelembapan",
"Konduktivitas"
)
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
when (npkDataState) {
is ResultState.Loading -> {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.shimmerEffect()
)
}
is ResultState.Error -> {
isRefreshing.value = false
DefaultErrorComponent(
modifier = Modifier.padding(vertical = 20.dp),
label = "Oops!",
message = npkDataState.message,
painter = painterResource(id = R.drawable.mascot_confused)
)
}
is ResultState.Success -> {
isRefreshing.value = false
val dataList = npkDataState.data?.data?.get(sensorId).orEmpty()
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
}
Spacer(modifier = Modifier.height(16.dp))
val hours = dataList.mapNotNull { it.hour?.toInt() }
val values = getValuesForSensorNpk(selectedSensor.value, dataList)
LineChart(
hours = hours,
values = values,
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.align(CenterHorizontally)
)
}
ResultState.Idle -> {}
}
Text(
"Grafik ini adalah grafik per hari ini tanggal $currentDate",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,
modifier = Modifier
.fillMaxWidth()
.alpha(0.5f)
.padding(vertical = 8.dp)
)
DetailSensorData(currentData = currentData, sensorId = sensorId)
}
}

View File

@ -0,0 +1,2 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
fun DynamicBottomSheet(
modifier: Modifier = Modifier,
options: List<String> = listOf("Pilih Item"),
width: Float = 0.5f,
onValueSelected: (String) -> Unit,
) {
val (showSheet, setShowSheet) = remember { mutableStateOf(false) }
@ -44,7 +45,7 @@ fun DynamicBottomSheet(
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f)
)
.padding(8.dp)
.fillMaxWidth(0.5f),
.fillMaxWidth(width),
verticalAlignment = Alignment.CenterVertically
) {

View File

@ -11,9 +11,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.asComposePath
@ -30,13 +32,17 @@ import kotlin.math.roundToInt
fun LineChart(
hours: List<Int> = emptyList(),
values: List<Double> = emptyList(),
horizontalValue: Double? = null,
modifier: Modifier = Modifier,
graphColor: Color = MainGreen
) {
Box(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(8.dp))
.padding(top = 24.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
.padding(top = 32.dp)
.padding(8.dp)
) {
val spacing = 100f
@ -62,29 +68,26 @@ fun LineChart(
Canvas(modifier = modifier) {
val spacePerHour = (size.width - spacing) / values.size
(0 until hours.size - 1 step 2).forEach { i ->
val value = values[i]
val hour = hours[i]
drawContext.canvas.nativeCanvas.apply {
drawText(
hour.toString(),
spacing + i * spacePerHour,
size.height - 5,
textPaint
)
}
hours.forEachIndexed { i, hour ->
drawContext.canvas.nativeCanvas.drawText(
hour.toString(),
spacing + i * spacePerHour,
size.height - 5,
textPaint
)
}
val priceStep = (upperValue - lowerValue) / 5f
val step = (upperValue - lowerValue) / 5f
(0..4).forEach { i ->
drawContext.canvas.nativeCanvas.apply {
drawText(
round(lowerValue + priceStep * i).toString(),
30f,
size.height - spacing - i * size.height / 5f,
textPaint
)
}
drawContext.canvas.nativeCanvas.drawText(
round(lowerValue + step * i).toString(),
30f,
size.height - spacing - i * size.height / 5f,
textPaint
)
}
var lastX = 0f
val strokePath = Path().apply {
val height = size.height
@ -105,6 +108,7 @@ fun LineChart(
quadraticTo(x1, y1, lastX, (y1 + y2) / 2f)
}
}
val fillPath = android.graphics.Path(strokePath.asAndroidPath())
.asComposePath()
.apply {
@ -112,6 +116,7 @@ fun LineChart(
lineTo(spacing, size.height - spacing)
close()
}
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
@ -130,21 +135,32 @@ fun LineChart(
cap = StrokeCap.Round
)
)
val labelPadding = 12.dp.toPx()
val labelPadding = 12.dp.toPx()
drawContext.canvas.nativeCanvas.drawText(
"Jumlah",
spacing / 2f,
labelPadding - 80,
labelPadding - 100,
textPaint
)
drawContext.canvas.nativeCanvas.drawText(
"Jam",
size.width - labelPadding,
size.width,
size.height - 4,
textPaint
)
horizontalValue?.let { value ->
val ratio = (value - lowerValue) / (upperValue - lowerValue)
val y = size.height - spacing - (ratio * size.height).toFloat()
drawLine(
color = Color.Red,
start = Offset(spacing, y),
end = Offset(size.width, y),
strokeWidth = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
)
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,157 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction
import android.graphics.Paint
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
import kotlin.math.round
import kotlin.math.roundToInt
@Composable
fun PriceChart(
day: List<Int> = emptyList(),
values: List<Double> = emptyList(),
horizontalValue: Double? = null,
modifier: Modifier = Modifier,
graphColor: Color = MainGreen
) {
Box(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
.padding(top = 32.dp, start = 16.dp)
.padding(8.dp)
) {
val spacing = 100f
val transparentGraphColor = remember {
graphColor.copy(alpha = 0.5f)
}
val upperValue = remember(values) {
(values.maxOrNull()?.plus(1))?.roundToInt() ?: 0
}
val lowerValue = remember(values) {
values.minOrNull()?.toInt() ?: 0
}
val density = LocalDensity.current
val isDarkMode = isSystemInDarkTheme()
val textPaint = remember(density) {
Paint().apply {
color =
if (isDarkMode) android.graphics.Color.WHITE else android.graphics.Color.BLACK
textAlign = Paint.Align.CENTER
textSize = density.run { 12.sp.toPx() }
}
}
Canvas(modifier = modifier) {
val spacePerDay = (size.width - spacing) / values.size
day.forEachIndexed { i, day ->
drawContext.canvas.nativeCanvas.drawText(
"h $day",
spacing + 40f + i * spacePerDay,
size.height - 5,
textPaint
)
}
val step = (upperValue - lowerValue) / 5f
(0..4).forEach { i ->
drawContext.canvas.nativeCanvas.drawText(
"Rp " + round(lowerValue + step * i).toInt().toString(),
30f,
size.height - spacing - i * size.height / 5f,
textPaint
)
}
var lastX = 0f
val strokePath = Path().apply {
val height = size.height
for (i in values.indices) {
val value = values[i]
val nextValue = values.getOrNull(i + 1) ?: values.last()
val leftRatio = (value - lowerValue) / (upperValue - lowerValue)
val rightRatio = (nextValue - lowerValue) / (upperValue - lowerValue)
val x1 = spacing + 40f + i * spacePerDay
val y1 = height - spacing - (leftRatio * height).toFloat()
val x2 = spacing + (i + 1) * spacePerDay
val y2 = height - spacing - (rightRatio * height).toFloat()
if (i == 0) {
moveTo(x1, y1)
}
lastX = (x1 + x2) / 2f
quadraticTo(x1, y1, lastX, (y1 + y2) / 2f)
}
}
val fillPath = android.graphics.Path(strokePath.asAndroidPath())
.asComposePath()
.apply {
lineTo(lastX, size.height - spacing)
lineTo(spacing, size.height - spacing)
close()
}
drawPath(
path = fillPath,
brush = Brush.verticalGradient(
colors = listOf(
transparentGraphColor,
Color.Transparent
),
endY = size.height - spacing
)
)
drawPath(
path = strokePath,
color = graphColor,
style = Stroke(
width = 3.dp.toPx(),
cap = StrokeCap.Round
)
)
val labelPadding = 12.dp.toPx()
drawContext.canvas.nativeCanvas.drawText(
"Harga",
spacing / 2f,
labelPadding - 100,
textPaint.apply { color = android.graphics.Color.GREEN }
)
horizontalValue?.let { value ->
val ratio = (value - lowerValue) / (upperValue - lowerValue)
val y = size.height - spacing - (ratio * size.height).toFloat()
drawLine(
color = Color.Red,
start = Offset(spacing, y),
end = Offset(size.width, y),
strokeWidth = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
)
}
}
}
}

View File

@ -0,0 +1,153 @@
package com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.presentation.screen.detail.DynamicBottomSheet
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PricePredictionScreen() {
val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry", "kentang", "Bayam")
val locationOptions = listOf("Malang", "Jombang", "Surabaya")
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Prediksi Harga Komoditas",
style = MaterialTheme.typography.titleMedium
)
}
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
)
}
) { innerPadding ->
Column(
modifier = Modifier.padding(innerPadding)
) {
Column(
modifier = Modifier
.padding(16.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(text = "Pilih Komoditas", style = MaterialTheme.typography.labelLarge)
DynamicBottomSheet(
width = 1f,
options = commodityOptions,
modifier = Modifier.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
) {
}
Text(text = "Pilih Lokasi", style = MaterialTheme.typography.labelLarge)
DynamicBottomSheet(
width = 1f,
options = locationOptions,
modifier = Modifier.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
) {
}
}
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Harga Hari Ini",
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.alpha(0.5f)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Rp 30.000 ",
style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.W400)
)
Text(
text = "↑ 2.5%",
style = MaterialTheme.typography.bodyLarge,
color = MainGreen
)
}
Text(
text = "Prediksi Besok",
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.alpha(0.5f)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Rp 28.000 ",
style = MaterialTheme.typography.headlineMedium,
color = MainGreen
)
Text(
text = "↓ 7,6%",
style = MaterialTheme.typography.bodyLarge,
color = Color.Red
)
}
PriceChart(
modifier = Modifier
.fillMaxWidth()
.height(140.dp),
day = listOf(1, 2, 3, 4, 5, 6, 7),
values = listOf(5000.0, 6000.0, 4000.0, 7000.0, 8500.0, 5450.0, 6400.0),
)
}
}
}
}

View File

@ -0,0 +1,80 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.presentation.recipe
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DynamicBottomSheet(
modifier: Modifier = Modifier,
width: Float = 0.5f,
options: List<String> = listOf("Pilih Item"),
onValueSelected: (String) -> Unit,
) {
val (showSheet, setShowSheet) = remember { mutableStateOf(false) }
val (selectedOption, setSelectedOption) = remember { mutableStateOf(options[0]) }
val sheetState = rememberModalBottomSheetState()
// Trigger UI to open the sheet
Row(
modifier = modifier
.clickable { setShowSheet(true) }
.border(
width = 1.dp,
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f)
)
.padding(8.dp)
.fillMaxWidth(width),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = selectedOption)
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.Filled.ArrowDropDown,
contentDescription = "Dropdown Arrow"
)
}
if (showSheet) {
ModalBottomSheet(
onDismissRequest = { setShowSheet(false) },
sheetState = sheetState
) {
options.forEach { option ->
Text(
text = option,
modifier = Modifier
.fillMaxWidth()
.clickable {
onValueSelected(option)
setSelectedOption(option)
setShowSheet(false)
}
.padding(16.dp)
)
}
}
}
}

View File

@ -0,0 +1,93 @@
package com.syaroful.agrilinkvocpro.growth_recipe_feature.presentation.recipe
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.growth_recipe_feature.R
import com.syaroful.agrilinkvocpro.presentation.screen.detail.LineChart
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GrowthRecipeScreen() {
val commodityOptions = listOf("Labu Kabocha", "Melon", "Strawberry")
val sensorOptions = listOf("Nitrogen", "Pospor", "Kalium")
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text("Optimal Growth Recipe", style = MaterialTheme.typography.titleMedium)
}
},
navigationIcon = {
IconButton(onClick = { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
)
}
) { innerPadding ->
Column(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, top = 16.dp)
.padding(innerPadding),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
DynamicBottomSheet(
options = commodityOptions,
) {
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.clip(RoundedCornerShape(8.dp))
) {
Image(
painter = painterResource(id = R.drawable.kabocha_pumpkin),
contentDescription = "commodity image",
contentScale = ContentScale.Crop
)
}
DynamicBottomSheet(
options = sensorOptions,
) {
}
LineChart(
modifier = Modifier.fillMaxWidth().height(140.dp),
hours = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
values = listOf(5.0, 6.0, 4.0, 7.0, 8.9, 5.0, 6.0, 4.9, 8.0, 7.0, 9.0),
horizontalValue = 6.0
)
}
}
}

View File

@ -0,0 +1,11 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun Long.toFormattedDate(): String {
val date = Date(this)
val format = SimpleDateFormat("dd MMM yyyy", Locale("id"))
return format.format(date)
}

View File

@ -0,0 +1,20 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface PlantDiagnosisDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDiagnosis(diagnosis: PlantDiagnosisEntity)
@Query("SELECT * FROM plant_diagnosis ORDER BY timestamp DESC")
fun getAllDiagnosis(): Flow<List<PlantDiagnosisEntity>>
@Query("SELECT * FROM plant_diagnosis WHERE id = :id")
fun getDiagnosisById(id: Int): Flow<PlantDiagnosisEntity?>
}

View File

@ -0,0 +1,20 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Suppress("DEPRECATED_ANNOTATION")
@Parcelize
@Entity(tableName = "plant_diagnosis")
data class PlantDiagnosisEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val diagnosis: String,
val cause: String,
val description: String,
val prevention: String,
val treatment: String,
val image: ByteArray,
val timestamp: Long = System.currentTimeMillis()
) : Parcelable

View File

@ -0,0 +1,19 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.dao.PlantDiagnosisDao
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import kotlinx.coroutines.flow.Flow
class PlantDiagnosisLocalRepository(
private val dao: PlantDiagnosisDao
) {
suspend fun saveDiagnosis(entity: PlantDiagnosisEntity) {
dao.insertDiagnosis(entity)
}
fun getAllDiagnosis(): Flow<List<PlantDiagnosisEntity>> = dao.getAllDiagnosis()
fun getDiagnosisById(id: Int): Flow<PlantDiagnosisEntity?> {
return dao.getDiagnosisById(id)
}
}

View File

@ -0,0 +1,186 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBitmap
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.detail.TextCardComponent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailHistoryScreen(
navController: NavHostController,
diagnosisId: Int,
viewModel: HistoryViewModel
) {
var plantDiagnosis by remember { mutableStateOf<PlantDiagnosisEntity?>(null) }
LaunchedEffect(diagnosisId) {
viewModel.getDiagnosisById(diagnosisId).collect {
plantDiagnosis = it
}
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Riwayat Deteksi",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
},
navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(innerPadding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(
10.dp,
Alignment.Top
)
) {
when (val data = plantDiagnosis) {
null -> {
DefaultErrorComponent(
label = "Waduh!",
message = "Data Tidak Ditemukan",
)
}
else -> {
val bitmap = try {
data.image.toBitmap().asImageBitmap()
} catch (e: Exception) {
null
}
bitmap?.let {
Card(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 180.dp),
shape = RoundedCornerShape(8.dp),
) {
Image(
modifier = Modifier.fillMaxSize(),
bitmap = it,
contentDescription = "Gambar Tanaman",
contentScale = ContentScale.Crop,
)
}
}
Box(
modifier = Modifier
.background(
color = Color.Red,
shape = RoundedCornerShape(4.dp)
)
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Text(
text = "Terkena Penyakit",
color = Color.White,
style = MaterialTheme.typography.labelMedium
)
}
Text(
text = "Penyakit Terdeteksi :",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFFF3D3D)
)
Text(
plantDiagnosis?.diagnosis ?: "-",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "Penyebab :",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFFFF3D3D)
)
Text(
plantDiagnosis?.cause ?: "-",
style = MaterialTheme.typography.titleSmall
)
TextCardComponent(
label = "Informasi Umum",
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = plantDiagnosis?.description ?: "-",
style = MaterialTheme.typography.bodyMedium
)
}
TextCardComponent(
label = "Pengobatan",
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = plantDiagnosis?.treatment ?: "-",
style = MaterialTheme.typography.bodyMedium
)
}
TextCardComponent(
label = "Pencegahan",
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = plantDiagnosis?.prevention ?: "-",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
}

View File

@ -0,0 +1,136 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.DefaultErrorComponent
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toBitmap
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.core.extention.toFormattedDate
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HistoryScreen(
navController: NavHostController,
viewModel: HistoryViewModel
) {
val diagnoses by viewModel.diagnoses.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Riwayat Deteksi",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
},
navigationIcon = {
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
}
) { innerPadding ->
when {
(diagnoses.isEmpty()) -> DefaultErrorComponent(
label = "Waduh!",
message = "Ga ada data sama sekali, coba tambahkan",
painter = painterResource(id = R.drawable.mascot_surprised)
)
else -> LazyColumn(
modifier = Modifier
.padding(innerPadding)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(diagnoses) { diagnosis ->
Box(
modifier = Modifier
.clickable {
navController.currentBackStackEntry?.savedStateHandle?.set(
"plantData",
diagnosis
)
navController.navigate("detail-history/${diagnosis.id}")
}
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(8.dp)
)
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(4.dp))
) {
Image(
bitmap = diagnosis.image.toBitmap().asImageBitmap(),
contentDescription = "Diagnosis Image"
)
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
"Diagnosis: ${diagnosis.diagnosis}",
style = MaterialTheme.typography.labelLarge
)
Spacer(modifier = Modifier.height(12.dp))
Text(
diagnosis.timestamp.toFormattedDate(),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.End
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,24 @@
package com.syaroful.agrilinkvocpro.plant_disease_detection_feature.presentation.history
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.local.entity.PlantDiagnosisEntity
import com.syaroful.agrilinkvocpro.plant_disease_detection_feature.data.repository.PlantDiagnosisLocalRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class HistoryViewModel(
private val repository: PlantDiagnosisLocalRepository
) : ViewModel() {
val diagnoses = repository.getAllDiagnosis().stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptyList()
)
fun getDiagnosisById(id: Int): Flow<PlantDiagnosisEntity?> {
return repository.getDiagnosisById(id)
}
}

Binary file not shown.