diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/ControlActuatorActivity.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/ControlActuatorActivity.kt index 65a85d3..4c2a3c3 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/ControlActuatorActivity.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/ControlActuatorActivity.kt @@ -1,20 +1,37 @@ package com.syaroful.agrilinkvocpro.control_feature import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import com.syaroful.agrilinkvocpro.control_feature.page.ControlActuatorScreen +import androidx.navigation.compose.rememberNavController +import com.syaroful.agrilinkvocpro.control_feature.di.controlModule +import com.syaroful.agrilinkvocpro.control_feature.di.networkModule +import com.syaroful.agrilinkvocpro.control_feature.di.viewModelModule +import com.syaroful.agrilinkvocpro.control_feature.navigation.NavGraph import com.syaroful.agrilinkvocpro.control_feature.ui.theme.AgrilinkVocproTheme +import org.koin.core.context.loadKoinModules +import org.koin.core.context.unloadKoinModules class ControlActuatorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // load Module + loadKoinModules(listOf(controlModule, networkModule, viewModelModule)) + Log.d("KOIN_DEBUG", "โœ… controlModule loaded, yeay ๐Ÿค—") enableEdgeToEdge() setContent { AgrilinkVocproTheme { - ControlActuatorScreen() + val navController = rememberNavController() + NavGraph(navController) } } } + + override fun onDestroy() { + super.onDestroy() + unloadKoinModules(listOf(controlModule, networkModule, viewModelModule)) + Log.d("KOIN_DEBUG", "๐Ÿงน controlModule unloaded") + } } diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/components/ControlCard.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/ControlCard.kt similarity index 81% rename from agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/components/ControlCard.kt rename to agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/ControlCard.kt index 3716346..45a4b02 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/components/ControlCard.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/ControlCard.kt @@ -1,4 +1,4 @@ -package com.syaroful.agrilinkvocpro.control_feature.components +package com.syaroful.agrilinkvocpro.control_feature.core.components import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -38,6 +39,7 @@ fun ControlCard( iconRes: Int, label: String, isOn: Boolean, + isLoading: Boolean, onToggle: (Boolean) -> Unit ) { val backgroundColor = if (isOn) DarkGreen else MaterialTheme.colorScheme.surfaceContainer @@ -75,17 +77,21 @@ fun ControlCard( modifier = Modifier.size(20.dp) ) } - Switch( - checked = isOn, - onCheckedChange = onToggle, - colors = SwitchDefaults.colors( - checkedThumbColor = DarkGreen, - uncheckedThumbColor = Color.White, - uncheckedBorderColor = trackColor, - checkedTrackColor = MainGreen, - uncheckedTrackColor = trackColor + if (isLoading) { + CircularProgressIndicator(color = MainGreen) + } else { + Switch( + checked = isOn, + onCheckedChange = onToggle, + colors = SwitchDefaults.colors( + checkedThumbColor = DarkGreen, + uncheckedThumbColor = Color.White, + uncheckedBorderColor = trackColor, + checkedTrackColor = MainGreen, + uncheckedTrackColor = trackColor + ) ) - ) + } } Spacer(modifier = Modifier.height(16.dp)) diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/DisableControlCard.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/DisableControlCard.kt new file mode 100644 index 0000000..cc4b168 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/components/DisableControlCard.kt @@ -0,0 +1,102 @@ +package com.syaroful.agrilinkvocpro.control_feature.core.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.syaroful.agrilinkvocpro.control_feature.R + +@Composable +fun DisableControlCard( + label: String, + isOn: Boolean, + iconRes: Int = R.drawable.ic_valve, + backgroundColor: Color = Color.Gray.copy(alpha = 0.1f), + iconTint: Color = Color.Gray, + textColor: Color = Color.Gray, +) { + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .clip(RoundedCornerShape(12.dp)) + .border(1.dp, backgroundColor.copy(alpha = 0.4f), RoundedCornerShape(12.dp)) + .background(backgroundColor) + .padding(16.dp) + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .size(32.dp) + .background( + color = iconTint.copy(alpha = 0.1f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier.size(20.dp) + ) + } +// Switch( +// checked = isOn, +// onCheckedChange = onToggle, +// colors = SwitchDefaults.colors( +// checkedThumbColor = Color.LightGray, +// uncheckedThumbColor = Color.LightGray, +// uncheckedBorderColor = trackColor, +// checkedTrackColor = backgroundColor, +// checkedBorderColor = trackColor, +// uncheckedTrackColor = trackColor +// ) +// ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text(label, color = textColor) + + Text( + if (isOn) "On" else "Off", + color = Color.Gray, + fontWeight = FontWeight.Bold + ) + } +} + +@Preview +@Composable +fun DisableControlCardPreview() { + DisableControlCard( + label = "Water Valve", + isOn = false, + ) +} + diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/state/ControlState.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/state/ControlState.kt new file mode 100644 index 0000000..90b1486 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/core/state/ControlState.kt @@ -0,0 +1,8 @@ +package com.syaroful.agrilinkvocpro.control_feature.core.state + +sealed class ControlState { + data object Idle : ControlState() + data object Loading : ControlState() + data class Success(val data: T?) : ControlState() + data class Error(val message: String) : ControlState() +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorStatusResponse.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorStatusResponse.kt new file mode 100644 index 0000000..f9b0e19 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorStatusResponse.kt @@ -0,0 +1,13 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.model + +data class ActuatorStatusResponse( + val `data`: List?, + val message: String? +) + +data class ActuatorStatus( + val currentStatus: String?, + val lastChangedAt: String?, + val lastTriggeredBy: String?, + val name: String? +) \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorType.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorType.kt new file mode 100644 index 0000000..3e57097 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ActuatorType.kt @@ -0,0 +1,7 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.model + +enum class ActuatorType(val displayName: String) { + WATER("Water Valve"), + NUTRIENT("Nutrient Valve"), + PUMP("Pump") +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ControlLogResponse.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ControlLogResponse.kt new file mode 100644 index 0000000..f0b1bb6 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/model/ControlLogResponse.kt @@ -0,0 +1,14 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.model + +data class ControlLogResponse( + val log: Log?, + val message: String? +) + +data class Log( + val action: String?, + val actuatorId: Int?, + val createdAt: String?, + val id: Int?, + val triggeredBy: String? +) \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt new file mode 100644 index 0000000..3761b17 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/network/ControlService.kt @@ -0,0 +1,34 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.network + +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorStatusResponse +import com.syaroful.agrilinkvocpro.control_feature.data.model.ControlLogResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface ControlService { + @GET("api/actuators/status") + suspend fun getActuatorStatus( + @Header("Authorization") authHeader: String, + ): Response + + @POST("api/actuators/water-valve/control") + suspend fun controlWaterValve( + @Header("Authorization") authHeader: String, + @Body action: String, + ): Response + + @POST("api/actuators/nutrient-valve/control") + suspend fun controlNutrientValve( + @Header("Authorization") authHeader: String, + @Body action: String, + ): Response + + @POST("api/actuators/pump/control") + suspend fun controlPump( + @Header("Authorization") authHeader: String, + @Body action: String, + ): Response +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt new file mode 100644 index 0000000..75fad74 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/data/repository/ControlRepository.kt @@ -0,0 +1,28 @@ +package com.syaroful.agrilinkvocpro.control_feature.data.repository + +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorStatusResponse +import com.syaroful.agrilinkvocpro.control_feature.data.model.ControlLogResponse +import com.syaroful.agrilinkvocpro.control_feature.data.network.ControlService +import retrofit2.Response + +class ControlRepository( + private val controlService: ControlService +) { + + suspend fun controlWaterValve(authHeader: String, action: String): Response{ + return controlService.controlWaterValve(authHeader, action) + } + + suspend fun controlNutrientValve(authHeader: String, action: String): Response{ + return controlService.controlNutrientValve(authHeader, action) + } + + suspend fun controlPump(authHeader: String, action: String): Response{ + return controlService.controlPump(authHeader, action) + } + + suspend fun getActuatorStatus(authHeader: String): Response { + return controlService.getActuatorStatus(authHeader) + } + +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/ControlModule.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/ControlModule.kt new file mode 100644 index 0000000..7a823e5 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/ControlModule.kt @@ -0,0 +1,8 @@ +package com.syaroful.agrilinkvocpro.control_feature.di + +import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository +import org.koin.dsl.module + +val controlModule = module { + single{ControlRepository(get())} +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/NetworkModule.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/NetworkModule.kt new file mode 100644 index 0000000..5a60868 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/NetworkModule.kt @@ -0,0 +1,27 @@ +package com.syaroful.agrilinkvocpro.control_feature.di + +import com.syaroful.agrilinkvocpro.control_feature.data.network.ControlService +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(3, TimeUnit.SECONDS) + .build() + } + + single { + Retrofit.Builder() + .baseUrl("http://labai.polinema.ac.id:3042/") + .client(get()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + single { get().create(ControlService::class.java) } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/ViewModelModule.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/ViewModelModule.kt new file mode 100644 index 0000000..1e73f54 --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/di/ViewModelModule.kt @@ -0,0 +1,9 @@ +package com.syaroful.agrilinkvocpro.control_feature.di + +import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val viewModelModule = module { + viewModel { ControlViewModel(get(), get()) } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt new file mode 100644 index 0000000..620a3fa --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/navigation/NavHost.kt @@ -0,0 +1,24 @@ +package com.syaroful.agrilinkvocpro.control_feature.navigation + +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlActuatorScreen +import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun NavGraph(navController: NavHostController) { + val controlViewModel: ControlViewModel = koinViewModel() + + NavHost(navController, startDestination = "control_screen") { + composable("control_screen") { + ControlActuatorScreen(controlViewModel) + } + composable("history_control_screen") { + Box() {} + } + } +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlActuatorScreen.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlActuatorScreen.kt deleted file mode 100644 index 5d3ff52..0000000 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlActuatorScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.syaroful.agrilinkvocpro.control_feature.page - -import androidx.compose.foundation.layout.Arrangement -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.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Info -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.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.syaroful.agrilinkvocpro.control_feature.R -import com.syaroful.agrilinkvocpro.control_feature.components.ControlCard - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ControlActuatorScreen() { - Scaffold( - topBar = { - TopAppBar( - title = { - Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { - Text("Control Actuator", style = MaterialTheme.typography.titleMedium) - } - }, - navigationIcon = { - IconButton(onClick = { }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - }, - actions = { - IconButton(onClick = { }) { - Icon(Icons.Filled.Info, contentDescription = "History") - } - } - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .padding(16.dp) - .verticalScroll(rememberScrollState()) - ) { - Text("Penyiraman Air", style = MaterialTheme.typography.titleMedium) - - Spacer(modifier = Modifier.height(8.dp)) - - ControlGrid( - iconRes = R.drawable.ic_valve, - items = List(4) { index -> "Bet ${index + 1}" } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text("Penyiraman Pupuk", style = MaterialTheme.typography.titleMedium) - - Spacer(modifier = Modifier.height(8.dp)) - - ControlGrid( - iconRes = R.drawable.ic_leaf, - items = List(4) { index -> "Bet ${index + 1}" } - ) - } - } -} - -@Composable -fun ControlGrid(iconRes: Int, items: List) { - val states = remember { items.map { mutableStateOf(false) } } - val gridHeight = (items.size / 2.0).coerceAtLeast(1.0) * 170 - - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.fillMaxWidth().height(gridHeight.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(items.size) { index -> - ControlCard( - iconRes = iconRes, - label = items[index], - isOn = states[index].value, - onToggle = { states[index].value = it } - ) - } - } -} - - -@Preview -@Composable -fun ControlActuatorScreenPreview() { - ControlActuatorScreen() -} - diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt index 029afbd..03fa266 100644 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/page/ControlScreen.kt @@ -1,75 +1,75 @@ -package com.syaroful.agrilinkvocpro.control_feature.page - -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.syaroful.agrilinkvocpro.control_feature.viewModel.ControlViewModel - -@Composable -fun ControlScreen( - modifier: Modifier = Modifier, - relayState: Boolean, - onRelayStateChange: (Boolean) -> Unit -) { - Scaffold { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "Kontrol Relay", style = MaterialTheme.typography.headlineMedium) - - Spacer(modifier = Modifier.height(24.dp)) - - Switch( - checked = relayState, - onCheckedChange = { isChecked -> - onRelayStateChange(isChecked) - } - ) - } - } -} - -@Composable -fun ControlScreenRoute( - modifier: Modifier = Modifier, - viewModel: ControlViewModel = viewModel() -) { - val relayState by viewModel.relayState.collectAsState() - - ControlScreen( - modifier = modifier, - relayState = relayState, - onRelayStateChange = { viewModel.setRelayState(it) } - ) -} - -@Preview(showBackground = true, name = "Light Mode") -@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES) -@Composable -fun ControlScreenPreview() { - ControlScreen( - relayState = false, - onRelayStateChange = {} - ) -} \ No newline at end of file +//package com.syaroful.agrilinkvocpro.control_feature.page +// +//import android.content.res.Configuration.UI_MODE_NIGHT_YES +//import androidx.compose.foundation.layout.Arrangement +//import androidx.compose.foundation.layout.Column +//import androidx.compose.foundation.layout.Spacer +//import androidx.compose.foundation.layout.fillMaxSize +//import androidx.compose.foundation.layout.height +//import androidx.compose.foundation.layout.padding +//import androidx.compose.material3.MaterialTheme +//import androidx.compose.material3.Scaffold +//import androidx.compose.material3.Switch +//import androidx.compose.material3.Text +//import androidx.compose.runtime.Composable +//import androidx.compose.runtime.collectAsState +//import androidx.compose.runtime.getValue +//import androidx.compose.ui.Alignment +//import androidx.compose.ui.Modifier +//import androidx.compose.ui.tooling.preview.Preview +//import androidx.compose.ui.unit.dp +//import androidx.lifecycle.viewmodel.compose.viewModel +//import com.syaroful.agrilinkvocpro.control_feature.presentation.control.ControlViewModel +// +//@Composable +//fun ControlScreen( +// modifier: Modifier = Modifier, +// relayState: Boolean, +// onRelayStateChange: (Boolean) -> Unit +//) { +// Scaffold { innerPadding -> +// Column( +// modifier = Modifier +// .fillMaxSize() +// .padding(innerPadding) +// .padding(24.dp), +// verticalArrangement = Arrangement.Center, +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// Text(text = "Kontrol Relay", style = MaterialTheme.typography.headlineMedium) +// +// Spacer(modifier = Modifier.height(24.dp)) +// +// Switch( +// checked = relayState, +// onCheckedChange = { isChecked -> +// onRelayStateChange(isChecked) +// } +// ) +// } +// } +//} +// +//@Composable +//fun ControlScreenRoute( +// modifier: Modifier = Modifier, +// viewModel: ControlViewModel = viewModel() +//) { +// val relayState by viewModel.relayState.collectAsState() +// +// ControlScreen( +// modifier = modifier, +// relayState = relayState, +// onRelayStateChange = { viewModel.setRelayState(it) } +// ) +//} +// +//@Preview(showBackground = true, name = "Light Mode") +//@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES) +//@Composable +//fun ControlScreenPreview() { +// ControlScreen( +// relayState = false, +// onRelayStateChange = {} +// ) +//} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt new file mode 100644 index 0000000..8ddcaae --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlActuatorScreen.kt @@ -0,0 +1,155 @@ +package com.syaroful.agrilinkvocpro.control_feature.presentation.control + +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Info +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.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.syaroful.agrilinkvocpro.control_feature.R +import com.syaroful.agrilinkvocpro.control_feature.core.components.ControlCard +import com.syaroful.agrilinkvocpro.control_feature.core.components.DisableControlCard +import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType +import com.syaroful.agrilinkvocpro.core.placeholder.shimmerEffect + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ControlActuatorScreen( + viewModel: ControlViewModel +) { + val allActuators by viewModel.allActuatorState.collectAsState() + val waterValveStatus by viewModel.waterValveStatus.collectAsState() + val nutritionValveStatus by viewModel.nutritionValveStatus.collectAsState() + val pumpStatus by viewModel.pumpStatus.collectAsState() + val isLoading by viewModel.isLoading.collectAsState() + + val isRefreshing = remember { mutableStateOf(false) } + + + LaunchedEffect(Unit) { + viewModel.getActuatorStatus() + } + Scaffold( + topBar = { + TopAppBar( + title = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text("Control Actuator", style = MaterialTheme.typography.titleMedium) + } + }, + navigationIcon = { + IconButton(onClick = { }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }, + actions = { + IconButton(onClick = { }) { + Icon(Icons.Filled.Info, contentDescription = "History") + } + } + ) + } + ) { innerPadding -> + PullToRefreshBox( + isRefreshing = isRefreshing.value, + onRefresh = { + isRefreshing.value = true + viewModel.getActuatorStatus() + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Daftar Actuator", style = MaterialTheme.typography.titleMedium) + + + // ControlGrid( + // iconRes = R.drawable.ic_valve, + // items = List(4) { index -> "Bet ${index + 1}" } + // ) + when (allActuators) { + is ControlState.Loading -> { + isRefreshing.value = false + Box( + modifier = Modifier + .fillMaxWidth() + .height(96.dp) + .shimmerEffect() + ) + } + + is ControlState.Error -> { + isRefreshing.value = false + DisableControlCard( + label = "Control Failed!", + isOn = false, + ) + } + + ControlState.Idle -> { + isRefreshing.value = false + } + + is ControlState.Success<*> -> { + isRefreshing.value = false + ControlCard( + iconRes = R.drawable.ic_valve, + label = "Water Valve", + isOn = waterValveStatus, + isLoading = isLoading, + onToggle = { viewModel.controlActuator(ActuatorType.WATER, !waterValveStatus) } + ) + ControlCard( + iconRes = R.drawable.ic_leaf, + label = "Nutrient Valve", + isOn = nutritionValveStatus, + isLoading = isLoading, + onToggle = { viewModel.controlActuator(ActuatorType.NUTRIENT, !nutritionValveStatus) } + ) + ControlCard( + iconRes = R.drawable.ic_valve, + label = "Pump", + isOn = pumpStatus, + isLoading = isLoading, + onToggle = { viewModel.controlActuator(ActuatorType.PUMP, !pumpStatus) } + ) + } + } + + + + } + } + } +} diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt new file mode 100644 index 0000000..8e8244c --- /dev/null +++ b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/presentation/control/ControlViewModel.kt @@ -0,0 +1,109 @@ +package com.syaroful.agrilinkvocpro.control_feature.presentation.control + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.syaroful.agrilinkvocpro.control_feature.core.state.ControlState +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorStatusResponse +import com.syaroful.agrilinkvocpro.control_feature.data.model.ActuatorType +import com.syaroful.agrilinkvocpro.control_feature.data.repository.ControlRepository +import com.syaroful.agrilinkvocpro.data.UserPreferences +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +private const val TAG = "ControlViewModel" + +class ControlViewModel( + private val userPreferences: UserPreferences, + private val repository: ControlRepository +) : ViewModel() { + private val _allActuatorState = + MutableStateFlow>(ControlState.Idle) + val allActuatorState: StateFlow> = _allActuatorState + + private val _waterValveStatus = MutableStateFlow(false) + val waterValveStatus: StateFlow = _waterValveStatus + + private val _nutritionValveStatus = MutableStateFlow(false) + val nutritionValveStatus: StateFlow = _nutritionValveStatus + + private val _pumpStatus = MutableStateFlow(false) + val pumpStatus: StateFlow = _pumpStatus + + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading + + fun getActuatorStatus() { + _allActuatorState.value = ControlState.Loading + viewModelScope.launch { + val token = userPreferences.tokenFlow.first() + val authHeader = "Bearer $token" + try { + delay(500L) + val response = repository.getActuatorStatus(authHeader) + if (response.isSuccessful) { + val data = response.body() + _allActuatorState.value = ControlState.Success(data) + data?.data?.forEach { actuator -> + val isOn = actuator?.currentStatus == "ON" + when (actuator?.name) { + ActuatorType.WATER.displayName -> _waterValveStatus.value = isOn + ActuatorType.NUTRIENT.displayName -> _nutritionValveStatus.value = isOn + ActuatorType.PUMP.displayName -> _pumpStatus.value = isOn + } + } + } else { + val errorBody = response.errorBody()?.string() + val errorMessage = errorBody ?: "Error ${response.code()}" + _allActuatorState.value = ControlState.Error(errorMessage) + } + } catch (e: Exception) { + _allActuatorState.value = ControlState.Error(e.message ?: "Unknown error") + } + } + } + + + fun controlActuator(actuator: ActuatorType, state: Boolean) { + _isLoading.value = true + viewModelScope.launch { + val token = userPreferences.tokenFlow.first() + val authHeader = "Bearer $token" + val stringState = if (state) "ON" else "OFF" + try { + delay(500L) + + val response = when (actuator) { + ActuatorType.WATER -> repository.controlWaterValve(authHeader, stringState) + ActuatorType.NUTRIENT -> repository.controlNutrientValve(authHeader, stringState) + ActuatorType.PUMP -> repository.controlPump(authHeader, stringState) + } + + if (response.isSuccessful) { + val isOn = response.body()?.log?.action == "ON" + updateActuatorStatus(actuator, isOn) + Log.d(TAG, "Successfully controlled ${actuator.displayName}") + } else { + val errorBody = response.errorBody()?.string() + Log.e(TAG, "Error controlling ${actuator.displayName}: $errorBody") + } + } catch (e: Exception) { + Log.e(TAG, "Exception while controlling ${actuator.displayName}: ${e.message}") + } finally { + _isLoading.value = false + } + } + } + + private fun updateActuatorStatus(actuator: ActuatorType, isOn: Boolean) { + when (actuator) { + ActuatorType.WATER -> _waterValveStatus.value = isOn + ActuatorType.NUTRIENT -> _nutritionValveStatus.value = isOn + ActuatorType.PUMP -> _pumpStatus.value = isOn + } + } + +} \ No newline at end of file diff --git a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/viewModel/ControlViewModel.kt b/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/viewModel/ControlViewModel.kt deleted file mode 100644 index e429eef..0000000 --- a/agrilinkvocpro/control_feature/src/main/java/com/syaroful/agrilinkvocpro/control_feature/viewModel/ControlViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.syaroful.agrilinkvocpro.control_feature.viewModel - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.firebase.database.DataSnapshot -import com.google.firebase.database.DatabaseError -import com.google.firebase.database.ValueEventListener -import com.google.firebase.database.ktx.database -import com.google.firebase.ktx.Firebase -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch - -private const val TAG = "ControlViewModel" - -class ControlViewModel : ViewModel() { - - private val _relayState = MutableStateFlow(false) - val relayState: StateFlow = _relayState - - private val database = Firebase.database - private val relayRef = database.getReference("relay1/state") - - init { - observeRelayState() - } - - private fun observeRelayState() { - relayRef.get() - .addOnSuccessListener { snapshot -> - val value = snapshot.getValue(Int::class.java) ?: 0 - _relayState.value = (value == 1) - }.addOnFailureListener{ - Log.e(TAG, "Failed to fetch initial relay state", it) - } - - relayRef.addValueEventListener(object : ValueEventListener { - override fun onDataChange(snapshot: DataSnapshot) { - val value = snapshot.getValue(Int::class.java) ?: 0 - _relayState.value = (value == 1) - } - override fun onCancelled(error: DatabaseError) { - Log.e(TAG, "Failed to observe relay state", error.toException()) - } - }) - } - - fun setRelayState(isOn: Boolean) { - viewModelScope.launch { - relayRef.setValue(if (isOn) 1 else 0) - .addOnFailureListener { - Log.e(TAG, "Failed to set relay state", it) - } - } - } -} \ No newline at end of file