add draft report
This commit is contained in:
parent
42d9ff8510
commit
5151379922
|
|
@ -0,0 +1,2 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.core.utils.extention
|
||||||
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.data.model
|
||||||
|
|
||||||
|
data class DhtGraphicDataResponse(
|
||||||
|
val `data`: Data?,
|
||||||
|
val message: String?,
|
||||||
|
val statusCode: Int?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
|
||||||
|
|
||||||
|
class DatePickerComponent {
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp
|
||||||
fun DynamicBottomSheet(
|
fun DynamicBottomSheet(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
options: List<String> = listOf("Pilih Item"),
|
options: List<String> = listOf("Pilih Item"),
|
||||||
|
width: Float = 0.5f,
|
||||||
onValueSelected: (String) -> Unit,
|
onValueSelected: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val (showSheet, setShowSheet) = remember { mutableStateOf(false) }
|
val (showSheet, setShowSheet) = remember { mutableStateOf(false) }
|
||||||
|
|
@ -44,7 +45,7 @@ fun DynamicBottomSheet(
|
||||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f)
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.fillMaxWidth(0.5f),
|
.fillMaxWidth(width),
|
||||||
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
@ -11,9 +11,11 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.asAndroidPath
|
import androidx.compose.ui.graphics.asAndroidPath
|
||||||
import androidx.compose.ui.graphics.asComposePath
|
import androidx.compose.ui.graphics.asComposePath
|
||||||
|
|
@ -30,13 +32,17 @@ import kotlin.math.roundToInt
|
||||||
fun LineChart(
|
fun LineChart(
|
||||||
hours: List<Int> = emptyList(),
|
hours: List<Int> = emptyList(),
|
||||||
values: List<Double> = emptyList(),
|
values: List<Double> = emptyList(),
|
||||||
|
horizontalValue: Double? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
graphColor: Color = MainGreen
|
graphColor: Color = MainGreen
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(8.dp))
|
.background(
|
||||||
.padding(top = 24.dp)
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.padding(top = 32.dp)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
val spacing = 100f
|
val spacing = 100f
|
||||||
|
|
@ -62,29 +68,26 @@ fun LineChart(
|
||||||
|
|
||||||
Canvas(modifier = modifier) {
|
Canvas(modifier = modifier) {
|
||||||
val spacePerHour = (size.width - spacing) / values.size
|
val spacePerHour = (size.width - spacing) / values.size
|
||||||
(0 until hours.size - 1 step 2).forEach { i ->
|
|
||||||
val value = values[i]
|
hours.forEachIndexed { i, hour ->
|
||||||
val hour = hours[i]
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
hour.toString(),
|
||||||
drawText(
|
spacing + i * spacePerHour,
|
||||||
hour.toString(),
|
size.height - 5,
|
||||||
spacing + i * spacePerHour,
|
textPaint
|
||||||
size.height - 5,
|
)
|
||||||
textPaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val priceStep = (upperValue - lowerValue) / 5f
|
|
||||||
|
val step = (upperValue - lowerValue) / 5f
|
||||||
(0..4).forEach { i ->
|
(0..4).forEach { i ->
|
||||||
drawContext.canvas.nativeCanvas.apply {
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
drawText(
|
round(lowerValue + step * i).toString(),
|
||||||
round(lowerValue + priceStep * i).toString(),
|
30f,
|
||||||
30f,
|
size.height - spacing - i * size.height / 5f,
|
||||||
size.height - spacing - i * size.height / 5f,
|
textPaint
|
||||||
textPaint
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastX = 0f
|
var lastX = 0f
|
||||||
val strokePath = Path().apply {
|
val strokePath = Path().apply {
|
||||||
val height = size.height
|
val height = size.height
|
||||||
|
|
@ -105,6 +108,7 @@ fun LineChart(
|
||||||
quadraticTo(x1, y1, lastX, (y1 + y2) / 2f)
|
quadraticTo(x1, y1, lastX, (y1 + y2) / 2f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val fillPath = android.graphics.Path(strokePath.asAndroidPath())
|
val fillPath = android.graphics.Path(strokePath.asAndroidPath())
|
||||||
.asComposePath()
|
.asComposePath()
|
||||||
.apply {
|
.apply {
|
||||||
|
|
@ -112,6 +116,7 @@ fun LineChart(
|
||||||
lineTo(spacing, size.height - spacing)
|
lineTo(spacing, size.height - spacing)
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPath(
|
drawPath(
|
||||||
path = fillPath,
|
path = fillPath,
|
||||||
brush = Brush.verticalGradient(
|
brush = Brush.verticalGradient(
|
||||||
|
|
@ -130,21 +135,32 @@ fun LineChart(
|
||||||
cap = StrokeCap.Round
|
cap = StrokeCap.Round
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val labelPadding = 12.dp.toPx()
|
|
||||||
|
|
||||||
|
val labelPadding = 12.dp.toPx()
|
||||||
drawContext.canvas.nativeCanvas.drawText(
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
"Jumlah",
|
"Jumlah",
|
||||||
spacing / 2f,
|
spacing / 2f,
|
||||||
labelPadding - 80,
|
labelPadding - 100,
|
||||||
textPaint
|
textPaint
|
||||||
)
|
)
|
||||||
|
|
||||||
drawContext.canvas.nativeCanvas.drawText(
|
drawContext.canvas.nativeCanvas.drawText(
|
||||||
"Jam",
|
"Jam",
|
||||||
size.width - labelPadding,
|
size.width,
|
||||||
size.height - 4,
|
size.height - 4,
|
||||||
textPaint
|
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))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
agrilinkvocpro/app/src/main/res/drawable/solar_user.png
Normal file
BIN
agrilinkvocpro/app/src/main/res/drawable/solar_user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
agrilinkvocpro/app/src/main/res/drawable/solar_user_circle.png
Normal file
BIN
agrilinkvocpro/app/src/main/res/drawable/solar_user_circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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?>
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
skripsi/Draft Skripsi - Muhamad Syaroful Anam.docx
Normal file
BIN
skripsi/Draft Skripsi - Muhamad Syaroful Anam.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user