feat: implement detail screen for NPK and DHT sensors

This commit is contained in:
Cutiful 2025-06-29 07:53:51 +07:00
parent f48a96c96c
commit c2aa13e4ce
29 changed files with 511 additions and 375 deletions

View File

@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-06-11T17:05:21.600763700Z">
<DropdownSelection timestamp="2025-06-20T02:45:08.711114800Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\syaro\.android\avd\Pixel_8_Pro_API_31.avd" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=RRCX401ZEKL" />
</handle>
</Target>
</DropdownSelection>

View File

@ -7,6 +7,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -16,7 +17,6 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
@ -24,14 +24,14 @@ import androidx.compose.ui.unit.dp
fun Modifier.shimmerEffect(
shimmerColors: List<Color> = listOf(
Color.LightGray.copy(alpha = 0.6f),
Color.LightGray.copy(alpha = 0.2f),
Color.LightGray.copy(alpha = 0.6f)
),
shimmerDuration: Int = 1000,
cornerRadius: Dp = 16.dp
cornerRadius: Dp = 16.dp
): Modifier = composed {
val shimmerColors = listOf(
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f),
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)
)
var size by remember { mutableStateOf(IntSize.Zero) }
val transition = rememberInfiniteTransition()

View File

@ -7,4 +7,9 @@ import java.util.Locale
fun Date.toFormattedString(): String {
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
return sdf.format(this)
}
fun Date.toDDMMYYYYString(): String {
val sdf = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
return sdf.format(this)
}

View File

@ -1,2 +1,33 @@
package com.syaroful.agrilinkvocpro.core.utils.extention
import com.syaroful.agrilinkvocpro.data.model.DhtWithHour
import com.syaroful.agrilinkvocpro.data.model.NpkWithHour
import java.text.SimpleDateFormat
import java.util.Date
fun getValuesForSensorNpk(sensor: String, data: List<NpkWithHour>): List<Double> {
return when (sensor) {
"Nitrogen" -> data.mapNotNull { it.soilNitrogen?.toDouble() }
"Pospor" -> data.mapNotNull { it.soilPhosphorus?.toDouble() }
"Kalium" -> data.mapNotNull { it.soilPotassium?.toDouble() }
"Suhu Tanah" -> data.mapNotNull { it.soilTemperature?.toDouble() }
"PH Tanah" -> data.mapNotNull { it.soilPh?.toDouble() }
"Kelembapan" -> data.mapNotNull { it.soilHumidity?.toDouble() }
"Konduktivitas" -> data.mapNotNull { it.soilConductivity?.toDouble() }
else -> emptyList()
}
}
fun getValuesForSensorDht(sensor: String, data: List<DhtWithHour>): List<Double> {
return when (sensor) {
"Kelembaban Udara" -> data.mapNotNull { it.viciHumidity?.toDouble() }
"Suhu Ruang" -> data.mapNotNull { it.viciTemperature?.toDouble() }
"Intensitas Cahaya" -> data.mapNotNull { it.viciLuminosity?.toDouble() }
else -> emptyList()
}
}
fun getCurrentDate(): String {
val sdf = SimpleDateFormat("dd MMMM yyyy", java.util.Locale.getDefault())
return sdf.format(Date())
}

View File

@ -1,7 +1,16 @@
package com.syaroful.agrilinkvocpro.data.model
data class DhtGraphicDataResponse(
val `data`: Data?,
val data: Map<String, List<DhtWithHour>>?,
val message: String?,
val statusCode: Int?
)
data class DhtWithHour(
val date: String?,
val hour: Int?,
val viciHumidity: Number?,
val viciLuminosity: Number?,
val viciTemperature: Number?
)

View File

@ -9,11 +9,11 @@ data class NpkGraphicDataResponse(
data class NpkWithHour(
val hour: Int?,
val date: String?,
val soiltemperature: Number?,
val soilhumidity: Number?,
val soilconductivity: Number?,
val soilph: Number?,
val soilnitrogen: Number?,
val soilphosphorus: Number?,
val soilpotassium: Number?,
val soilTemperature: Number?,
val soilHumidity: Number?,
val soilConductivity: Number?,
val soilPh: Number?,
val soilNitrogen: Number?,
val soilPhosphorus: Number?,
val soilPotassium: Number?,
)

View File

@ -13,17 +13,17 @@ data class SensorData(
)
data class Dht(
val vicitemperature: Number?,
val vicihumidity: Number?,
val viciluminosity: Number?
val viciTemperature: Number?,
val viciHumidity: Number?,
val viciLuminosity: Number?
)
data class Npk(
val soiltemperature: Number?,
val soilhumidity: Number?,
val soilconductivity: Number?,
val soilph: Number?,
val soilnitrogen: Number?,
val soilphosphorus: Number?,
val soilpotassium: Number?
val soilTemperature: Number?,
val soilHumidity: Number?,
val soilConductivity: Number?,
val soilPh: Number?,
val soilNitrogen: Number?,
val soilPhosphorus: Number?,
val soilPotassium: Number?
)

View File

@ -1,5 +1,6 @@
package com.syaroful.agrilinkvocpro.data.network
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
@ -13,23 +14,24 @@ import retrofit2.http.Query
data class LoginRequest(
val username: String,
val password: String
val password: String,
val remember_me: Boolean = true
)
interface ApiService {
@POST("auth/login")
suspend fun login(
@Header("Authorization") authHeader: String,
@Body body: LoginRequest
@Body body: LoginRequest,
): Response<LoginResponse>
@GET("api/sensor/getLatest")
@GET("api/sensor-readings/latest")
suspend fun getLatestSensorData(
@Header("Authorization") authHeader: String
): Response<SensorDataResponse>
@GET("api/sensor/getData")
suspend fun getNpk1DataSensor(
@GET("api/sensor-readings/search")
suspend fun getNpkDataSensor(
@Header("Authorization") authHeader: String,
@Query("range[start]") startDate: String,
@Query("range[end]") endDate: String,
@ -37,4 +39,13 @@ interface ApiService {
@Query("sensor") sensor: String
): Response<NpkGraphicDataResponse>
@GET("api/sensor-readings/search")
suspend fun getDhtDataSensor(
@Header("Authorization") authHeader: String,
@Query("range[start]") startDate: String,
@Query("range[end]") endDate: String,
@Query("range[time_range]") timeRange: String = "HOURLY",
@Query("sensor") sensor: String
): Response<DhtGraphicDataResponse>
}

View File

@ -11,6 +11,6 @@ class AuthRepository(
suspend fun login(username: String, password: String): Response<LoginResponse> {
val credential = okhttp3.Credentials.basic(username, password)
val requestBody = LoginRequest(username, password)
return apiService.login(credential, requestBody)
return apiService.login(authHeader = credential, body = requestBody)
}
}

View File

@ -1,5 +1,6 @@
package com.syaroful.agrilinkvocpro.data.repository
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.network.ApiService
@ -25,6 +26,16 @@ class SensorDataRepository(private val apiService: ApiService) {
timeRange: String = "HOURLY",
sensor: String
): Response<NpkGraphicDataResponse> {
return apiService.getNpk1DataSensor(authHeader, startDate, endDate, timeRange, sensor)
return apiService.getNpkDataSensor(authHeader, startDate, endDate, timeRange, sensor)
}
suspend fun getDhtDataSensor(
authHeader: String,
startDate: String,
endDate: String,
timeRange: String = "HOURLY",
sensor: String
): Response<DhtGraphicDataResponse> {
return apiService.getDhtDataSensor(authHeader, startDate, endDate, timeRange, sensor)
}
}

View File

@ -5,10 +5,12 @@ import okhttp3.OkHttpClient
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
val networkModule = module {
single {
OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.build()
}

View File

@ -48,7 +48,7 @@ fun SetupNavigation(
},
onNavigateToRegister = {
navController.navigate("register") {
popUpTo("login") { inclusive = true }
popUpTo("register") { inclusive = true }
}
}
)

View File

@ -1,14 +1,8 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
import androidx.compose.foundation.layout.Arrangement
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.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@ -22,19 +16,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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.getCurrentDate
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DetailDhtContent
import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.DetailNpkContent
import org.koin.androidx.compose.koinViewModel
import java.text.SimpleDateFormat
import java.util.Date
@ -45,185 +32,69 @@ fun DetailScreen(
modifier: Modifier = Modifier,
detailViewModel: DetailViewModel = koinViewModel(),
) {
val date = remember { mutableStateOf(Date().toFormattedString()) }
LaunchedEffect(sensorId) {
detailViewModel.fetchNpkData(sensorId)
}
val currentData = detailViewModel.currentSensorData
val sdf = SimpleDateFormat("dd MMMM yyyy", java.util.Locale.getDefault())
val currentDate = sdf.format(Date())
val options = listOf(
"Nitrogen",
"Pospor",
"Kalium",
"Suhu Tanah",
"PH Tanah",
"Kelembapan",
"Konduktivitas"
)
Scaffold(
topBar = {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text(
"Detail Grafik",
style = MaterialTheme.typography.titleMedium
)
}
},
)
},
) { innerPadding ->
val npkDataState by detailViewModel.npkDataState.collectAsState()
val isRefreshing = remember { mutableStateOf(false) }
PullToRefreshBox(
isRefreshing = isRefreshing.value,
onRefresh = {
isRefreshing.value = true
detailViewModel.fetchNpkData(sensorId)
},
) {
Column(
modifier = Modifier
.padding(innerPadding)
.padding(top = 16.dp)
.padding(horizontal = 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 as ResultState.Error).message,
painter = painterResource(id = R.drawable.mascot_confused)
)
}
ResultState.Idle -> {
}
is ResultState.Success -> {
isRefreshing.value = false
val selectedSensor = remember { mutableStateOf(options[0]) }
val result = (npkDataState as ResultState.Success).data
val npkData = result?.data?.get(sensorId)
val hours = npkData?.filter { it.hour != null }?.map { it.hour!!.toInt() }
?: emptyList()
val values = when (selectedSensor.value) {
"Nitrogen" -> npkData?.mapNotNull { it.soilnitrogen?.toDouble() } ?: emptyList()
"Pospor" -> npkData?.mapNotNull { it.soilphosphorus?.toDouble() } ?: emptyList()
"Kalium" -> npkData?.mapNotNull { it.soilpotassium?.toDouble() } ?: emptyList()
"Suhu Tanah" -> npkData?.mapNotNull { it.soiltemperature?.toDouble() } ?: emptyList()
"PH Tanah" -> npkData?.mapNotNull { it.soilph?.toDouble() } ?: emptyList()
"Kelembapan" -> npkData?.mapNotNull { it.soilhumidity?.toDouble() } ?: emptyList()
"Konduktivitas" -> npkData?.mapNotNull { it.soilconductivity?.toDouble() } ?: emptyList()
else -> emptyList()
}
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
}
Spacer(modifier = Modifier.height(16.dp))
LineChart(
hours = hours,
values = values,
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.align(CenterHorizontally)
)
}
}
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)
)
if (currentData != null) {
val dataSensor = when (sensorId) {
"npk1" -> currentData.data?.npk1
"npk2" -> currentData.data?.npk2
else -> null
}
Column(
verticalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.Top
)
) {
DataSensorBar(
label = "Nitrogen",
value = dataSensor?.soilnitrogen ?: 0,
painter = painterResource(id = R.drawable.npk), // Assuming the max value is 255
percentage = (dataSensor?.soilnitrogen?.toFloat() ?: 0f) / 50f
)
DataSensorBar(
label = "Pospor",
value = dataSensor?.soilphosphorus ?: 0,
painter = painterResource(id = R.drawable.npk), // Assuming the max value is 255
percentage = (dataSensor?.soilphosphorus?.toFloat() ?: 0f) / 255f
)
DataSensorBar(
label = "Kalium",
value = dataSensor?.soilpotassium ?: 0,
painter = painterResource(id = R.drawable.npk), // Assuming the max value is 255
percentage = (dataSensor?.soilpotassium?.toFloat() ?: 0f) / 255f
)
DataSensorBar(
label = "Kelembaban",
value = dataSensor?.soilhumidity ?: 0,
painter = painterResource(id = R.drawable.soil_humidity), // Assuming the max value is 100
percentage = (dataSensor?.soilhumidity?.toFloat() ?: 0f) / 100f
)
DataSensorBar(
label = "Suhu Tanah",
value = dataSensor?.soiltemperature ?: 0,
painter = painterResource(id = R.drawable.soil_temperature), // Assuming the max value is 50 (adjust as needed)
percentage = (dataSensor?.soiltemperature?.toFloat() ?: 0f) / 50f
)
DataSensorBar(
label = "PH Tanah",
value = dataSensor?.soilph ?: 0,
painter = painterResource(id = R.drawable.meters), // Assuming the max value is 14
percentage = (dataSensor?.soilph?.toFloat() ?: 0f) / 14f
)
DataSensorBar(
label = "Konduktivitas",
value = dataSensor?.soilconductivity ?: 0,
painter = painterResource(id = R.drawable.electricity),
percentage = (dataSensor?.soilconductivity?.toFloat() ?: 0f) / 200f
)
}
} else {
Text("data Sesnor kosong")
}
}
if (sensorId == "dht") {
detailViewModel.fetchDhtData(date = date.value, sensor = sensorId)
} else {
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
}
}
val npkDataState by detailViewModel.npkDataState.collectAsState()
val dhtDataState by detailViewModel.dhtDataState.collectAsState()
val currentData = detailViewModel.currentSensorData
val isRefreshing = remember { mutableStateOf(false) }
val currentDate = remember { mutableStateOf(getCurrentDate()) }
Scaffold(
topBar = { DetailTopBar() }) { innerPadding ->
PullToRefreshBox(
isRefreshing = isRefreshing.value, onRefresh = {
isRefreshing.value = true
if (sensorId == "dht") {
detailViewModel.fetchDhtData(date = date.value, sensor = sensorId)
} else {
detailViewModel.fetchNpkData(date = date.value, sensor = sensorId)
}
}) {
if (sensorId == "dht") DetailDhtContent(
modifier = Modifier.padding(innerPadding),
viewModel = detailViewModel,
dhtDataState = dhtDataState,
currentData = currentData,
sensorId = sensorId,
isRefreshing = isRefreshing,
date = date,
currentDate = currentDate
)
else DetailNpkContent(
modifier = Modifier.padding(innerPadding),
viewModel = detailViewModel,
npkDataState = npkDataState,
currentData = currentData,
sensorId = sensorId,
isRefreshing = isRefreshing,
date = date,
currentDate = currentDate
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailTopBar() {
TopAppBar(
title = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Text("Detail Grafik", style = MaterialTheme.typography.titleMedium)
}
})
}

View File

@ -7,6 +7,7 @@ import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.core.utils.extention.mapToUserFriendlyError
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import com.syaroful.agrilinkvocpro.data.UserPreferences
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.NpkGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.data.repository.SensorDataRepository
@ -32,22 +33,25 @@ class DetailViewModel(
private val _npkDataState = MutableStateFlow<ResultState<NpkGraphicDataResponse>>(ResultState.Idle)
val npkDataState: StateFlow<ResultState<NpkGraphicDataResponse>> = _npkDataState.asStateFlow()
private val _dhtDataState = MutableStateFlow<ResultState<DhtGraphicDataResponse>>(ResultState.Idle)
val dhtDataState: StateFlow<ResultState<DhtGraphicDataResponse>> = _dhtDataState.asStateFlow()
private val today = Date()
fun fetchNpkData(
date: String = today.toFormattedString(),
sensor: String,
) {
viewModelScope.launch {
_npkDataState.value = ResultState.Loading
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
val formattedToday = today.toFormattedString()
try {
delay(2000L)
delay(1000L)
val response = sensorDataRepository.getNpkDataSensor(
authHeader = authHeader,
startDate = formattedToday,
endDate = formattedToday,
startDate = date,
endDate = date,
timeRange = "HOURLY",
sensor = sensor
)
@ -67,4 +71,38 @@ class DetailViewModel(
}
}
}
fun fetchDhtData(
date: String = today.toFormattedString(),
sensor: String,
) {
viewModelScope.launch {
_dhtDataState.value = ResultState.Loading
val token = userPreferences.tokenFlow.first()
val authHeader = "Bearer $token"
try {
delay(1000L)
val response = sensorDataRepository.getDhtDataSensor(
authHeader = authHeader,
startDate = date,
endDate = date,
timeRange = "HOURLY",
sensor = sensor
)
if (response.isSuccessful) {
response.body()?.let { body ->
_dhtDataState.value = ResultState.Success(body)
} ?: run {
_dhtDataState.value = ResultState.Error("Data tidak ditemukan")
}
} else {
_dhtDataState.value = ResultState.Error("Error: ${response.code()} - ${response.message()}")
}
} catch (e: Exception) {
val errorMessage = mapToUserFriendlyError(e)
_dhtDataState.value = ResultState.Error(errorMessage)
Log.d(TAG, "Failed to fetch data: ${e.message}")
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.background

View File

@ -1,4 +1,50 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
class DatePickerComponent {
}
import android.app.DatePickerDialog
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.utils.extention.toFormattedString
import java.util.Calendar
@Composable
fun DatePickerComponent(
modifier: Modifier = Modifier,
onDateSelected: (String) -> Unit,
) {
val context = LocalContext.current
val calendar = remember { Calendar.getInstance() }
IconButton(
onClick = {
val datePickerDialog = DatePickerDialog(
context,
{ _, year, month, dayOfMonth ->
calendar.set(year, month, dayOfMonth)
val selectedDate = calendar.time.toFormattedString()
onDateSelected(selectedDate)
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
)
datePickerDialog.show()
},
modifier = modifier
) {
Icon(
painter = painterResource(id = R.drawable.ic_calendar),
modifier = Modifier.size(24.dp),
contentDescription = "Pilih Tanggal"
)
}
}

View File

@ -1,7 +1,9 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
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
@ -25,36 +27,36 @@ 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.core.utils.extention.getValuesForSensorDht
import com.syaroful.agrilinkvocpro.data.model.DhtGraphicDataResponse
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
import com.syaroful.agrilinkvocpro.presentation.screen.detail.DetailViewModel
@Composable
fun DetailContent(
fun DetailDhtContent(
modifier: Modifier = Modifier,
npkDataState: ResultState<NpkGraphicDataResponse?>,
viewModel: DetailViewModel,
dhtDataState: ResultState<DhtGraphicDataResponse?>,
currentData: SensorDataResponse?,
sensorId: String,
isRefreshing: MutableState<Boolean>,
currentDate: String,
date: MutableState<String>,
currentDate: MutableState<String>,
) {
val selectedSensor = remember { mutableStateOf("Nitrogen") }
val options = listOf(
"Nitrogen",
"Pospor",
"Kalium",
"Suhu Tanah",
"PH Tanah",
"Kelembapan",
"Konduktivitas"
"Kelembaban Udara",
"Suhu Ruang",
"Intensitas Cahaya",
)
val selectedSensor = remember { mutableStateOf("Kelembaban Udara") }
Column(
modifier = modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
when (npkDataState) {
when (dhtDataState) {
is ResultState.Loading -> {
Box(
modifier = Modifier
@ -69,25 +71,35 @@ fun DetailContent(
DefaultErrorComponent(
modifier = Modifier.padding(vertical = 20.dp),
label = "Oops!",
message = npkDataState.message,
message = dhtDataState.message,
painter = painterResource(id = R.drawable.mascot_confused)
)
}
is ResultState.Success -> {
isRefreshing.value = false
val dataList = npkDataState.data?.data?.get(sensorId).orEmpty()
val dataList = dhtDataState.data?.data?.get(sensorId).orEmpty()
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
}
DatePickerComponent() {
date.value = it
viewModel.fetchDhtData(date = date.value, sensor = sensorId)
currentDate.value = date.value
}
}
Spacer(modifier = Modifier.height(16.dp))
val hours = dataList.mapNotNull { it.hour?.toInt() }
val values = getValuesForSensor(selectedSensor.value, dataList)
val values = getValuesForSensorDht(selectedSensor.value, dataList)
LineChart(
hours = hours,
@ -103,7 +115,7 @@ fun DetailContent(
}
Text(
"Grafik ini adalah grafik per hari ini tanggal $currentDate",
"Grafik ini adalah grafik per tanggal ${currentDate.value}",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,
@ -115,17 +127,4 @@ fun DetailContent(
DetailSensorData(currentData = currentData, sensorId = sensorId)
}
}
@Composable
fun SensorSelector(
selectedSensor: String,
onSensorSelected: (String) -> Unit,
options: List<String>
) {
DynamicBottomSheet(
options = options,
) {
onSensorSelected
}
}
}

View File

@ -1,7 +1,9 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
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
@ -28,15 +30,18 @@ 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
import com.syaroful.agrilinkvocpro.presentation.screen.detail.DetailViewModel
@Composable
fun DetailNpkContent(
modifier: Modifier = Modifier,
viewModel: DetailViewModel,
npkDataState: ResultState<NpkGraphicDataResponse?>,
currentData: SensorDataResponse?,
sensorId: String,
isRefreshing: MutableState<Boolean>,
currentDate: String,
date: MutableState<String>,
currentDate: MutableState<String>,
) {
val selectedSensor = remember { mutableStateOf("Nitrogen") }
val options = listOf(
@ -78,10 +83,20 @@ fun DetailNpkContent(
isRefreshing.value = false
val dataList = npkDataState.data?.data?.get(sensorId).orEmpty()
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
DynamicBottomSheet(
options = options
) { selected ->
selectedSensor.value = selected
}
DatePickerComponent() {
date.value = it
viewModel.fetchNpkData(date = date.value, sensor = sensorId)
currentDate.value = date.value
}
}
Spacer(modifier = Modifier.height(16.dp))
@ -103,7 +118,7 @@ fun DetailNpkContent(
}
Text(
"Grafik ini adalah grafik per hari ini tanggal $currentDate",
"Grafik ini adalah grafik per tanggal ${currentDate.value}",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic,

View File

@ -1,2 +1,119 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.data.model.Dht
import com.syaroful.agrilinkvocpro.data.model.Npk
import com.syaroful.agrilinkvocpro.data.model.SensorDataResponse
@Composable
fun DetailSensorData(
currentData: SensorDataResponse?,
sensorId: String
) {
val dataSensor = when (sensorId) {
"npk1" -> currentData?.data?.npk1
"npk2" -> currentData?.data?.npk2
"dht" -> currentData?.data?.dht
else -> null
}
if (dataSensor == null) {
Text("Data sensor kosong")
return
}
when (dataSensor) {
is Npk -> {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SensorBar(
label = "Nitrogen",
value = dataSensor.soilNitrogen,
max = 50f,
icon = R.drawable.npk
)
SensorBar(
label = "Pospor",
value = dataSensor.soilPhosphorus,
max = 255f,
icon = R.drawable.npk
)
SensorBar(
label = "Kalium",
value = dataSensor.soilPotassium,
max = 255f,
icon = R.drawable.npk
)
SensorBar(
label = "Kelembaban",
value = dataSensor.soilHumidity,
max = 100f,
icon = R.drawable.soil_humidity
)
SensorBar(
label = "Suhu Tanah",
value = dataSensor.soilTemperature,
max = 50f,
icon = R.drawable.soil_temperature
)
SensorBar(
label = "PH Tanah",
value = dataSensor.soilPh,
max = 14f,
icon = R.drawable.meters
)
SensorBar(
label = "Konduktivitas",
value = dataSensor.soilConductivity,
max = 200f,
icon = R.drawable.electricity
)
}
}
is Dht -> {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SensorBar(
label = "Kelembaban Udara",
value = dataSensor.viciHumidity,
max = 50f,
icon = R.drawable.humidity
)
SensorBar(
label = "Intensitas Cahaya",
value = dataSensor.viciLuminosity,
max = 255f,
icon = R.drawable.luminosity
)
SensorBar(
label = "Suhu Udara",
value = dataSensor.viciTemperature,
max = 255f,
icon = R.drawable.weather
)
}
}
else -> {}
}
}
@Composable
fun SensorBar(
label: String,
value: Number?,
max: Float,
icon: Int
) {
DataSensorBar(
label = label,
value = value ?: 0,
percentage = (value?.toFloat() ?: 0f) / max,
painter = painterResource(id = icon)
)
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable

View File

@ -1,4 +1,4 @@
package com.syaroful.agrilinkvocpro.presentation.screen.detail
package com.syaroful.agrilinkvocpro.presentation.screen.detail.component
import android.graphics.Paint
import androidx.compose.foundation.Canvas

View File

@ -129,9 +129,9 @@ fun HomeScreen(
},
betNUmber = 1,
commodity = "Kabocha",
nitrogenValue = sensorData.npk1?.soilnitrogen.toString(),
phosphorValue = sensorData.npk1?.soilphosphorus.toString(),
potassiumValue = sensorData.npk1?.soilpotassium.toString(),
nitrogenValue = sensorData.npk1?.soilNitrogen.toString(),
phosphorValue = sensorData.npk1?.soilPhosphorus.toString(),
potassiumValue = sensorData.npk1?.soilPotassium.toString(),
commodityImage = painterResource(id = R.drawable.kabocha)
)
BetDataComponent(
@ -140,9 +140,9 @@ fun HomeScreen(
},
betNUmber = 2,
commodity = "Melon",
nitrogenValue = sensorData.npk2?.soilnitrogen.toString(),
phosphorValue = sensorData.npk2?.soilphosphorus.toString(),
potassiumValue = sensorData.npk2?.soilpotassium.toString(),
nitrogenValue = sensorData.npk2?.soilNitrogen.toString(),
phosphorValue = sensorData.npk2?.soilPhosphorus.toString(),
potassiumValue = sensorData.npk2?.soilPotassium.toString(),
commodityImage = painterResource(id = R.drawable.melon)
)
}
@ -150,11 +150,11 @@ fun HomeScreen(
"DHT" -> {
DhtDataComponent(
onClick = {
navController.navigate("detail-screen/dht")
},
temperatureValue = sensorData.dht?.vicitemperature.toString(),
humidityValue = sensorData.dht?.vicihumidity.toString(),
luminosityValue = sensorData.dht?.viciluminosity.toString(),
temperatureValue = sensorData.dht?.viciTemperature.toString(),
humidityValue = sensorData.dht?.viciHumidity.toString(),
luminosityValue = sensorData.dht?.viciLuminosity.toString(),
)
}

View File

@ -5,11 +5,13 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -47,8 +49,10 @@ fun RegisterScreen(
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(innerPadding),
.padding(innerPadding)
.imePadding(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Image(
@ -69,14 +73,32 @@ fun RegisterScreen(
style = textTheme.titleSmall.copy(color = DarkGrey),
textAlign = TextAlign.Center
)
AppTextField(
hint = "Name",
keyboardType = KeyboardType.Email,
leadingIcon = painterResource(
R.drawable.solar_user_circle
),
value = name,
onValueChange = { name = it }
)
AppTextField(
hint = "Username",
keyboardType = KeyboardType.Email,
leadingIcon = painterResource(
R.drawable.solar_user
),
value = username,
onValueChange = { username = it }
)
AppTextField(
hint = "Email",
keyboardType = KeyboardType.Email,
leadingIcon = painterResource(
R.drawable.icon_email
),
value = username,
onValueChange = { username = it }
value = email,
onValueChange = { email = it }
)
AppPasswordField(
hint = "Password",
@ -84,30 +106,12 @@ fun RegisterScreen(
value = password,
onValueChange = { password = it }
)
AppButton(
label = "Daftar",
) {
}
Spacer(modifier = Modifier.height(16.dp))
// Error Message
// if (loginState is ResultState.Error) {
// Text(
// modifier = Modifier.padding(16.dp),
// text = (loginState as ResultState.Error).message,
// color = Color.Red
// )
// }
// Navigate to Home if Success
// LaunchedEffect(loginState) {
// if (loginState is ResultState.Success<*>) {
// onLoginSuccess()
// }
// }
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center

View File

@ -0,0 +1,28 @@
package com.syaroful.agrilinkvocpro.presentation.screen.register
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.syaroful.agrilinkvocpro.core.utils.ResultState
import com.syaroful.agrilinkvocpro.data.model.LoginResponse
import com.syaroful.agrilinkvocpro.data.repository.AuthRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class RegisterViewModel(
authRepository: AuthRepository,
) : ViewModel(){
private val _signupState = MutableStateFlow<ResultState<LoginResponse>>(ResultState.Idle)
val signupState: StateFlow<ResultState<LoginResponse>> = _signupState
fun signup(name: String, username: String, email: String, password: String){
_signupState.value = ResultState.Loading
viewModelScope.launch {
try {
}catch (e: Exception){
}
}
}
}

View File

@ -4,13 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrilinkvocpro.commodity_price_prediction_feature.presentation.prediction.PricePredictionScreen
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
class PricePredictionActivity : ComponentActivity() {
@ -19,30 +13,8 @@ class PricePredictionActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
AgrilinkVocproTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
PricePredictionScreen()
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AgrilinkVocproTheme {
Greeting("Android")
}
}

View File

@ -24,7 +24,7 @@ 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.screen.detail.component.DynamicBottomSheet
import com.syaroful.agrilinkvocpro.presentation.theme.MainGreen
@OptIn(ExperimentalMaterial3Api::class)

View File

@ -32,6 +32,7 @@ navigationCompose = "2.9.0"
okhttp = "4.12.0"
retrofit = "2.9.0"
retrofit2KotlinxSerializationConverter = "0.8.0"
roomRuntime = "2.7.1"
runtime = "1.8.2"
runtimeAndroid = "1.8.1"
ycharts = "2.1.0"
@ -48,6 +49,9 @@ androidx-datastore-preferences = { module = "androidx.datastore:datastore-prefer
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
androidx-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "runtime" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
feature-delivery = { module = "com.google.android.play:feature-delivery", version.ref = "featureDelivery" }

View File

@ -4,13 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrilinkvocpro.growth_recipe_feature.presentation.recipe.GrowthRecipeScreen
import com.syaroful.agrilinkvocpro.presentation.theme.AgrilinkVocproTheme
class GrowthRecipeActivity : ComponentActivity() {
@ -19,29 +13,8 @@ class GrowthRecipeActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
AgrilinkVocproTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
GrowthRecipeScreen()
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AgrilinkVocproTheme {
Greeting("Android")
}
}

View File

@ -25,7 +25,7 @@ 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
import com.syaroful.agrilinkvocpro.presentation.screen.detail.component.LineChart
@OptIn(ExperimentalMaterial3Api::class)