commit 528e4776ee46774bbf94ad71af7d3dfefaa395f2 Author: Aditya Date: Mon Aug 3 12:15:55 2026 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..6be6642 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +UI tugas akhir \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..7b3006b --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..005afb3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,47 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..8d81632 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Backup.txt b/Backup.txt new file mode 100644 index 0000000..5e82deb --- /dev/null +++ b/Backup.txt @@ -0,0 +1,1036 @@ +"Otomatis" -> { + + + Text( + text = "Pemantauan sensor", + fontSize = 15.sp, + fontFamily = Inria_Regular, + color = Color.Gray, + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 130.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + + // ================== KELEMBABAN UDARA ================== + Column( + modifier = Modifier.padding(top = 15.dp, start = 19.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(60.dp) + ) { + + CircularProgressIndicator( + progress = 1f, + color = Color.LightGray, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + CircularProgressIndicator( + progress = progressKU, + color = Color(0xFF4FC3F7), + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + Text( + text = "$ku%", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + + Text("Kel. Udara", fontSize = 10.sp, fontWeight = FontWeight.Bold) + + Text( + text = statusKelembaban(ku), + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray, + modifier = Modifier.offset(y = -8.dp, x = 10.dp) + ) + } + + // ================== SUHU UDARA ================== + Column( + modifier = Modifier.padding(top = 15.dp, start = 10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(60.dp) + ) { + + CircularProgressIndicator( + progress = 1f, + color = Color.LightGray, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + CircularProgressIndicator( + progress = progressSU, + color = Color(0xFF4FC3F7), + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + Text( + text = "$su°C", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + + Text("Suhu Udara", fontSize = 10.sp, fontWeight = FontWeight.Bold) + + Text( + text = statusSuhu(su), + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray, + modifier = Modifier.offset(y = -8.dp, x = 10.dp) + ) + } + + // ================== KELEMBABAN TANAH ================== + Column( + modifier = Modifier.padding(top = 15.dp, start = 10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(60.dp) + ) { + + CircularProgressIndicator( + progress = 1f, + color = Color.LightGray, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + CircularProgressIndicator( + progress = progressKT, + color = Color(0xFF4FC3F7), + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + Text( + text = "$kt%", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + + Text("Kel. Tanah", fontSize = 10.sp, fontWeight = FontWeight.Bold) + + Text( + text = statusKelembaban(kt), + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray, + modifier = Modifier.offset(y = -8.dp, x = 10.dp) + ) + } + + // ================== INTENSITAS HUJAN ================== + Column( + modifier = Modifier.padding(top = 15.dp, start = 10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(60.dp) + ) { + + CircularProgressIndicator( + progress = 1f, + color = Color.LightGray, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + CircularProgressIndicator( + progress = progressIH, + color = Color(0xFF4FC3F7), + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + Text( + text = "$ih%", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + + Text("Int. Hujan", fontSize = 10.sp, fontWeight = FontWeight.Bold) + + Text( + text = when { + ih < 40 -> "Ringan" + ih < 70 -> "Sedang" + else -> "Deras" + }, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.Gray, + modifier = Modifier.offset(y = -8.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ){ + Text( + text = "Atur jadwal", + fontSize = 15.sp, + color = Color.Gray, + fontFamily = Inria_Regular, + modifier = Modifier + .padding(horizontal = 28.dp) + ) + + Image( + painter = painterResource(id = R.drawable.calendar), + contentDescription = "gambar 1", + modifier = Modifier + .size(13.dp) + .offset(y = 2.dp, x = -20.dp), + colorFilter = ColorFilter.tint(Color(0xFF6B6666)) + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + var jadwalList by remember { + mutableStateOf(listOf()) + } + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + .height(190.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(vertical = 12.dp) + ) { + + // semua jadwal tampil + jadwalList.forEachIndexed { index, jadwal -> + Column( + modifier = Modifier.padding(horizontal = 20.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + // kiri + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.trash), + contentDescription = "Hapus jadwal", + modifier = Modifier + .size(14.dp) + .clickable { + jadwalList = + jadwalList.toMutableList().also { + it.removeAt(index) + } + } + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "Jadwal ${index + 1}", + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) + } + + // kanan edit + Image( + painter = painterResource(id = R.drawable.edit_text), + contentDescription = "Edit jadwal", + modifier = Modifier + .size(14.dp) + .clickable { + + val timePickerDialog = TimePickerDialog( + context, + { _, hourOfDay, minute -> + + val status = + getWaktuStatus(hourOfDay, minute) + + val editedJadwal = + String.format( + "%02d:%02d %s", + hourOfDay, + minute, + status + ) + + jadwalList = + jadwalList.toMutableList().also { + it[index] = editedJadwal + } + }, + 0, + 0, + true + ) + + timePickerDialog.show() + } + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = jadwal, + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) + + HorizontalDivider( + modifier = Modifier.padding(vertical = 10.dp), + thickness = 0.8.dp, + color = Color.Gray.copy(alpha = 0.3f) + ) + } + } + + // tombol tambah + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(40.dp) + .clickable { + + val timePickerDialog = TimePickerDialog( + context, + { _, hourOfDay, minute -> + + val status = + getWaktuStatus(hourOfDay, minute) + + val newJadwal = + String.format( + "%02d:%02d %s", + hourOfDay, + minute, + status + ) + + jadwalList = jadwalList + newJadwal + }, + 0, + 0, + true + ) + + timePickerDialog.show() + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "+", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF2F6F4E) + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = "Tambah Jadwal", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF2F6F4E) + ) + } + } + } + + // popup edit custom + if (showEditDialog) { + AlertDialog( + onDismissRequest = { + showEditDialog = false + }, + title = { + Text( + text = "Edit Jadwal", + fontWeight = FontWeight.Bold + ) + }, + text = { + Column { + Text( + text = "Jadwal saat ini:", + fontSize = 12.sp + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = selectedTimeText, + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + }, + confirmButton = { + TextButton( + onClick = { + showEditDialog = false + + val timePickerDialog = TimePickerDialog( + context, + { _, hourOfDay, minute -> + + val status = + getWaktuStatus(hourOfDay, minute) + + selectedTimeText = + String.format( + "%02d:%02d %s", + hourOfDay, + minute, + status + ) + }, + 0, + 0, + true + ) + + timePickerDialog.show() + } + ) { + Text("Ubah") + } + }, + dismissButton = { + TextButton( + onClick = { + showEditDialog = false + } + ) { + Text("Batal") + } + } + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + + Text( + text = "Atur jeda penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + color = Color.Gray, + modifier = Modifier + .padding(horizontal = 28.dp) + ) + + Image( + painter = painterResource(id = R.drawable.clock), + contentDescription = "gambar 1", + modifier = Modifier.size(14.dp) + .offset(y = 1.dp, x = -20.dp), + colorFilter = ColorFilter.tint(Color(0xFF6B6666)) + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 110.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + + Column( + modifier = Modifier + .padding(top = 15.dp) // 🔥 kasih jarak dari atas + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically // 🔥 center! + ) { + + BasicTextField( + value = jeda, + onValueChange = { + if (it.all { char -> char.isDigit() }) { + jeda = it.take(2) + } + }, + singleLine = true, + textStyle = LocalTextStyle.current.copy( + fontSize = 28.sp, // 🔥 lebih besar + fontWeight = FontWeight.Bold, + fontFamily = Poppins_Black, + color = Color.Black.copy(alpha = 0.8f) + ), + modifier = Modifier.width(60.dp) + .offset(x = 4.dp), + decorationBox = { innerTextField -> + if (jeda.isEmpty()) { + Text( + "00", + fontSize = 28.sp, + fontFamily = Poppins_Black, + fontWeight = FontWeight.Bold, + color = Color.Black.copy(alpha = 0.7f) + ) + } + innerTextField() + } + ) + + Box( + modifier = Modifier.offset(x = -3.dp) + ) { + Text( + text = selectedUnit, + fontSize = 14.sp, // 🔥 sedikit diperbesar + fontFamily = Poppins_Regular, + fontWeight = FontWeight.Bold, + color = Color.Black.copy(alpha = 0.7f), + modifier = Modifier + .clickable { expanded = true } + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + listOf("Detik", "Menit", "Jam").forEach { unit -> + DropdownMenuItem( + text = { Text(unit) }, + onClick = { + selectedUnit = unit + expanded = false + } + ) + } + } + } + } + + Spacer(modifier = Modifier.height(13.dp)) // 🔥 jarak tombol + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 60.dp) + .offset(y = -10.dp) + .height(42.dp) + .scale(scale), + onClick = { + if (jeda.isEmpty()) { + Toast.makeText(context, "Masukkan jeda dulu", Toast.LENGTH_SHORT).show() + return@Card + } + + val jedaInt = jeda.toInt() + + val jedaDalamDetik = when (selectedUnit) { + "Detik" -> jedaInt + "Menit" -> jedaInt * 60 + "Jam" -> jedaInt * 3600 + else -> jedaInt + } + + saveJeda(jedaDalamDetik.toString(), context) + }, + interactionSource = interactionSource, + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Simpan", + fontSize = 15.sp, + color = Color.White, + fontFamily = Poppins_Regular, + fontWeight = FontWeight.Bold + ) + } + } + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + + Text( + text = "Atur ambang batas", + fontSize = 15.sp, + fontFamily = Inria_Regular, + color = Color.Gray, + modifier = Modifier + .padding(horizontal = 28.dp) + ) + + Image( + painter = painterResource(id = R.drawable.warning), + contentDescription = "gambar 1", + modifier = Modifier.size(14.dp) + .offset(y = 1.dp, x = -20.dp), + colorFilter = ColorFilter.tint(Color(0xFF6B6666)) + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 200.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + Column(modifier = Modifier.fillMaxSize()) { + + // ================= TANAH ================= + Column(modifier = Modifier.padding(top = 15.dp)) { + Row( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.decagon), + contentDescription = null, + modifier = Modifier.size(14.dp), + colorFilter = ColorFilter.tint(Color(0xFF6B6666)) + ) + + Text( + text = "Kelembapan tanah", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 10.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 39.dp, end = 15.dp, top = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = if (tanah == 0 && !isSavedTanah) "-" else tanah.toString(), + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.width(5.dp)) + + Text("%") + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = if (isSavedTanah) "Tersimpan" else "Belum diatur", + fontSize = 10.sp + ) + + Spacer(modifier = Modifier.width(5.dp)) + + if (isSavedTanah) { + Image( + painter = painterResource(id = R.drawable.checked), + contentDescription = null, + modifier = Modifier.size(13.dp) + ) + } + + Spacer(modifier = Modifier.width(5.dp)) + + Image( + painter = painterResource(id = R.drawable.edit_text), + contentDescription = "edit", + modifier = Modifier + .size(13.dp) + .clickable { + currentField = "tanah" + inputValue = tanah.toString() + showDialog = true + } + ) + } + } + } + + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + + // ================= SUHU ================= + Column(modifier = Modifier.padding(top = 8.dp)) { + Row( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.thermometer), + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + + Text( + text = "Suhu udara", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 10.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 39.dp, end = 15.dp, top = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = if (suhu == 0 && !isSavedSuhu) "-" else suhu.toString(), + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.width(5.dp)) + + Text("%") + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = if (isSavedSuhu) "Tersimpan" else "Belum diatur", + fontSize = 10.sp + ) + + Spacer(modifier = Modifier.width(5.dp)) + + if (isSavedSuhu) { + Image( + painter = painterResource(id = R.drawable.checked), + contentDescription = null, + modifier = Modifier.size(13.dp) + ) + } + + Spacer(modifier = Modifier.width(5.dp)) + + Image( + painter = painterResource(id = R.drawable.edit_text), + contentDescription = "edit", + modifier = Modifier + .size(13.dp) + .clickable { + currentField = "suhu" + inputValue = suhu.toString() + showDialog = true + } + ) + } + } + } + + HorizontalDivider(modifier = Modifier.padding(top = 8.dp)) + + // ================= UDARA ================= + Column(modifier = Modifier.padding(top = 8.dp)) { + Row( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.drop), + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + + Text( + text = "Kelembapan udara", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 10.dp) + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 39.dp, end = 15.dp, top = 2.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + + Row(verticalAlignment = Alignment.Bottom) { + Text( + text = if (udara == 0 && !isSavedUdara) "-" else udara.toString(), + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.width(5.dp)) + + Text("%") + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = if (isSavedUdara) "Tersimpan" else "Belum diatur", + fontSize = 10.sp + ) + + Spacer(modifier = Modifier.width(5.dp)) + + if (isSavedUdara) { + Image( + painter = painterResource(id = R.drawable.checked), + contentDescription = null, + modifier = Modifier.size(13.dp) + ) + } + + Spacer(modifier = Modifier.width(5.dp)) + + Image( + painter = painterResource(id = R.drawable.edit_text), + contentDescription = "edit", + modifier = Modifier + .size(13.dp) + .clickable { + currentField = "udara" + inputValue = udara.toString() + showDialog = true + } + ) + } + } + } + } + } + + if (showDialog) { + AlertDialog( + onDismissRequest = { + showDialog = false + }, + title = { + Text("Atur Nilai") + }, + text = { + OutlinedTextField( + value = inputValue, + onValueChange = { + inputValue = it + }, + label = { + Text("Masukkan angka") + } + ) + }, + confirmButton = { + TextButton( + onClick = { + + val value = inputValue.toIntOrNull() + + if (value == null) { + Toast.makeText(context, "Masukkan angka!", Toast.LENGTH_SHORT).show() + return@TextButton + } + + val newValue = value.coerceIn(0, 100) + + when (currentField) { + "tanah" -> { + tanah = newValue + isSavedTanah = newValue != 0 + } + "suhu" -> { + suhu = newValue + isSavedSuhu = newValue != 0 + } + "udara" -> { + udara = newValue + isSavedUdara = newValue != 0 + } + } + + scope.launch { + try { + RetrofitClient.api.saveSetting(1, tanah, suhu, udara) + Toast.makeText(context, "Berhasil disimpan", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Gagal menyimpan", Toast.LENGTH_SHORT).show() + } + } + + showDialog = false + } + ) { + Text("Simpan") + } + }, + + dismissButton = { + Row { + + TextButton( + onClick = { + + when (currentField) { + "tanah" -> { + tanah = 0 + isSavedTanah = false + } + "suhu" -> { + suhu = 0 + isSavedSuhu = false + } + "udara" -> { + udara = 0 + isSavedUdara = false + } + } + + scope.launch { + try { + RetrofitClient.api.saveSetting(1, tanah, suhu, udara) + Toast.makeText(context, "Berhasil dihapus", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Gagal menghapus", Toast.LENGTH_SHORT).show() + } + } + + showDialog = false + } + ) { + Text("Hapus", color = Color.Red) + } + + Spacer(modifier = Modifier.width(8.dp)) + + TextButton( + onClick = { showDialog = false } + ) { + Text("Batal") + } + } + } + ) + } + + + Spacer(modifier = Modifier.height(25.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .padding(bottom = 15.dp) + .fillMaxWidth() + .height(50.dp) + .scale(scale), // ✅ animasi dari kode kamu + onClick = { + viewModel.setConfig( + jadwal = "08:00", + ambang = 60, + jeda = 10 + ) + }, + interactionSource = interactionSource, + shape = RoundedCornerShape(30.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Simpan", + fontSize = 15.sp, + color = Color.White, + fontFamily = Poppins_Bold, + fontWeight = FontWeight.Bold + ) + } + } + } \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/RiwayatViewModel.kt b/app/RiwayatViewModel.kt new file mode 100644 index 0000000..12e1172 --- /dev/null +++ b/app/RiwayatViewModel.kt @@ -0,0 +1,26 @@ +package com.adit.smartplant.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.adit.smartplant.model.RiwayatItem +import com.adit.smartplant.network.RetrofitClient +import kotlinx.coroutines.launch + +class RiwayatViewModel : ViewModel() { + + var riwayatList by mutableStateOf>(emptyList()) + private set + + fun loadRiwayat(userId: Int) { + viewModelScope.launch { + try { + riwayatList = RetrofitClient.api.getRiwayat(userId) + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..b832b80 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,107 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + + alias(libs.plugins.ksp) +} + +android { + namespace = "com.example.uitugasakhir" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.uitugasakhir" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + + implementation(platform(libs.androidx.compose.bom)) + + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + implementation("androidx.navigation:navigation-compose:2.7.7") + + implementation("androidx.compose.material:material-icons-extended") + + implementation("com.android.volley:volley:1.2.1") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + implementation("com.google.accompanist:accompanist-swiperefresh:0.32.0") + + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + implementation("com.airbnb.android:lottie-compose:6.4.0") + + implementation("io.coil-kt:coil-compose:2.5.0") + implementation("io.coil-kt:coil-gif:2.5.0") + + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") + + implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5") + implementation("com.github.hannesa2:paho.mqtt.android:4.2.4") + + implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/uitugasakhir/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/uitugasakhir/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..c65efce --- /dev/null +++ b/app/src/androidTest/java/com/example/uitugasakhir/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.uitugasakhir + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.uitugasakhir", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5ef289e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..e906905 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/Page/Page Monitoring.kt b/app/src/main/java/Page/Page Monitoring.kt new file mode 100644 index 0000000..41051ff --- /dev/null +++ b/app/src/main/java/Page/Page Monitoring.kt @@ -0,0 +1,878 @@ +package com.example.uitugasakhir + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Inter +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.ui.platform.LocalContext +import android.widget.Toast +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import org.json.JSONObject +import androidx.compose.runtime.LaunchedEffect +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.runtime.Composable +import com.example.uitugasakhir.ViewModel.RiwayatViewModel +import com.example.uitugasakhir.ViewModel.MQTTViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import android.util.Log +import org.json.JSONArray +import com.example.uitugasakhir.ViewModel.SensorViewModel +import com.google.accompanist.swiperefresh.* +import com.example.uitugasakhir.model.JadwalItem +import com.example.uitugasakhir.ViewModel.ManualViewModel +import com.example.uitugasakhir.api.RetrofitClient +import androidx.compose.animation.core.tween +import com.example.uitugasakhir.ui_manual.SensorSection +import com.example.uitugasakhir.ui_manual.PenyiramanSection +import com.example.uitugasakhir.ui_manual.RiwayatSection +import com.example.uitugasakhir.ui_manual.RataRataSection + +import com.example.uitugasakhir.ui_otomatis.JadwalSection +import com.example.uitugasakhir.ui_otomatis.JedaPenyiramanCard + +import com.example.uitugasakhir.aksi.DeleteDialog +import com.example.uitugasakhir.ui.komponen.WarningPopup +import android.media.MediaPlayer + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings + +import com.example.uitugasakhir.ui_otomatis.SensorSectionOtomatis +import com.example.uitugasakhir.ui_otomatis.RiwayatSectionOtomatis +import com.example.uitugasakhir.ui_otomatis.AmbangBatasSection + +import com.example.uitugasakhir.ViewModel.PumpViewModel +import com.example.uitugasakhir.ViewModel.MainViewModel +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue + +import com.example.uitugasakhir.mqtt.MqttManager +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.ui.res.stringResource +import com.example.uitugasakhir.ui_otomatis.RataRataOtomatisSection + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ManualScreen( + navController: NavController, + sensorVm: SensorViewModel, + mainVm: MainViewModel +) { + // Riwayat viewModel + val viewModel = mainVm + val mainViewModel: MainViewModel = viewModel() + val mode = mainVm.mode + val isLoading = mainVm.isLoading + val context = LocalContext.current + + val snackbarHostState = remember { + SnackbarHostState() + } + + var showPopup by remember { + mutableStateOf(false) + } + + val mqttVm: MQTTViewModel = viewModel() + val mqttState = mqttVm.state + + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false + ) + + var mutePopupUntil by remember { + mutableLongStateOf(0L) + } + + var disablePopup1Minute by remember { + mutableStateOf(false) + } + + var popupMessage by remember { + mutableStateOf("") + } + + + val mqtt = remember { + MqttManager(context) + } + + val mediaPlayerManual = remember { + MediaPlayer.create(context, R.raw.peringatan) + } + + DisposableEffect(Unit) { + onDispose { + mediaPlayerManual.release() + } + } + + var autoStatus by remember { + mutableStateOf("Monitoring sistem") + } + + LaunchedEffect(Unit) { + + viewModel.initMQTT(context) + + viewModel.loadRiwayat(context) + + Log.d( + "LOAD_RIWAYAT", + "Size = ${viewModel.riwayatList.size}" + ) + } + + LaunchedEffect(Unit) { + mainViewModel.loadAverageSensor(context) + } + + // Set pompa + val pumpVm: PumpViewModel = viewModel() + + // ✅ FIX 1: rename supaya tidak bentrok + val manualViewModel: ManualViewModel = viewModel() + val swipeState = rememberSwipeRefreshState(manualViewModel.isLoading) + + val riwayatVm: RiwayatViewModel = viewModel() + + var showDeleteDialog by remember { mutableStateOf(false) } + var selectedDeleteId by remember { mutableStateOf("") } + + var targetMode by remember { mutableStateOf("") } + + + var jeda by remember { mutableStateOf("") } + + val formattedJeda = jeda.padStart(2, '0') + + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1f, + label = "scale", + animationSpec = tween(300) + ) + + var selected by remember { mutableStateOf("Manual") } + + var showConfirmDialog by remember { mutableStateOf(false) } + var showSettingDialog by remember { mutableStateOf(false) } + + val status = sensorVm.status + + + //Pop up + LaunchedEffect(Unit) { + + mqtt.connect( + + onConnected = { + Log.d("MQTT", "CONNECTED") + }, + + onMessage = { topic, msg -> + + try { + + val json = JSONObject(msg) + + // ================= ALARM ================= + if (topic == "smartplant/alarm") { + + val message = + json.optString("message") + + if (message != "AMAN") { + + val now = System.currentTimeMillis() + + // CEK APAKAH POPUP SEDANG DIMUTE + if (now > mutePopupUntil) { + + showPopup = true + popupMessage = message + } + + } else { + + showPopup = false + } + } + + // ================= SENSOR ================= + if (topic == "smartplant/sensor") { + + val tanah = + json.optInt("tanah") + + val hujan = + json.optInt("hujan") + + Log.d( + "HUJAN_DEBUG", + "HUJAN = $hujan" + ) + + // ================= HUJAN ================= + if (hujan > 80) { + + autoStatus = + "Hujan terdeteksi.\nPenyiraman ditunda" + } + + // ================= TANAH KERING ================= + else if (tanah < 20) { + + autoStatus = + "Penyiraman otomatis berjalan" + } + + // ================= NORMAL ================= + else { + + autoStatus = + "Tanah lembab.\nPompa tidak aktif" + } + } + + } catch (e: Exception) { + + Log.e( + "MQTT_ERROR", + e.toString() + ) + } + } + ) + } + + // LOAD DATA + LaunchedEffect(Unit) { + riwayatVm.loadRiwayat(1) + sensorVm.getData(context) + } + + //========== Batas Akhir ============ + + var jedaDetik by remember { + mutableIntStateOf(0) + } + + // Jadwal ui + + var jadwalList by remember { + mutableStateOf(listOf()) + } + + val loadJadwal = remember { + + { + + val url = "http://kopikaosmartsystem.my.id/get_jadwal.php" + + val request = StringRequest( + Request.Method.GET, + url, + + { response -> + + try { + + Log.d("API_RESPONSE", response) + + val jsonObject = JSONObject(response) + + val jsonArray = jsonObject.getJSONArray("jadwal") + + val temp = mutableListOf() + + for (i in 0 until jsonArray.length()) { + + val obj = jsonArray.getJSONObject(i) + + temp.add( + + JadwalItem( + id = obj.getString("id"), + waktu = obj.getString("waktu"), + status = obj.getString("status") + ) + ) + } + + jadwalList = temp + + } catch (e: Exception) { + + Log.e("JSON_ERROR", e.toString()) + } + }, + + { error -> + + Log.e("API_ERROR", error.toString()) + } + ) + + Volley.newRequestQueue(context).add(request) + } + } + + LaunchedEffect(Unit) { + loadJadwal() + } + + + + LaunchedEffect(status) { + if (status.isNotEmpty()) { + Toast.makeText(context, status.toString(), Toast.LENGTH_SHORT).show() + } + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + + + + Box( + modifier = Modifier.fillMaxSize() + ) { + // HEADER HIJAU + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + Image( + painter = painterResource(id = R.drawable.back), + contentDescription = "kembali", + modifier = Modifier + .size(15.dp) + .offset(x = (-160).dp, y = 1.dp) + .clickable { + navController.popBackStack() + } + ) + + Text( + text = "Penyiraman", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + } + + // CONTENT PUTIH + Card( + modifier = Modifier + .padding(top = 64.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = 15.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + // JUDUL MODE + Text( + text = stringResource(R.string.watering_mode), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.select_a_watering_mode), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.align(Alignment.Start).padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // TOGGLE MANUAL OTOMATIS + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .offset(y = 23.dp) + .height(55.dp) + .clip(RoundedCornerShape(50.dp)) + .background(Color(0xFFD9D9D9)) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + // ================= OTOMATIS ================= + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(20.dp)) + .background( + if (selected == "Otomatis") + Color(0xFF2F6F4E) + else + Color.Transparent + ) + .clickable { + if (selected != "Otomatis") { + targetMode = "Otomatis" + showConfirmDialog = true + } + }, + contentAlignment = Alignment.Center + ) { + Text( + "Otomatis", + color = if (selected == "Otomatis") + Color.White else Color.Black + ) + } + + // ================= MANUAL ================= + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(20.dp)) + .background( + if (selected == "Manual") + Color(0xFF2F6F4E) + else + Color.Transparent + ) + .clickable { + if (selected != "Manual") { + targetMode = "Manual" + showConfirmDialog = true + } + }, + contentAlignment = Alignment.Center + ) { + Text( + "Manual", + color = if (selected == "Manual") + Color.White else Color.Black + ) + } + } + } + + // Ikon Pengaturan + Box( + ) { + if (selected == "Otomatis") { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Pengaturan", + tint = Color.Black.copy(alpha = 0.7f), // 🔥 ganti putih biar kelihatan (background abu2) + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 15.dp, end = 8.dp) + .offset(x = 150.dp) + .size(30.dp) + .clickable { + showSettingDialog = true + } + ) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + // Tampilan monitoring otomatis + if (selected == "Otomatis") { + + SensorSectionOtomatis() + + Spacer(modifier = Modifier.height(15.dp)) + + RataRataOtomatisSection( + snackbarHostState = snackbarHostState + ) + + Text( + text = stringResource(R.string.system_status), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier + .padding(top = 15.dp) + .padding(horizontal = 25.dp) + .fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.displays_the_status_of_the_sensors_pump_and_watering_mode), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 25.dp) + ) + + Card( + modifier = Modifier + .padding(top = 15.dp) + .padding(horizontal = 25.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + Box( + modifier = Modifier + .size(12.dp) + .background( + when { + + autoStatus.contains("Hujan") -> + Color(0xFF2196F3) + + autoStatus.contains("ditunda") -> + Color(0xFF2196F3) + + autoStatus.contains("berjalan") -> + Color(0xFF2F6F4E) + + autoStatus.contains("Menunggu") -> + Color(0xFFFF9800) + + autoStatus.contains("Jeda") -> + Color(0xFFFF9800) + + autoStatus.contains("lembab") -> + Color.Gray + + else -> + Color.LightGray + }, + RoundedCornerShape(50) + ) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = autoStatus, + fontSize = 14.sp, + color = Color.DarkGray + ) + } + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + if (selected == "Otomatis") { + RiwayatSectionOtomatis(viewModel.riwayatList) + } + } + + // Pop up Konfirmasi + if (showConfirmDialog) { + AlertDialog( + onDismissRequest = { + showConfirmDialog = false + }, + title = { + Text("Konfirmasi") + }, + text = { + Text("Anda akan beralih ke mode $targetMode. Tekan OK untuk melanjutkan.") + }, + confirmButton = { + TextButton( + onClick = { + selected = targetMode + mqttVm.setMode(targetMode) + showConfirmDialog = false + sensorVm.setMode(targetMode) + mqttVm.setMode(targetMode) + } + ) { + Text("OK") + } + }, + dismissButton = { + TextButton( + onClick = { + showConfirmDialog = false + } + ) { + Text("Batal") + } + } + ) + } + + // popup pengaturan jadwal + if (showSettingDialog) { + + ModalBottomSheet( + onDismissRequest = { + showSettingDialog = false + }, + sheetState = sheetState, + containerColor = Color.White + ) { + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.7f) // 70% tinggi layar + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + + Text( + text = "Pengaturan Mode Otomatis", + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(16.dp)) + + JadwalSection( + jadwalList = jadwalList, + loadJadwal = { loadJadwal() }, + context = context, + onDeleteClick = { id -> + selectedDeleteId = id + showDeleteDialog = true + } + ) + + Spacer(modifier = Modifier.height(15.dp)) + + JedaPenyiramanCard( + onJedaChanged = { + jedaDetik = it + } + ) + + Spacer(modifier = Modifier.height(15.dp)) + + AmbangBatasSection() + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + + TextButton( + onClick = { + showSettingDialog = false + } + ) { + Text("Batal") + } + + Button( + onClick = { + selected = "Otomatis" + viewModel.setMode("Otomatis") + showSettingDialog = false + } + ) { + Text("Simpan") + } + } + + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + //Aksi tombol delete + if (showDeleteDialog) { + DeleteDialog( + showDialog = true, + selectedId = selectedDeleteId, + context = context, + onDismiss = { + showDeleteDialog = false + }, + onSuccess = { + loadJadwal() + } + ) + } + + // KONTEN BERDASARKAN MODE + when (selected) { + + "Manual" -> { + + // Tampilan utama monitoring sensor + Box( + modifier = Modifier.fillMaxSize() + ) { + + // =================== SEMUA KONTEN UTAMA =================== + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + + // Tampilan utama monitoring sensor + SensorSection() + + Spacer(modifier = Modifier.height(15.dp)) + + RataRataSection( + snackbarHostState = snackbarHostState + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // Tampilan utama penyiraman sensor + + PenyiramanSection( + mode = mode, + pumpStatus = mqttState.pump, + isLoading = isLoading, + onPumpChange = { status -> + + // publish MQTT + mqttVm.setPump(status == "ON") + + // simpan riwayat + viewModel.onPumpChange( + context = context, + status = status, + mode = mode + ) + } + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // Tampilan utama riwayat sensor + RiwayatSection( + riwayatList = viewModel.riwayatList + ) + } + } + } + } + } + } + } + } + + + WarningPopup( + show = showPopup, + message = popupMessage, + + onDismiss = { + + // JIKA CHECKBOX AKTIF + if (disablePopup1Minute) { + + mutePopupUntil = + System.currentTimeMillis() + 60000 + } + + showPopup = false + }, + + disablePopup1Minute = disablePopup1Minute, + + onDisableChange = { + disablePopup1Minute = it + } + ) +} diff --git a/app/src/main/java/Page/Page Notifikasi.kt b/app/src/main/java/Page/Page Notifikasi.kt new file mode 100644 index 0000000..9c385e0 --- /dev/null +++ b/app/src/main/java/Page/Page Notifikasi.kt @@ -0,0 +1,377 @@ +package com.example.uitugasakhir.Page + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +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.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.SensorViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* +import androidx.compose.foundation.clickable + +import android.util.Log + + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NotifikasiScreen( + navController: NavController, + sensorVm: SensorViewModel +) { + + val list = sensorVm.warningList + + var selectedFilter by remember { + mutableStateOf("Semua") + } + + val filteredList = when (selectedFilter) { + + "Tanah" -> + list.filter { + it.message.contains("tanah", ignoreCase = true) + } + + "Udara" -> + list.filter { + it.message.contains("udara", ignoreCase = true) + } + + else -> list + } + + LaunchedEffect(Unit) { + sensorVm.loadLogs() + } + + + val selected = sensorVm.selectedItems + + LaunchedEffect(Unit) { + + sensorVm.readAllNotifications() + } + + LaunchedEffect(list) { + Log.d("NOTIF_DEBUG", "Jumlah notif: ${list.size}") + list.forEach { + Log.d("NOTIF_DEBUG", "Item: ${it.id} - ${it.message}") + } + } + + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + + + Box(modifier = Modifier.fillMaxSize()) { + + // ========================= + // HEADER HIJAU + // ========================= + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp) + .background(Color(0xFF2F6F4E)), + contentAlignment = Alignment.TopCenter + ) { + + Image( + painter = painterResource(id = R.drawable.back), + contentDescription = "back", + modifier = Modifier + .size(18.dp) + .offset(x = -160.dp, y = 30.dp) + .clickable { + navController.popBackStack() + } + ) + + Text( + text = "Notifikasi", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(top = 30.dp) + ) + + IconButton( + onClick = { + val deleted = list.toList() + + sensorVm.deleteAllWarnings() + + scope.launch { + val res = snackbarHostState.showSnackbar( + message = "Semua notifikasi dihapus", + actionLabel = "UNDO", + duration = SnackbarDuration.Short + ) + + if (res == SnackbarResult.ActionPerformed) { + sensorVm.restoreDeleted(deleted) + } + } + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 20.dp, end = 16.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Hapus Semua", + tint = Color.White + ) + } + } + + // ========================= + // CARD PUTIH FLOATING + // ========================= + Card( + modifier = Modifier + .fillMaxSize() + .padding(top = 70.dp), + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp), + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + + // ========================= + // BOTTOM DELETE BAR + // ========================= + bottomBar = { + if (selected.isNotEmpty()) { + BottomAppBar(containerColor = Color(0xFF1976D2)) { + + Text( + text = "${selected.size} dipilih", + color = Color.White, + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) + + IconButton(onClick = { + val deleted = list.filter { it.id in selected } + + sensorVm.deleteSelected() + + scope.launch { + val res = snackbarHostState.showSnackbar( + message = "Terhapus", + actionLabel = "UNDO" + ) + + if (res == SnackbarResult.ActionPerformed) { + sensorVm.restoreDeleted(deleted) + } + } + }) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + tint = Color.White + ) + } + } + } + } + ) { padding -> + + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + + FilterChip( + selected = selectedFilter == "Semua", + onClick = { + selectedFilter = "Semua" + }, + label = { + Text("Semua") + } + ) + + FilterChip( + selected = selectedFilter == "Tanah", + onClick = { + selectedFilter = "Tanah" + }, + label = { + Text("🌱 Tanah") + } + ) + + FilterChip( + selected = selectedFilter == "Udara", + onClick = { + selectedFilter = "Udara" + }, + label = { + Text("🌡 Udara") + } + ) + } + + LazyColumn( + modifier = Modifier.weight(1f) + ) { + + items(filteredList, key = { it.id }) { item -> + + Log.d("NOTIF_ITEM", "Render item: ${item.id}") + + val isSelected = selected.contains(item.id) + + val bgColor by animateColorAsState( + if (isSelected) Color(0xFFE3F2FD) else Color.White + ) + + // ========================= + // SWIPE DELETE (VERSI STABIL) + // ========================= + SwipeToDismissBox( + state = rememberSwipeToDismissBoxState( + confirmValueChange = { value -> + if (value == SwipeToDismissBoxValue.EndToStart || + value == SwipeToDismissBoxValue.StartToEnd + ) { + sensorVm.deleteWarning(item) + + scope.launch { + snackbarHostState.showSnackbar("Dihapus") + } + } + true + } + ), + + backgroundContent = { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Red) + .padding(end = 20.dp), + contentAlignment = Alignment.CenterEnd + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + tint = Color.White + ) + } + } + ) { + + // ========================= + // ITEM UI + // ========================= + Row( + modifier = Modifier + .fillMaxWidth() + .background(bgColor) + .pointerInput(Unit) { + detectTapGestures( + onTap = { + if (selected.isNotEmpty()) { + sensorVm.toggleSelection(item) + } + }, + onLongPress = { + sensorVm.toggleSelection(item) + } + ) + } + .padding(16.dp) + ) { + + val icon = when { + + item.message.contains("tanah", true) -> "🌱" + + item.message.contains("udara", true) -> "🌡" + + else -> "⚠️" + + } + + Text(icon, fontSize = 20.sp) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + + Text( + text = item.status, + fontSize = 12.sp, + color = Color(0xFF2F6F4E), + fontWeight = FontWeight.Bold + ) + + Text( + text = item.message, + fontSize = 14.sp + ) + + Text( + text = formatTime(item.time), + fontSize = 12.sp, + color = Color.Gray + ) + } + + if (isSelected) { + Text("✔", color = Color(0xFF1976D2)) + } + } + } + + Divider() + } + } + } + } + } + } +} + +// ========================= +// FORMAT TIME +// ========================= +fun formatTime(time: Long): String { + val sdf = SimpleDateFormat("HH:mm", Locale.getDefault()) + return sdf.format(Date(time)) +} \ No newline at end of file diff --git a/app/src/main/java/Page/Page Otomatis.kt b/app/src/main/java/Page/Page Otomatis.kt new file mode 100644 index 0000000..925b859 --- /dev/null +++ b/app/src/main/java/Page/Page Otomatis.kt @@ -0,0 +1,324 @@ +package com.example.uitugasakhir + + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.mutableStateOf +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Inter +import androidx.navigation.NavController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.draw.clip +import androidx.compose.foundation.background +import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue + + +@Composable +fun OtomatisScreen(navController: NavController) { + + var selected by remember { mutableStateOf("Manual") } + var selectedPump by remember { mutableStateOf("Matikan pompa") } + + Box( + modifier = Modifier + .fillMaxSize() + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + contentAlignment = Alignment.TopCenter + ) { + + // Card hijau di atas + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + + Image( + painter = painterResource(id = R.drawable.back), + contentDescription = "kembali", + modifier = Modifier + .size(15.dp) + .offset(x = -160.dp, y = 1.dp) + .clickable { + navController.popBackStack() + } + ) + + Text( + text = "Adityaaaa", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + } + + Card( + modifier = Modifier + .padding(top = 64.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Column( + modifier = Modifier + .padding(top = 15.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + text = "Mode Penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color(0XFF6B6666).copy(alpha = 0.7f), + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Box( + modifier = Modifier + .wrapContentHeight() + .padding(bottom = 15.dp) + .padding(horizontal = 25.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .offset(y = 23.dp) + .height(55.dp) + .clip(RoundedCornerShape(50.dp)) + .background(Color(0xFFD9D9D9)) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + // OTOMATIS + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(20.dp)) + .background( + if (selected == "Otomatis") Color(0xFF2F6F4E) else Color.Transparent + ) + .clickable { selected = "Otomatis" + navController.navigate("otomatis") }, + + contentAlignment = Alignment.Center + ) { + Text( + text = "Otomatis", + color = if (selected == "Otomatis") Color.White else Color.Black + ) + } + + // MANUAL + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(20.dp)) + .background( + if (selected == "Manual") Color(0xFF2F6F4E) else Color.Transparent + ) + .clickable { selected = "Manual" + navController.navigate("manual")}, + contentAlignment = Alignment.Center + ) { + Text( + text = "Manual", + color = if (selected == "Manual") Color.White else Color.Black + ) + } + } + }} + + Text( + text = "Pemantauan sensor", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color(0XFF6B6666).copy(alpha = 0.7f), + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Box( + modifier = Modifier + .wrapContentHeight() + .padding(bottom = 15.dp) + .padding(horizontal = 25.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) {}} + + + Text( + text = "Penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color(0XFF6B6666).copy(alpha = 0.7f), + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Box( + modifier = Modifier + .wrapContentHeight() + .padding(bottom = 15.dp) + .padding(horizontal = 25.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp) + .offset(y = 23.dp) + .height(55.dp) + .clip(RoundedCornerShape(50.dp)) + .background(Color(0xFFD9D9D9)) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + // OTOMATIS + Box( + modifier = Modifier + .weight(1f) + .height(60.dp) + .clip(RoundedCornerShape(20.dp)) + .background( + if (selectedPump == "Hidupkan pompa") Color(0xFF2F6F4E) else Color.Transparent + ) + .clickable { selectedPump = "Hidupkan pompa" }, + contentAlignment = Alignment.Center + ) { + Text( + text = "Hidupkan pompa", + color = if (selectedPump == "Hidupkan pompa") Color.White else Color.Black + ) + } + + // MANUAL + Box( + modifier = Modifier + .weight(1f) + .height(60.dp) + .clip(RoundedCornerShape(20.dp)) + .background( + if (selectedPump == "Matikan pompa") Color(0xFFD81F23) else Color.Transparent + ) + .clickable { selectedPump = "Matikan pompa" }, + contentAlignment = Alignment.Center + ) { + Text( + text = "Matikan pompa", + color = if (selectedPump == "Matikan pompa") Color.White else Color.Black + ) + } + } + }} + + Text( + text = "Riwayat penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color(0XFF6B6666).copy(alpha = 0.7f), + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Box( + modifier = Modifier + .wrapContentHeight() + .padding(bottom = 15.dp) + .padding(horizontal = 25.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) {}} + } + } + } +} + diff --git a/app/src/main/java/Page/Page Pengaturan.kt b/app/src/main/java/Page/Page Pengaturan.kt new file mode 100644 index 0000000..92175bf --- /dev/null +++ b/app/src/main/java/Page/Page Pengaturan.kt @@ -0,0 +1,162 @@ +package com.example.uitugasakhir + + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.res.painterResource +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Inter +import androidx.navigation.NavController +import android.widget.Toast +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource + +import com.example.uitugasakhir.pengaturan.AkunCardSection +import com.example.uitugasakhir.pengaturan.BahasaCardSection +import com.example.uitugasakhir.pengaturan.BantuanCardSection +import com.example.uitugasakhir.pengaturan.NotifikasiCardSection +import com.example.uitugasakhir.ViewModel.MainViewModel +import androidx.lifecycle.viewmodel.compose.viewModel + +import com.example.uitugasakhir.ViewModel.SensorViewModel + + + +@Composable +fun PengaturanScreen(navController: NavController) { + + val context = LocalContext.current + val mainViewModel: MainViewModel = viewModel() + val sensorViewModel: SensorViewModel = viewModel() + + Box( + modifier = Modifier + .fillMaxSize() + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + contentAlignment = Alignment.TopCenter + ) { + + // Card hijau di atas + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + + Image( + painter = painterResource(id = R.drawable.back), + contentDescription = "kembali", + modifier = Modifier + .size(15.dp) + .offset(x = -160.dp, y = 1.dp) + .clickable { + navController.popBackStack() + } + ) + + Text( + text = stringResource(R.string.setting), + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + } + + Card( + modifier = Modifier + .padding(top = 64.dp) + .verticalScroll(rememberScrollState()) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Column( + modifier = Modifier + .padding(top = 15.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + // Tampilan akun + AkunCardSection(viewModel = mainViewModel) + + Spacer(modifier = Modifier.height(15.dp)) + + // Tampilan Bahasa + BahasaCardSection( + viewModel = mainViewModel + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // Tampilan Notifikasi + NotifikasiCardSection( + viewModel = mainViewModel, + sensorViewModel = sensorViewModel + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // Tampilan Bantuan + BantuanCardSection( + viewModel = mainViewModel, + navController = navController + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.out), + color = Color.Red, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(28.dp) + .clickable { + Toast.makeText(context, "Berhasil logout", Toast.LENGTH_SHORT).show() + + // contoh pindah ke login + navController.navigate("login") { + popUpTo(0) + } + } + ) + } + } + } +} + diff --git a/app/src/main/java/Page/Page Riwayat.kt b/app/src/main/java/Page/Page Riwayat.kt new file mode 100644 index 0000000..ce6a545 --- /dev/null +++ b/app/src/main/java/Page/Page Riwayat.kt @@ -0,0 +1,856 @@ +package com.example.uitugasakhir + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.runtime.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.navigation.NavController +import androidx.compose.foundation.background +import android.content.Context +import android.os.Environment +import android.widget.Toast +import java.io.File +import java.io.FileWriter +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.draw.scale +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.uitugasakhir.model.RiwayatItem +import com.example.uitugasakhir.ViewModel.MainViewModel +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import android.content.ContentValues +import android.os.Build +import android.provider.MediaStore +import android.util.Log + +import java.text.SimpleDateFormat +import java.util.Locale + +import androidx.compose.material.icons.filled.Download +import com.example.app.ui.theme.Inria_Regular + + +@Composable +fun RiwayatScreen(navController: NavController) { + + val viewModel: MainViewModel = viewModel() + + //Riwayat hari ini, minggu ini dan bulan ini + + val data = viewModel.riwayatList + + val penyiramanData = viewModel.riwayatList.filter { + it.aksi == "Pompa Dinyalakan" + } + + val dataMingguan = viewModel.getDataMingguan(penyiramanData) + val dataHarian = viewModel.getDataHarian(penyiramanData) + + // Halaman tabel selanjutnya dan sebelumnya + var currentPage by remember { mutableStateOf(0) } + val pageSize = 10 + + var isDownloading by remember { mutableStateOf(false) } + var isSuccess by remember { mutableStateOf(null) } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + + val scale by animateFloatAsState( + targetValue = if (isDownloading) 0.9f else 1f, + animationSpec = tween(200) + ) + + // STATE + + var showFileNameDialog by remember { mutableStateOf(false) } + + var fileName by remember { mutableStateOf("") } + + var selectedFilter by remember { mutableStateOf("Semua") } + var selectedDateFilter by remember { mutableStateOf("Semua") } + + // Total penyiraman + val sdfServer = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + + val dataHariIni = penyiramanData.count { + + val split = it.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: return@count false + + val date = sdfServer.parse(tanggal) ?: return@count false + + val cal1 = java.util.Calendar.getInstance() + val cal2 = java.util.Calendar.getInstance() + + cal1.time = date + cal2.time = java.util.Date() + + cal1.get(java.util.Calendar.DAY_OF_YEAR) == + cal2.get(java.util.Calendar.DAY_OF_YEAR) && + cal1.get(java.util.Calendar.YEAR) == + cal2.get(java.util.Calendar.YEAR) + } + + val dataMingguIni = penyiramanData.count { + + val split = it.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: return@count false + + val date = sdfServer.parse(tanggal) ?: return@count false + + val cal1 = java.util.Calendar.getInstance() + val cal2 = java.util.Calendar.getInstance() + + cal1.time = date + cal2.time = java.util.Date() + + cal1.get(java.util.Calendar.WEEK_OF_YEAR) == + cal2.get(java.util.Calendar.WEEK_OF_YEAR) && + cal1.get(java.util.Calendar.YEAR) == + cal2.get(java.util.Calendar.YEAR) + } + + val dataBulanIni = penyiramanData.count { + + val split = it.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: return@count false + + val date = sdfServer.parse(tanggal) ?: return@count false + + val cal1 = java.util.Calendar.getInstance() + val cal2 = java.util.Calendar.getInstance() + + cal1.time = date + cal2.time = java.util.Date() + + cal1.get(java.util.Calendar.MONTH) == + cal2.get(java.util.Calendar.MONTH) && + cal1.get(java.util.Calendar.YEAR) == + cal2.get(java.util.Calendar.YEAR) + } + + val totalData = penyiramanData.size + + // ================= FILTER FIX ================= + val filteredData = data.filter { item -> + + val split = item.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: "" + val jam = split.getOrNull(1) ?: "" + + val matchFilter = + selectedFilter == "Semua" || + item.mode == selectedFilter || + item.status == selectedFilter + + + val matchDate = when (selectedDateFilter) { + + "Hari ini" -> { + val todayStr = java.text.SimpleDateFormat("dd-MM-yyyy") + .format(java.util.Date()) + tanggal == todayStr + } + + else -> true + } + + matchFilter && matchDate + } + + val totalPage = (filteredData.size + pageSize - 1) / pageSize + + val pagedData = filteredData + .drop(currentPage * pageSize) + .take(pageSize) + + LaunchedEffect(Unit) { + viewModel.loadRiwayat(context) + } + + // ================= UI ================= + Box(modifier = Modifier.fillMaxSize()) { + + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.align(Alignment.BottomCenter) + ) + + // HEADER + Card( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors(Color(0xFF2F6F4E)) + ) { + Box( + modifier = Modifier.fillMaxSize().padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + + Image( + painter = painterResource(id = R.drawable.back), + contentDescription = "back", + modifier = Modifier + .size(15.dp) + .offset(x = (-160).dp) + .clickable { navController.popBackStack() } + ) + + Text( + text = "Riwayat", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + + // CONTENT + Card( + modifier = Modifier + .padding(top = 58.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp), + colors = CardDefaults.cardColors(Color.White) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + .animateContentSize() + ) { + + Text("Total Penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // SUMMARY FIX + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color(0xFFF2F2F2)), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column(modifier = Modifier.padding(12.dp)) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceAround + ) { + + SummaryMini("Hari", dataHariIni) + SummaryMini("Minggu", dataMingguIni) + SummaryMini("Bulan", dataBulanIni) + SummaryMini("Total", totalData) + } + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text("Grafik Penyiraman Minggu Ini", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color(0xFFF2F2F2)), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column(Modifier.padding(16.dp)) { + + Text("Minggu Ini", fontWeight = FontWeight.Normal) + + Spacer(Modifier.height(12.dp)) + + BarChart(dataHarian) + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text("Grafik Penyiraman Bulan Ini", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(Color(0xFFF2F2F2)), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column(Modifier.padding(16.dp)) { + + Text("Bulan Ini", fontWeight = FontWeight.Normal) + + Spacer(Modifier.height(12.dp)) + + BarChart(dataMingguan) + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = "Mode", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(15.dp)) + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 4.dp) + ) { + items(listOf("Semua", "Otomatis", "Manual", "Berhasil", "Gagal")) { item -> + + FilterChipModern( + label = item, + selected = selectedFilter == item, + onClick = { selectedFilter = item } + ) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = "Periode", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(15.dp)) + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 4.dp) + ) { + items(listOf("Semua", "Hari ini", "Minggu ini", "Bulan ini")) { item -> + + FilterChipModern( + label = item, + selected = selectedDateFilter == item, + onClick = { selectedDateFilter = item } + ) + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + text = "Data: ${filteredData.size}", + fontSize = 13.sp, + color = Color.Gray + ) + + Button( + onClick = { + + if (filteredData.isEmpty()) { + + Toast.makeText( + context, + "Tidak ada data", + Toast.LENGTH_SHORT + ).show() + + } else { + + fileName = "" + showFileNameDialog = true + } + }, enabled = !isDownloading, + modifier = Modifier + .scale(scale) + .height(42.dp), + shape = RoundedCornerShape(20.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF2F6F4E) + ), + contentPadding = PaddingValues(horizontal = 14.dp) + ) { + + Icon( + imageVector = Icons.Default.Download, + contentDescription = "Download", + tint = Color.White, + modifier = Modifier.size(18.dp) + ) + + Spacer(modifier = Modifier.width(6.dp)) + + Text( + text = if (isDownloading) "Loading..." else "Unduh", + fontSize = 12.sp, + color = Color.White + ) + } + } + + Spacer(modifier = Modifier.height(15.dp)) + + // HEADER TABLE + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFF2F6F4E)) + .padding(8.dp) + ) { + TableText("No", 0.1f) + TableText("Tanggal", 0.3f) + TableText("Jam", 0.2f) + TableText("Mode", 0.2f) + TableText("Status", 0.2f) + TableText("", 0.1f) + } + + pagedData.forEachIndexed { index, item -> + + val split = item.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: "-" + val jam = split.getOrNull(1) ?: "-" + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + colors = CardDefaults.cardColors(Color(0xFFF8F8F8)), + shape = RoundedCornerShape(12.dp) + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + TableText("${index + 1 + (currentPage * pageSize)}", 0.1f) + TableText(tanggal, 0.25f) + TableText(jam, 0.2f) + TableText(item.mode, 0.2f) + TableText(item.status, 0.15f) + + Box( + modifier = Modifier.weight(0.1f), + contentAlignment = Alignment.Center + ) { + IconButton( + onClick = { + viewModel.deleteRiwayat(context, item) + } + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Hapus", + tint = Color.Red, + modifier = Modifier.size(18.dp) + ) + } + } + } + } + } + + Spacer(Modifier.height(10.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + "<", + modifier = Modifier + .clickable { + if (currentPage > 0) currentPage-- + } + .padding(12.dp), + fontSize = 20.sp + ) + + Text( + "Page ${currentPage + 1} / $totalPage", + modifier = Modifier.padding(12.dp) + ) + + Text( + ">", + modifier = Modifier + .clickable { + if (currentPage < totalPage - 1) currentPage++ + } + .padding(12.dp), + fontSize = 20.sp + ) + } + } + } + } + + if (showFileNameDialog) { + + AlertDialog( + + onDismissRequest = { + showFileNameDialog = false + }, + + title = { + Text("Nama File") + }, + + text = { + + OutlinedTextField( + + value = fileName, + + onValueChange = { + fileName = it + }, + + label = { + Text("Masukkan nama file") + }, + + singleLine = true + + ) + }, + + confirmButton = { + + Button( + + onClick = { + + scope.launch { + + isDownloading = true + + val success = exportToCSV( + + context, + + filteredData, + + if (fileName.isBlank()) + "Riwayat.csv" + else + "$fileName.csv" + + ) + + isDownloading = false + + if (success) { + + Toast.makeText( + context, + "Berhasil disimpan", + Toast.LENGTH_LONG + ).show() + + } else { + + Toast.makeText( + context, + "Gagal menyimpan", + Toast.LENGTH_LONG + ).show() + } + + showFileNameDialog = false + } + + } + + ) { + + Text("Simpan") + } + + }, + + dismissButton = { + + TextButton( + + onClick = { + + showFileNameDialog = false + + } + + ) { + + Text("Batal") + + } + + } + + ) + + } +} + +// ================= COMPONENT FIX ================= + +@Composable +fun SummaryMini(label: String, value: Int) { + + val isToday = label == "Hari" + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "$value", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = if (isToday) Color(0xFF2F6F4E) else Color.Black + ) + Text( + text = label, + fontSize = 10.sp, + color = Color.Gray + ) + } +} + +@Composable +fun FilterChip(label: String, selected: Boolean, onClick: () -> Unit) { + Card( + modifier = Modifier.clickable { onClick() }, + colors = CardDefaults.cardColors( + if (selected) Color(0xFF2F6F4E) else Color.LightGray + ) + ) { + Text( + text = label, + modifier = Modifier.padding(8.dp), + color = if (selected) Color.White else Color.Black + ) + } +} + +@Composable +fun RowScope.TableText( + text: String, + weight: Float, + color: Color = Color.Black +) { + Text( + text = text, + modifier = Modifier.weight(weight), // ✔ aman karena RowScope + fontSize = 12.sp, + color = color, + textAlign = TextAlign.Center + ) +} + +@Composable +fun TableText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Black +) { + Text( + text = text, + modifier = modifier, + fontSize = 12.sp, + color = color, + textAlign = TextAlign.Center + ) +} + + +// ================= EXPORT ================= +fun exportToCSV( + context: Context, + data: List, + fileName: String +): Boolean { + + return try { + + val values = ContentValues().apply { + + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "text/csv") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put( + MediaStore.MediaColumns.RELATIVE_PATH, + Environment.DIRECTORY_DOWNLOADS + ) + } + } + + val collection = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.Downloads.EXTERNAL_CONTENT_URI + } else { + MediaStore.Files.getContentUri("external") + } + + val uri = context.contentResolver.insert( + collection, + values + ) ?: return false + + context.contentResolver.openOutputStream(uri)?.use { output -> + + output.write("No,Tanggal,Jam,Mode,Status\n".toByteArray()) + + data.forEachIndexed { index, item -> + + val split = item.waktu.split(" ") + + val tanggal = split.getOrNull(0) ?: "-" + val jam = split.getOrNull(1) ?: "-" + + val row = + "${index + 1},$tanggal,$jam,${item.mode},${item.status}\n" + + output.write(row.toByteArray()) + } + + output.flush() + } + + Log.d("CSV", "Berhasil menyimpan: $fileName") + + true + + } catch (e: Exception) { + + e.printStackTrace() + Log.e("CSV", "Gagal export", e) + + false + } +} + +@Composable +fun FilterChipModern( + label: String, + selected: Boolean, + onClick: () -> Unit +) { + Surface( + shape = RoundedCornerShape(20.dp), + color = if (selected) Color(0xFF2F6F4E) else Color(0xFFEDEDED), + shadowElevation = if (selected) 4.dp else 0.dp, + modifier = Modifier.clickable { onClick() } + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp), + fontSize = 12.sp, + color = if (selected) Color.White else Color.Black + ) + } +} + +@Composable +fun BarChart(data: List>) { + + val maxValue = (data.maxOfOrNull { it.second } ?: 1).toFloat() + + Row( + modifier = Modifier + .fillMaxWidth() + .height(150.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.Bottom + ) { + + data.forEach { (label, value) -> + + val heightRatio = value / maxValue + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text("$value", fontSize = 10.sp) + + Spacer(modifier = Modifier.height(4.dp)) + + Box( + modifier = Modifier + .width(20.dp) + .height((100 * heightRatio).dp) + .background(Color(0xFF2F6F4E), RoundedCornerShape(6.dp)) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text(label, fontSize = 10.sp) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/Page/Page Statistik.kt b/app/src/main/java/Page/Page Statistik.kt new file mode 100644 index 0000000..ff0d4dd --- /dev/null +++ b/app/src/main/java/Page/Page Statistik.kt @@ -0,0 +1,2 @@ +package Page + diff --git a/app/src/main/java/Page/Page dashboard.kt b/app/src/main/java/Page/Page dashboard.kt new file mode 100644 index 0000000..9a2d255 --- /dev/null +++ b/app/src/main/java/Page/Page dashboard.kt @@ -0,0 +1,290 @@ +package com.example.uitugasakhir + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.res.painterResource +import com.example.app.ui.theme.Inria_Bold +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Inter +import androidx.navigation.NavController +import androidx.compose.runtime.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.foundation.gestures.detectTapGestures +import com.example.uitugasakhir.ViewModel.SensorViewModel +import com.example.uitugasakhir.ViewModel.MainViewModel + + + + +@Composable +fun DashboardScreen( + navController: NavController, + sensorVm: SensorViewModel, + mainVm: MainViewModel +) { + + val unreadCount = sensorVm.unreadCount + + Box(modifier = Modifier.fillMaxSize()) { + + // HEADER + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = "Beranda", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + } + + // BODY + Card( + modifier = Modifier + .padding(top = 64.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Image( + painter = painterResource(id = R.drawable.kopikakao), + contentDescription = "Logo", + modifier = Modifier.size(200.dp) + ) + + Text( + text = "Selamat Datang!", + fontSize = 20.sp, + fontFamily = Inria_Bold, + fontWeight = FontWeight.Bold, + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = "Monitoring & Penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = "Kopi & Kakao", + fontSize = 15.sp, + fontFamily = Inria_Regular, + modifier = Modifier + .align(Alignment.Start) + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(30.dp)) + + // 🔥 CARD MENU (SUDAH FIX) + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 25.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MenuCard( + "Monitoring &\nPenyiraman", + modifier = Modifier.weight(1f), + imageRes = R.drawable.sprinkler_irigation, + onClick = { navController.navigate("manual") } + ) + + MenuCard( + title = "Notifikasi", + modifier = Modifier.weight(1f), + imageRes = R.drawable.notification, + badgeCount = unreadCount, + onClick = { + navController.navigate("notifikasi") + } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(15.dp) + ) { + MenuCard( + "Riwayat", + modifier = Modifier.weight(1f), + imageRes = R.drawable.chronometer, + onClick = { navController.navigate("riwayat") } + ) + + MenuCard( + "Pengaturan", + modifier = Modifier.weight(1f), + imageRes = R.drawable.cogwheel, + onClick = { navController.navigate("pengaturan") } + ) + } + } + } + } + } + } +} + +@Composable +fun MenuCard( + title: String, + modifier: Modifier = Modifier, + badgeCount: Int = 0, + onClick: () -> Unit, + imageRes: Int +) { + var pressed by remember { mutableStateOf(false) } + + val scale by animateFloatAsState( + targetValue = if (pressed) 1.1f else 1f, // zoom + animationSpec = spring( + dampingRatio = 0.5f, + stiffness = 300f + ), + label = "" + ) + + Box( + modifier = modifier + .height(150.dp) + .width(140.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .pointerInput(Unit) { + detectTapGestures( + onPress = { + pressed = true + tryAwaitRelease() // tunggu sampai dilepas + pressed = false + }, + onTap = { + onClick() + } + ) + } + ) { + + Card( + modifier = Modifier.fillMaxSize(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFDCEFE3) + ) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + + Column { + + Image( + painter = painterResource(id = imageRes), + contentDescription = title, + modifier = Modifier + .size(30.dp) + .align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = title, + textAlign = TextAlign.Center + ) + } + } + } + + // 🔴 BADGE + if (badgeCount > 0) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .offset(x = (-6).dp, y = 6.dp) + .size(22.dp) + .background(Color.Red, RoundedCornerShape(50)), + contentAlignment = Alignment.Center + ) { + Text( + text = badgeCount.toString(), + color = Color.White, + fontSize = 11.sp, + fontWeight = FontWeight.Bold + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/Page/Page intro.kt b/app/src/main/java/Page/Page intro.kt new file mode 100644 index 0000000..aa8dbeb --- /dev/null +++ b/app/src/main/java/Page/Page intro.kt @@ -0,0 +1,45 @@ +package com.example.uitugasakhir + +import android.window.SplashScreen +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.graphics.Color +import androidx.compose.foundation.background +import androidx.compose.foundation.Image +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.layout.ContentScale + + + +@Composable +fun SplashScreen() { + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color(0xFF2F6F4E)) + .padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Image( + painter = painterResource(id = R.drawable.logo_kopi), + contentDescription = "Logo Kopi", + modifier = Modifier + .fillMaxWidth() + .height(250.dp), + contentScale = ContentScale.Crop + ) + + CircularProgressIndicator( + color = Color.White, + strokeWidth = 4.dp + ) + } +} + diff --git a/app/src/main/java/Page/Page login.kt b/app/src/main/java/Page/Page login.kt new file mode 100644 index 0000000..432a8f1 --- /dev/null +++ b/app/src/main/java/Page/Page login.kt @@ -0,0 +1,484 @@ +package com.example.uitugasakhir + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.res.painterResource +import com.example.app.ui.theme.Inria_Bold +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Inter +import androidx.navigation.NavController +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.ui.platform.LocalContext +import android.content.Context +import android.widget.Toast +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.android.volley.Request +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.graphics.ColorFilter +import okhttp3.ResponseBody +import retrofit2.Call +import org.json.JSONObject +import retrofit2.Callback +import retrofit2.Response +import com.example.uitugasakhir.api.RetrofitClient + + +@Composable +fun LoginScreen(navController: NavController) { + + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + var passwordVisible by remember { mutableStateOf(false) } + + val context = LocalContext.current + + fun loginUser( + email: String, + password: String, + context: Context, + navController: NavController + ) { + val url = "http://kopikaosmartsystem.my.id/login.php" + + val request = object : StringRequest( + Request.Method.POST, url, + + { response -> + + try { + val jsonObject = JSONObject(response) + + if (jsonObject.getString("status") == "success") { + + val nama = jsonObject.getString("nama") + val emailUser = jsonObject.getString("email") + + val sharedPreferences = + context.getSharedPreferences("USER_DATA", Context.MODE_PRIVATE) + + val editor = sharedPreferences.edit() + editor.putString("nama", nama) + editor.putString("email", emailUser) + editor.apply() + + Toast.makeText(context, "Login Berhasil", Toast.LENGTH_SHORT).show() + + navController.navigate("dashboard") + + } else { + Toast.makeText( + context, + jsonObject.getString("message"), + Toast.LENGTH_SHORT + ).show() + } + + } catch (e: Exception) { + Toast.makeText( + context, + "Response error: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + }, + + { error -> + Toast.makeText( + context, + "Error: ${error.message}", + Toast.LENGTH_LONG + ).show() + } + + ) { + override fun getParams(): MutableMap { + return hashMapOf( + "email" to email, + "password" to password + ) + } + } + + val queue = Volley.newRequestQueue(context) + queue.add(request) + } + + Box( + modifier = Modifier + .fillMaxSize() + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + contentAlignment = Alignment.TopCenter + ) { + + // Card hijau di atas + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + + Text( + text = "Halaman Login", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + } + + Card( + modifier = Modifier + .padding(top = 58.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Image( + painter = painterResource(id = R.drawable.kopikakao), + contentDescription = "Logo Kopi", + Modifier.size(200.dp) + ) + + Text( + text = "Selamat Datang!", + fontSize = 20.sp, + fontFamily = Inria_Bold, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = "Silahkan masukkan email dan password anda", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black.copy(alpha = 0.7f) + ) + + Spacer(modifier = Modifier.height(30.dp)) + + Box( + modifier = Modifier + .wrapContentHeight() + .padding(bottom = 50.dp) + .padding(horizontal = 40.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier + .size(width = 276.dp, height = 259.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + + Spacer(modifier = Modifier.width(10.dp)) + + Image( + painter = painterResource(id = R.drawable.mail), + contentDescription = "Email", + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp), + colorFilter = ColorFilter.tint(Color.Gray) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + BasicTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + decorationBox = { innerTextField -> + if (email.isEmpty()) { + Text( + "Email", + color = Color.Gray + ) + } + innerTextField() + } + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Card( + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + + Spacer(modifier = Modifier.width(10.dp)) + + Image( + painter = painterResource(id = R.drawable.password), + contentDescription = "Password", + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp), + colorFilter = ColorFilter.tint(Color.Gray) + + ) + + Spacer(modifier = Modifier.width(10.dp)) + + BasicTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.weight(1f), + singleLine = true, + + // 🔒 Enkripsi tampilan password + visualTransformation = if (passwordVisible) + VisualTransformation.None + else + PasswordVisualTransformation(), + + decorationBox = { innerTextField -> + if (password.isEmpty()) { + Text( + "Password", + color = Color.Gray + ) + } + innerTextField() + } + ) + + // 👁️ Icon show/hide password + IconButton(onClick = { + passwordVisible = !passwordVisible + }) { + Icon( + painter = painterResource( + id = if (passwordVisible) + R.drawable.ic_eye_open // mata terbuka + else + R.drawable.ic_eye_closed // mata tertutup + ), + contentDescription = "Toggle Password", + modifier = Modifier.size(18.dp), + tint = Color.Gray + ) + } + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Lupa password?", + fontSize = 14.sp, + fontFamily = Inter, + color = Color.White, + modifier = Modifier + .offset(x = 70.dp) + .clickable { + navController.navigate("forgot_password") + } + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Card( + onClick = { loginUser(email, password, context, navController) }, + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF22C55E) + ) + ) { + Text( + text = "Masuk", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + color = Color.White, + modifier = Modifier + .padding(top = 13.dp) + .align(Alignment.CenterHorizontally) + ) + } + + } + } + } + + Row { + + Text( + text = "Belum mempunyai akun?", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + color = Color.Black.copy(alpha = 0.7f) + ) + + Spacer(modifier = Modifier.width(5.dp)) + + Text( + text = "Buat akun", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + color = Color(0xFF22C55E), + modifier = Modifier.clickable { + navController.navigate("register") + } + ) + } + } + } + } +} + +@Composable +fun ForgotPasswordScreen(navController: NavController) { + + var email by remember { mutableStateOf("") } + var message by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + + fun sendForgotPassword(email: String, callback: (String) -> Unit) { + RetrofitClient.api.forgotPassword(email.trim()) + .enqueue(object : Callback { + + override fun onResponse( + call: Call, + response: Response + ) { + callback(response.body()?.string() ?: "No response") + } + + override fun onFailure( + call: Call, + t: Throwable + ) { + callback("Error: ${t.message}") + } + }) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Center + ) { + + Text( + text = "Reset Password", + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(20.dp)) + + TextField( + value = email, + onValueChange = { email = it }, + placeholder = { Text("Masukkan email") }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Button( + onClick = { + isLoading = true + sendForgotPassword(email) { + message = it + isLoading = false + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Kirim Link Reset") + } + + Spacer(modifier = Modifier.height(20.dp)) + + if (isLoading) { + CircularProgressIndicator() + } + + Text(message) + } +} + diff --git a/app/src/main/java/Page/Page register.kt b/app/src/main/java/Page/Page register.kt new file mode 100644 index 0000000..c9a6348 --- /dev/null +++ b/app/src/main/java/Page/Page register.kt @@ -0,0 +1,460 @@ +package com.example.uitugasakhir + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import com.example.app.ui.theme.Inria_Bold +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Inter +import androidx.navigation.NavController +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.ColorFilter +import android.content.Context +import android.widget.Toast +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.android.volley.Request + + + +@Composable +fun RegisterScreen(navController: NavController) { + + var nama by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } + var confirmPasswordVisible by remember { mutableStateOf(false) } + + val context = LocalContext.current + + fun registerUser( + nama: String, + email: String, + password: String, + confirmPassword: String, + context: Context, + navController: NavController + ) { + if (password != confirmPassword) { + Toast.makeText(context, "Password tidak sama", Toast.LENGTH_SHORT).show() + return + } + + val url = "http://kopikaosmartsystem.my.id/register.php" // GANTI IP KAMU + + val request = object : StringRequest( + Request.Method.POST, url, + { response -> + if (response.contains("Register berhasil")) { + Toast.makeText(context, "Berhasil daftar", Toast.LENGTH_SHORT).show() + navController.navigate("login") + } else { + Toast.makeText(context, response, Toast.LENGTH_SHORT).show() + } + }, + { error -> + Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_LONG).show() + } + ) { + override fun getParams(): MutableMap { + return hashMapOf( + "nama" to nama, + "email" to email, + "password" to password + ) + } + } + + val queue = Volley.newRequestQueue(context) + queue.add(request) + } + + Box( + modifier = Modifier + .fillMaxSize() + ) { + + Box( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + contentAlignment = Alignment.TopCenter + ) { + + // Card hijau di atas + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + + Text( + text = "Halaman Login", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + } + + Card( + modifier = Modifier + .padding(top = 58.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Image( + painter = painterResource(id = R.drawable.kopikakao), + contentDescription = "Logo Kopi", + Modifier.size(200.dp) + ) + + Text( + text = "Selamat Datang!", + fontSize = 20.sp, + fontFamily = Inria_Bold, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = "Silahkan masukkan email dan password anda", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black.copy(alpha = 0.7f) + ) + + Spacer(modifier = Modifier.height(30.dp)) + + Box( + modifier = Modifier + .wrapContentHeight() + .padding(bottom = 10.dp) + .padding(horizontal = 40.dp), + contentAlignment = Alignment.TopCenter + ) { + Card( + modifier = Modifier + .size(width = 276.dp, height = 350.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card( + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + + Spacer(modifier = Modifier.width(10.dp)) + + Image( + painter = painterResource(id = R.drawable.user), + contentDescription = "Nama", + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp), + colorFilter = ColorFilter.tint(Color.Gray) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + BasicTextField( + value = nama, + onValueChange = { nama = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + decorationBox = { innerTextField -> + if (nama.isEmpty()) { + Text( + "Nama", + color = Color.Gray + ) + } + innerTextField() + } + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Card( + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + + Spacer(modifier = Modifier.width(10.dp)) + + Image( + painter = painterResource(id = R.drawable.mail), + contentDescription = "Email", + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp), + colorFilter = ColorFilter.tint(Color.Gray) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + BasicTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + decorationBox = { innerTextField -> + if (email.isEmpty()) { + Text( + "Email", + color = Color.Gray + ) + } + innerTextField() + } + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Card( + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + + Spacer(modifier = Modifier.width(10.dp)) + + Image( + painter = painterResource(id = R.drawable.password), + contentDescription = "Password", + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp), + colorFilter = ColorFilter.tint(Color.Gray) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + BasicTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.weight(1f), + singleLine = true, + + // 🔒 Enkripsi tampilan password + visualTransformation = if (passwordVisible) + VisualTransformation.None + else + PasswordVisualTransformation(), + + decorationBox = { innerTextField -> + if (password.isEmpty()) { + Text( + "Password", + color = Color.Gray + ) + } + innerTextField() + } + ) + + // 👁️ Icon show/hide password + IconButton(onClick = { + passwordVisible = !passwordVisible + }) { + Icon( + painter = painterResource( + id = if (passwordVisible) + R.drawable.ic_eye_open // mata terbuka + else + R.drawable.ic_eye_closed // mata tertutup + ), + contentDescription = "Toggle Password", + modifier = Modifier.size(18.dp), + tint = Color.Gray + ) + } + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Card( + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFFFFF) + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + + Spacer(modifier = Modifier.width(10.dp)) + + Image( + painter = painterResource(id = R.drawable.password), + contentDescription = "ConfirmPassword", + modifier = Modifier + .size(20.dp) + .padding(start = 5.dp), + colorFilter = ColorFilter.tint(Color.Gray) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + BasicTextField( + value = confirmPassword, + onValueChange = { confirmPassword = it }, + modifier = Modifier.weight(1f), + singleLine = true, + + // 🔒 Enkripsi tampilan password + visualTransformation = if (confirmPasswordVisible) + VisualTransformation.None + else + PasswordVisualTransformation(), + + decorationBox = { innerTextField -> + if (confirmPassword.isEmpty()) { + Text( + "Konfirmasi password", + color = Color.Gray + ) + } + innerTextField() + } + ) + + // 👁️ Icon show/hide password + IconButton(onClick = { + confirmPasswordVisible = !confirmPasswordVisible + }) { + Icon( + painter = painterResource( + id = if (confirmPasswordVisible) + R.drawable.ic_eye_open // mata terbuka + else + R.drawable.ic_eye_closed // mata tertutup + ), + contentDescription = "Toggle Password", + modifier = Modifier.size(18.dp), + tint = Color.Gray + ) + } + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + Card( + onClick = { registerUser(nama, email, password, confirmPassword, context, navController) }, + modifier = Modifier + .size(width = 235.dp, height = 46.dp), + shape = RoundedCornerShape(25.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF22C55E) + ) + ) { + Text( + text = "Daftar", + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Normal, + color = Color.White, + modifier = Modifier.padding(top = 13.dp) + .align(Alignment.CenterHorizontally) + ) + } + } + } + } + } + } + } +} + + diff --git a/app/src/main/java/Page/Panduan.kt b/app/src/main/java/Page/Panduan.kt new file mode 100644 index 0000000..4320a2a --- /dev/null +++ b/app/src/main/java/Page/Panduan.kt @@ -0,0 +1,257 @@ +package com.example.uitugasakhir + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import com.example.app.ui.theme.Inter +import androidx.compose.foundation.layout.fillMaxHeight +import com.example.app.ui.theme.Inria_Regular + + +@Composable +fun PanduanScreen(navController: NavController) { + + Box(modifier = Modifier.fillMaxSize()) { + + Card( + modifier = Modifier + .fillMaxWidth() + .height(160.dp), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors(Color(0xFF2F6F4E)) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 22.dp), + contentAlignment = Alignment.TopCenter + ) { + Image( + painter = painterResource(id = R.drawable.back), + contentDescription = "kembali", + modifier = Modifier + .size(15.dp) + .offset(x = -160.dp, y = 1.dp) + .clickable { navController.popBackStack() } + ) + + Text( + text = stringResource(R.string.guide), + fontSize = 16.sp, + fontFamily = Inter, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } + + // CONTENT + Card( + modifier = Modifier + .padding(top = 58.dp) + .fillMaxSize(), + shape = RoundedCornerShape(topStart = 24.dp), + colors = CardDefaults.cardColors(Color.White) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + .animateContentSize() + ) { + Text( + text = stringResource(R.string.first_step), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFA7C7E7) + ), + elevation = CardDefaults.cardElevation(4.dp) + ){ + + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.main_features), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFB7E4C7) + ), + elevation = CardDefaults.cardElevation(4.dp) + ){ + + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.automatic_watering), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFBDE0FE) + ), + elevation = CardDefaults.cardElevation(4.dp) + ){ + + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.manual_control), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFA3B18A) + ), + elevation = CardDefaults.cardElevation(4.dp) + ){ + + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.notifications), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFD6A5) + ), + elevation = CardDefaults.cardElevation(4.dp) + ){ + + } + + Spacer(modifier = Modifier.height(15.dp)) + + Text( + text = stringResource(R.string.history_analytics), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFFC8DD) + ), + elevation = CardDefaults.cardElevation(4.dp) + ){ + + } + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/aksi/Aksi_UI.kt b/app/src/main/java/aksi/Aksi_UI.kt new file mode 100644 index 0000000..67920b3 --- /dev/null +++ b/app/src/main/java/aksi/Aksi_UI.kt @@ -0,0 +1,75 @@ +package com.example.uitugasakhir.aksi + +import android.content.Context +import android.util.Log +import android.widget.Toast +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley + +@Composable +fun DeleteDialog( + showDialog: Boolean, + selectedId: String, + context: Context, + onDismiss: () -> Unit, + onSuccess: () -> Unit +) { + + if (!showDialog) return + + AlertDialog( + onDismissRequest = { onDismiss() }, + + title = { + Text("Konfirmasi") + }, + + text = { + Text("Yakin ingin menghapus jadwal ini?") + }, + + confirmButton = { + TextButton( + onClick = { + + Log.d("DELETE", "HAPUS ID: $selectedId") + + val request = object : StringRequest( + Request.Method.POST, + "http://kopikaosmartsystem.my.id/hapus_jadwal.php", + { response -> + Log.d("API", "Response: $response") + Toast.makeText(context, "Berhasil dihapus", Toast.LENGTH_SHORT).show() + + onDismiss() + onSuccess() // refresh data + }, + { error -> + Log.e("API", error.toString()) + Toast.makeText(context, "Gagal hapus", Toast.LENGTH_SHORT).show() + } + ) { + override fun getParams(): MutableMap { + return hashMapOf("id" to selectedId) + } + } + + Volley.newRequestQueue(context).add(request) + } + ) { + Text("OK") + } + }, + + dismissButton = { + TextButton( + onClick = { onDismiss() } + ) { + Text("Batal") + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/api/ApiAmbangBatas.kt b/app/src/main/java/api/ApiAmbangBatas.kt new file mode 100644 index 0000000..2610cc1 --- /dev/null +++ b/app/src/main/java/api/ApiAmbangBatas.kt @@ -0,0 +1,149 @@ +package com.example.uitugasakhir.api + +import android.content.Context +import android.util.Log +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.example.uitugasakhir.model.AmbangBatas +import org.json.JSONObject + +object ApiAmbangBatas { + + // 🔥 GET DATA + fun getAmbangBatas( + context: Context, + onSuccess: (AmbangBatas) -> Unit, + onError: (String) -> Unit + ) { + + val url = + "http://kopikaosmartsystem.my.id/get_ambang_batas.php" + + val request = StringRequest( + Request.Method.GET, + url, + { response -> + + try { + Log.d("API_GET_RAW", response) + + val json = JSONObject(response) + + if (json.getString("status") == "success") { + + val data = AmbangBatas( + min_tanah = json.getInt("min_tanah"), + min_suhu = json.getInt("min_suhu"), + min_udara = json.getInt("min_udara"), + max_tanah = json.getInt("max_tanah"), + max_suhu = json.getInt("max_suhu"), + max_udara = json.getInt("max_udara"), + critical_tanah = json.getInt("critical_tanah") + ) + + onSuccess(data) + + } else { + onError("Data tidak tersedia") + } + + } catch (e: Exception) { + Log.e("JSON_ERROR_GET", response) + onError("Response bukan JSON (cek server/API error)") + } + }, + { error -> + + Log.e("GET_ERROR", error.toString()) + + error.networkResponse?.let { + + Log.e( + "GET_CODE", + it.statusCode.toString() + ) + + Log.e( + "GET_BODY", + String(it.data) + ) + } + + onError(error.toString()) + } + ) + + Volley.newRequestQueue(context).add(request) + } + + + // 🔥 SAVE / UPDATE DATA + fun saveAmbangBatas( + context: Context, + minTanah: Int, + minSuhu: Int, + minUdara: Int, + maxTanah: Int, + maxSuhu: Int, + maxUdara: Int, + criticalTanah: Int, + onSuccess: (String) -> Unit, + onError: (String) -> Unit + ) { + + val url = "http://kopikaosmartsystem.my.id/save_ambang_batas.php" + + val request = object : StringRequest( + Method.POST, + url, + { response -> + + try { + Log.d("API_SAVE_RAW", response) + + val json = JSONObject(response) + onSuccess(json.getString("message")) + + } catch (e: Exception) { + Log.e("JSON_ERROR_SAVE", response) + onError("Response bukan JSON (cek server/API error)") + } + }, + { error -> + + Log.e("SAVE_ERROR", error.toString()) + + error.networkResponse?.let { + + Log.e( + "SAVE_CODE", + it.statusCode.toString() + ) + + Log.e( + "SAVE_BODY", + String(it.data) + ) + } + + onError(error.toString()) + } + ) { + + override fun getParams(): MutableMap { + return hashMapOf( + "min_tanah" to minTanah.toString(), + "min_suhu" to minSuhu.toString(), + "min_udara" to minUdara.toString(), + "max_tanah" to maxTanah.toString(), + "max_suhu" to maxSuhu.toString(), + "max_udara" to maxUdara.toString(), + "critical_tanah" to criticalTanah.toString() + ) + } + } + + Volley.newRequestQueue(context).add(request) + } +} \ No newline at end of file diff --git a/app/src/main/java/api/ApiDurasi.kt b/app/src/main/java/api/ApiDurasi.kt new file mode 100644 index 0000000..feb37f5 --- /dev/null +++ b/app/src/main/java/api/ApiDurasi.kt @@ -0,0 +1,36 @@ +package com.example.uitugasakhir.api + +import android.content.Context +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley + +object ApiDurasi { + + fun saveDurasi( + context: Context, + durasi: Int + ) { + + val url = + "http://192.168.0.105/save_duration.php" + + val request = object : StringRequest( + Method.POST, + url, + {}, + {} + ) { + + override fun getParams(): MutableMap { + + return hashMapOf( + "durasi" to durasi.toString() + ) + } + } + + Volley.newRequestQueue(context) + .add(request) + } +} \ No newline at end of file diff --git a/app/src/main/java/api/ApiService.kt b/app/src/main/java/api/ApiService.kt new file mode 100644 index 0000000..5ccd9a6 --- /dev/null +++ b/app/src/main/java/api/ApiService.kt @@ -0,0 +1,164 @@ +package com.example.uitugasakhir.api + +import com.example.uitugasakhir.model.SensorResponse +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.* +import com.example.uitugasakhir.model.RiwayatItem +import com.example.uitugasakhir.model.Sensor +import com.example.uitugasakhir.model.SettingResponse +import com.example.uitugasakhir.model.AmbangBatas + +// jadwal +import com.example.uitugasakhir.model.JadwalItem +import com.example.uitugasakhir.model.ResponseMessage + +// Jeda +import com.example.uitugasakhir.model.UnreadResponse +import com.example.uitugasakhir.model.JedaResponse + +import com.example.uitugasakhir.model.WarningItem +import com.example.uitugasakhir.model.ApiResponse + +import com.example.uitugasakhir.model.WarningResponse + +interface ApiService { + + @FormUrlEncoded + @POST("forgot_password.php") + fun forgotPassword( + @Field("email") email: String + ): Call + + @GET("get_setting.php") + suspend fun getSetting( + @Query("user_id") userId: Int + ): Response + + @FormUrlEncoded + @POST("save_setting.php") + suspend fun saveSetting( + @Field("user_id") userId: Int, + @Field("kelembapan_tanah") tanah: Int, + @Field("suhu_udara") suhu: Int, + @Field("kelembapan_udara") udara: Int + ): Response + + @GET("config.php") + suspend fun setConfig( + @Query("jadwal") jadwal: String, + @Query("ambang") ambang: Int, + @Query("jeda") jeda: Int + ): String + + @GET("get_riwayat.php") + suspend fun getRiwayat( + @Query("user_id") userId: Int + ): List + + @FormUrlEncoded + @POST("save_riwayat.php") + suspend fun saveRiwayat( + @Field("aksi") aksi: String, + @Field("status") status: String, + @Field("keterangan") keterangan: String + ): Response + + @GET("get_sensor.php") + suspend fun getSensor(): SensorResponse + + @GET("setting_notifikasi.php") + suspend fun getSettingFromApi(): SettingResponse + + @GET("get_ambang_batas.php") + suspend fun getAmbangBatas(): AmbangBatas + + @FormUrlEncoded + @POST("save_log.php") + suspend fun saveLog( + @Field("jenis") jenis: String, + @Field("kategori") kategori: String, + @Field("pesan") pesan: String, + @Field("level") level: String, + @Field("nilai_sensor") nilai: String?, + @Field("min_batas") min: String?, + @Field("max_batas") max: String? + ): okhttp3.ResponseBody + + @FormUrlEncoded + @POST("delete_log.php") + suspend fun deleteLog( + @Field("id") id: Int + ): Response + + @GET("get_log.php") + suspend fun getLogs(): List + + + // jadwal + @GET("get_jadwal.php") + suspend fun getJadwal(): List + + // TAMBAH jadwal + @FormUrlEncoded + @POST("tambah_jadwal.php") + suspend fun tambahJadwal( + @Field("waktu") waktu: String, + @Field("status_waktu") status: String + ): ResponseMessage + + // HAPUS jadwal + @FormUrlEncoded + @POST("delete_jadwal.php") + suspend fun deleteJadwal( + @Field("id") id: Int + ): ResponseMessage + + // Jeda + // GET jeda + @GET("get_jeda.php") + suspend fun getJeda(): Response + + // UPDATE jeda + @FormUrlEncoded + @POST("set_jeda.php") + suspend fun setJeda( + @Field("jeda") jeda: String + ): Response + + // setting pompa + @FormUrlEncoded + @POST("set_pump.php") + suspend fun setPump( + @Field("status") status: String, + @Field("mode") mode: String + ): Response + + @GET("get_unread_count.php") + suspend fun getUnreadCount(): UnreadResponse + + @FormUrlEncoded + @POST("read_notification.php") + suspend fun readNotification( + @Field("id") id: Int + ): ResponseBody + + @POST("read_all_notifications.php") + suspend fun readAllNotifications(): ResponseBody + + @GET("get_warning.php") + suspend fun getWarning(): WarningResponse + + @GET("acknowledge_warning.php") + suspend fun acknowledgeWarning(): Response + + @FormUrlEncoded + @POST("insert_warning.php") + suspend fun insertWarning( + + @Field("pesan") pesan: String + + ): Response + +} \ No newline at end of file diff --git a/app/src/main/java/api/RetrofitClient.kt b/app/src/main/java/api/RetrofitClient.kt new file mode 100644 index 0000000..b8e7836 --- /dev/null +++ b/app/src/main/java/api/RetrofitClient.kt @@ -0,0 +1,17 @@ +package com.example.uitugasakhir.api + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +object RetrofitClient { + + private const val BASE_URL = "http://kopikaosmartsystem.my.id/" + + val api: ApiService by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/MainActivity.kt b/app/src/main/java/com/example/uitugasakhir/MainActivity.kt new file mode 100644 index 0000000..f241740 --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/MainActivity.kt @@ -0,0 +1,193 @@ +package com.example.uitugasakhir + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.* +import kotlinx.coroutines.delay +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.compose.animation.* +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.uitugasakhir.Page.NotifikasiScreen +import com.example.uitugasakhir.ViewModel.MainViewModel +import android.content.Intent +import androidx.compose.ui.platform.LocalContext +import com.example.uitugasakhir.ViewModel.SensorViewModel +import android.os.Build +import androidx.activity.result.contract.ActivityResultContracts +import com.example.uitugasakhir.model.AlertEvent +import android.util.Log + + + + +class MainActivity : ComponentActivity() { + + private var pendingRoute: String? = null + + private val requestPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + startSensorService() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) + } else { + startSensorService() + } + + handleIntent(intent) + + setContent { + + val mainVm: MainViewModel = viewModel() + + AppNavigation( + routeFromNotif = pendingRoute, + mainVm = mainVm + ) + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent) { + pendingRoute = intent.getStringExtra("route") + } + + private fun startSensorService() { + val serviceIntent = Intent(this, SensorMonitoringService::class.java) + } +} + + +@Composable +fun AppNavigation( + routeFromNotif: String?, + mainVm: MainViewModel +) { + + val context = LocalContext.current + + val sensorVm: SensorViewModel = viewModel() + + LaunchedEffect(Unit) { + + sensorVm.loadUnreadCount() + } + + var showSplash by remember { mutableStateOf(!AppSession.isSplashShown) } + + val navController = rememberNavController() + + var navReady by remember { mutableStateOf(false) } + + val alert = mainVm.alertEvent + + var showPopupState = remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + mainVm.alertEvent.collect { event -> + + Log.d("ALERT_FLOW", "UI RECEIVED ALERT → ${event.title}") + + + SensorNotification.showNotification( + context, + event.title, + event.message + ) + } + } + + LaunchedEffect(Unit) { + delay(3000) + showSplash = false + AppSession.isSplashShown = true + } + + if (showSplash) { + SplashScreen() + return + } + + LaunchedEffect(Unit) { + navReady = true + } + + LaunchedEffect(routeFromNotif, navReady) { + if (navReady) { + routeFromNotif?.let { + navController.navigate(it) { + launchSingleTop = true + } + } + } + } + + NavHost( + navController = navController, + startDestination = "dashboard" + ) { + + composable("manual") { + ManualScreen( + navController = navController, + sensorVm = sensorVm, + mainVm = mainVm + ) + } + + composable("notifikasi") { + NotifikasiScreen( + navController = navController, + sensorVm = sensorVm + ) + } + + composable("dashboard") { + + DashboardScreen( + navController = navController, + sensorVm = sensorVm, + mainVm = mainVm + ) + } + + composable("pengaturan") { + PengaturanScreen(navController) + } + + composable("riwayat") { + RiwayatScreen(navController) + } + + composable("login") { + LoginScreen(navController) + } + + composable("register") { + RegisterScreen(navController) + } + + composable("forgot_password") { + ForgotPasswordScreen(navController) + } + + composable("panduan") { + PanduanScreen(navController) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/ui/theme/AlertEvent.kt b/app/src/main/java/com/example/uitugasakhir/ui/theme/AlertEvent.kt new file mode 100644 index 0000000..1797dd6 --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/ui/theme/AlertEvent.kt @@ -0,0 +1,6 @@ +package com.example.uitugasakhir.model + +data class AlertEvent( + val title: String, + val message: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/ui/theme/AppSession.kt b/app/src/main/java/com/example/uitugasakhir/ui/theme/AppSession.kt new file mode 100644 index 0000000..ca3f37c --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/ui/theme/AppSession.kt @@ -0,0 +1,5 @@ +package com.example.uitugasakhir + +object AppSession { + var isSplashShown = false +} \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/ui/theme/Color.kt b/app/src/main/java/com/example/uitugasakhir/ui/theme/Color.kt new file mode 100644 index 0000000..b67cafb --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.uitugasakhir.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/ui/theme/Font.kt b/app/src/main/java/com/example/uitugasakhir/ui/theme/Font.kt new file mode 100644 index 0000000..7152152 --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/ui/theme/Font.kt @@ -0,0 +1,28 @@ +package com.example.app.ui.theme + +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import com.example.uitugasakhir.R + +val Inter = FontFamily( + Font(R.font.inter_medium, FontWeight.Medium) +) +val Inria_Regular = FontFamily( + Font(R.font.inria_regular, FontWeight.Normal) +) +val Inria_Bold = FontFamily( + Font(R.font.inriasans_bold, FontWeight.Bold) +) +val Poppins_Bold = FontFamily( + Font(R.font.poppins_bold, FontWeight.Bold) +) +val Poppins_Regular = FontFamily( + Font(R.font.poppins_regular, FontWeight.Bold) +) +val Poppins_Medium = FontFamily( + Font(R.font.poppins_medium, FontWeight.Bold) +) +val Poppins_Black = FontFamily( + Font(R.font.poppins_black, FontWeight.Bold) +) \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/ui/theme/Theme.kt b/app/src/main/java/com/example/uitugasakhir/ui/theme/Theme.kt new file mode 100644 index 0000000..b30e32c --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.example.uitugasakhir.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun UITugasAkhirTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/uitugasakhir/ui/theme/Type.kt b/app/src/main/java/com/example/uitugasakhir/ui/theme/Type.kt new file mode 100644 index 0000000..3666e6a --- /dev/null +++ b/app/src/main/java/com/example/uitugasakhir/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.uitugasakhir.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/komponen/PopupNotifikasi.kt b/app/src/main/java/komponen/PopupNotifikasi.kt new file mode 100644 index 0000000..4a005af --- /dev/null +++ b/app/src/main/java/komponen/PopupNotifikasi.kt @@ -0,0 +1,93 @@ +package com.example.uitugasakhir.ui.komponen + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.Row +import androidx.compose.ui.Modifier + + + +@Composable +fun WarningPopup( + + show: Boolean, + message: String, + + onDismiss: () -> Unit, + + disablePopup1Minute: Boolean, + onDisableChange: (Boolean) -> Unit +) { + + if (show) { + + AlertDialog( + + containerColor = Color(0xFF222831), + + onDismissRequest = onDismiss, + + title = { + + Text( + "🚨 PERINGATAN SISTEM", + color = Color.Red, + fontWeight = FontWeight.Bold + ) + }, + + text = { + + Column { + + Text( + text = message, + color = Color.White + ) + + Spacer( + modifier = Modifier.height(12.dp) + ) + + Row( + verticalAlignment = + Alignment.CenterVertically + ) { + + Checkbox( + checked = disablePopup1Minute, + + onCheckedChange = + onDisableChange + ) + + Text( + text = + "Nonaktifkan selama 1 menit", + + color = Color.White + ) + } + } + }, + + confirmButton = { + + TextButton( + onClick = onDismiss + ) { + + Text( + "OK", + color = Color.Green + ) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/komponen/WebSocketClient.kt b/app/src/main/java/komponen/WebSocketClient.kt new file mode 100644 index 0000000..833322a --- /dev/null +++ b/app/src/main/java/komponen/WebSocketClient.kt @@ -0,0 +1,55 @@ +package com.example.uitugasakhir.komponen + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.rememberNavController +import com.example.uitugasakhir.ManualScreen +import com.example.uitugasakhir.Page.NotifikasiScreen +import com.example.uitugasakhir.ViewModel.MainViewModel +import com.example.uitugasakhir.ViewModel.SensorViewModel +import viewmodel.NotificationViewModel +import okhttp3.* + +class WebSocketClient( + private val notifVm: NotificationViewModel +) { + + private val client = OkHttpClient() + private var webSocket: WebSocket? = null + + fun connect() { + + val request = Request.Builder() + .url("ws://server-kamu/ws") // ganti nanti + .build() + + val listener = object : WebSocketListener() { + + override fun onMessage(webSocket: WebSocket, text: String) { + notifVm.updateFromSocket(text) + } + + override fun onFailure( + webSocket: WebSocket, + t: Throwable, + response: Response? + ) { + t.printStackTrace() + } + } + + webSocket = client.newWebSocket(request, listener) + } + + fun disconnect() { + webSocket?.close(1000, "close") + } +} + + + + diff --git a/app/src/main/java/model/MQTTState.kt b/app/src/main/java/model/MQTTState.kt new file mode 100644 index 0000000..0619386 --- /dev/null +++ b/app/src/main/java/model/MQTTState.kt @@ -0,0 +1,15 @@ +package com.example.uitugasakhir.model + +data class MQTTState( + + val tanah: Int = 0, + val suhu: Int = 0, + val udara: Int = 0, + val hujan: Int = 0, + + val pump: String = "OFF", + + val status: String = "Standby", + + val alarm: String = "AMAN" +) diff --git a/app/src/main/java/model/ManualViewModel.kt b/app/src/main/java/model/ManualViewModel.kt new file mode 100644 index 0000000..6ae3355 --- /dev/null +++ b/app/src/main/java/model/ManualViewModel.kt @@ -0,0 +1,16 @@ +package com.example.uitugasakhir.ViewModel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import androidx.compose.runtime.* +import com.example.uitugasakhir.api.RetrofitClient +import com.example.uitugasakhir.model.Sensor + +class ManualViewModel : ViewModel() { + + var data by mutableStateOf(null) + var isLoading by mutableStateOf(false) + + +} \ No newline at end of file diff --git a/app/src/main/java/model/Riwayat.kt b/app/src/main/java/model/Riwayat.kt new file mode 100644 index 0000000..a5eb10e --- /dev/null +++ b/app/src/main/java/model/Riwayat.kt @@ -0,0 +1,162 @@ +package com.example.uitugasakhir.model +import com.google.gson.annotations.SerializedName +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.* +import androidx.room.Database +import androidx.room.RoomDatabase +import android.content.Context +import androidx.room.Room + + + +object DatabaseInstance { + + fun get(context: Context): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + "app_db" + ).build() + } +} + +@Entity(tableName = "riwayat") +data class RiwayatEntity( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val waktu: String, + val aksi: String, + val keterangan: String, + val jeda: Int, + val status: String, + val mode: String +) + +@Dao +interface RiwayatDao { + + @Query("SELECT * FROM riwayat ORDER BY id DESC") + suspend fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(list: List) + + @Insert + suspend fun insert(item: RiwayatEntity) + + @Query("DELETE FROM riwayat") + suspend fun clear() +} + +@Database( + entities = [RiwayatEntity::class], + version = 1 +) +abstract class AppDatabase : RoomDatabase() { + abstract fun riwayatDao(): RiwayatDao +} + + + +data class RiwayatItem( + val id: Int, + val waktu: String, + val aksi: String, + val keterangan: String, + val status: String, + val jeda: Int, + val mode: String +) + +data class Sensor( + @SerializedName("kelembapan_udara") + val kelembaban_udara: Float, + + @SerializedName("suhu_udara") + val suhu_udara: Float, + + @SerializedName("kelembapan_tanah") + val kelembaban_tanah: Float, + + @SerializedName("intensitas_hujan") + val intensitas_hujan: Float, +) + +// jadwal +data class JadwalItem( + val id: String, + val waktu: String, + val status: String +) + +data class ResponseMessage( + val success: Boolean, + val message: String +) + +data class SettingResponse( + val status: String, + val message: String? = null, + val kelembapan_tanah: Int = 0, + val suhu_udara: Int = 0, + val kelembapan_udara: Int = 0 +) + +data class Setting( + val kelembapan_tanah: Int, + val suhu_udara: Int, + val kelembapan_udara: Int +) + +data class AmbangBatas( + val min_tanah: Int, + val min_suhu: Int, + val min_udara: Int, + val max_tanah: Int, + val max_suhu: Int, + val max_udara: Int, + val critical_tanah: Int +) + +// buzzer + led dan ui sinkron +data class WarningResponse( + val id: Int, + val status_warning: Int, + val status_buzzer: Int, + val status_led: Int, + val acknowledged: Int, + val pesan: String +) + +//notifikasi +data class WarningItem( + val id: Int, + val status: String, + val message: String, + val time: Long, + val mode: String +) + +data class JedaResponse( + val success: Boolean, + val message: String?, + val data: JedaData? +) + +data class JedaData( + val id: String? = null, + val jeda: String? = null +) + +// set pompa +data class ApiResponse( + val status: String, + val pump: String? = null, + val message: String? = null +) + +data class UnreadResponse( + val status: String, + val total: Int +) \ No newline at end of file diff --git a/app/src/main/java/model/RiwayatCard.kt b/app/src/main/java/model/RiwayatCard.kt new file mode 100644 index 0000000..a8b0013 --- /dev/null +++ b/app/src/main/java/model/RiwayatCard.kt @@ -0,0 +1,34 @@ +package com.adit.smartplant.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.uitugasakhir.model.RiwayatItem + +@Composable +fun RiwayatCard( + riwayatList: List +) { + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 214.dp) + ) { + LazyColumn { + items(riwayatList) { item -> + Text( + text = "${item.waktu} ${item.aksi}", + fontSize = 14.sp + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/model/RiwayatViewModel.kt b/app/src/main/java/model/RiwayatViewModel.kt new file mode 100644 index 0000000..9d37d4c --- /dev/null +++ b/app/src/main/java/model/RiwayatViewModel.kt @@ -0,0 +1,26 @@ +package com.example.uitugasakhir.ViewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.uitugasakhir.model.RiwayatItem +import kotlinx.coroutines.launch +import com.example.uitugasakhir.api.RetrofitClient + +class RiwayatViewModel : ViewModel() { + + var riwayatList by mutableStateOf>(emptyList()) + private set + + fun loadRiwayat(userId: Int) { + viewModelScope.launch { + try { + riwayatList = RetrofitClient.api.getRiwayat(userId) + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/model/SensorResponse.kt b/app/src/main/java/model/SensorResponse.kt new file mode 100644 index 0000000..09ea37e --- /dev/null +++ b/app/src/main/java/model/SensorResponse.kt @@ -0,0 +1,8 @@ +package com.example.uitugasakhir.model + +data class SensorResponse( + val kelembaban_udara: Int, + val suhu_udara: Int, + val kelembaban_tanah: Int, + val intensitas_hujan: Int +) \ No newline at end of file diff --git a/app/src/main/java/model/SensorViewModel.kt b/app/src/main/java/model/SensorViewModel.kt new file mode 100644 index 0000000..b137a90 --- /dev/null +++ b/app/src/main/java/model/SensorViewModel.kt @@ -0,0 +1,1158 @@ +package com.example.uitugasakhir.ViewModel + +import android.util.Log +import androidx.compose.runtime.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.uitugasakhir.api.RetrofitClient +import com.example.uitugasakhir.model.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +import android.content.Context +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.example.uitugasakhir.model.RiwayatItem +import org.json.JSONArray +import org.json.JSONObject + +import java.text.SimpleDateFormat +import java.util.* +import android.app.Activity +import java.util.Locale + +import android.widget.Toast + +import com.example.uitugasakhir.SensorNotification +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +import com.example.uitugasakhir.mqtt.MqttManager + + + + +class SensorViewModel : ViewModel() { + + var warningData by mutableStateOf(null) + + var notifTanahAktif by mutableStateOf(true) + private set + + var notifHujanAktif by mutableStateOf(true) + private set + + var buzzerAktif by mutableStateOf(true) + private set + + var warningList = mutableStateListOf() + private set + + // ========================= + var selectedItems by mutableStateOf>(emptySet()) + private set + + var status by mutableStateOf("Aman") + private set + + var keterangan by mutableStateOf("Semua kondisi normal") + private set + + var data by mutableStateOf(null) + private set + + var isLoading by mutableStateOf(false) + private set + + var currentAmbang by mutableStateOf(null) + private set + + var manualWarningMessage by mutableStateOf("") + private set + + var autoWarningMessage by mutableStateOf("") + private set + + var isManualWarningActive by mutableStateOf(false) + private set + + var isAutoWarningActive by mutableStateOf(false) + private set + + private var lastTriggerTime = 0L + private var lastWarningMessages = emptySet() + + var currentMode by mutableStateOf("Manual") + private set + + + + fun getWarningData() { + + viewModelScope.launch { + + try { + + warningData = + RetrofitClient.api.getWarning() + + } catch (e: Exception) { + + e.printStackTrace() + } + } + } + + private val _pumpStatus = mutableStateOf("OFF") + + fun matikanAlarm() { + + viewModelScope.launch { + + try { + + RetrofitClient.api.acknowledgeWarning() + + getWarningData() + + } catch (e: Exception) { + + e.printStackTrace() + } + } + } + + fun kirimWarning(pesan: String) { + + viewModelScope.launch { + + try { + + RetrofitClient.api.insertWarning(pesan) + + } catch (e: Exception) { + + e.printStackTrace() + } + } + } + + fun setNotifTanah(enabled: Boolean) { + notifTanahAktif = enabled + } + + fun setNotifHujan(enabled: Boolean) { + notifHujanAktif = enabled + } + + fun setBuzzer(enabled: Boolean) { + buzzerAktif = enabled + } + + fun setMode(mode: String) { + currentMode = mode + } + + var unreadCount by mutableStateOf(0) + private set + + fun loadUnreadCount() { + + viewModelScope.launch { + + try { + + val response = + RetrofitClient.api.getUnreadCount() + + unreadCount = response.total + + } catch (e: Exception) { + + Log.e( + "UNREAD", + e.message.toString() + ) + } + } + } + + fun readNotification(id: Int) { + + viewModelScope.launch { + + try { + + RetrofitClient.api.readNotification(id) + + loadUnreadCount() + + } catch (e: Exception) { + + Log.e( + "READ_NOTIF", + e.message.toString() + ) + } + } + } + + fun readAllNotifications() { + + viewModelScope.launch { + + try { + + RetrofitClient.api.readAllNotifications() + + unreadCount = 0 + + } catch (e: Exception) { + + Log.e( + "READ_ALL", + e.message.toString() + ) + } + } + } + + // ========================= + // FIX: trigger warning + // ========================= + private fun triggerWarning( + message: String, + mode: String + ) { + + val now = System.currentTimeMillis() + + if (now - lastTriggerTime < 3000) return + lastTriggerTime = now + + val newItem = WarningItem( + id = 0, + status = "warning", + message = message, + time = now, + mode = mode + ) + + if (warningList.none { it.message == message }) { + warningList.add(0, newItem) + } + + if (warningList.size > 20) { + warningList.removeLast() + } + + // ========================= + // TAMBAHAN PENTING + // ========================= + kirimWarning(message) + + getWarningData() + + // ========================= + + data?.let { sensor -> + currentAmbang?.let { ambang -> + sendLogToServer( + listOf(message), + sensor, + ambang + ) + } + } + } + + fun clearWarning(mode: String) { + + if (mode == "Manual") { + isManualWarningActive = false + manualWarningMessage = "" + } else { + isAutoWarningActive = false + autoWarningMessage = "" + } + } + + // ========================= + // SENSOR LOOP + // ========================= + fun getData(context: Context) { + viewModelScope.launch { + while (true) { + isLoading = true + + try { + val sensorResponse = RetrofitClient.api.getSensor() + val ambangResponse = RetrofitClient.api.getAmbangBatas() + + val sensorData = Sensor( + kelembaban_udara = sensorResponse.kelembaban_udara.toFloat(), + suhu_udara = sensorResponse.suhu_udara.toFloat(), + kelembaban_tanah = sensorResponse.kelembaban_tanah.toFloat(), + intensitas_hujan = sensorResponse.intensitas_hujan.toFloat() + ) + + val ambangData = AmbangBatas( + min_tanah = ambangResponse.min_tanah, + max_tanah = ambangResponse.max_tanah, + critical_tanah = ambangResponse.critical_tanah, + min_suhu = ambangResponse.min_suhu, + max_suhu = ambangResponse.max_suhu, + min_udara = ambangResponse.min_udara, + max_udara = ambangResponse.max_udara + ) + + data = sensorData + currentAmbang = ambangData + + if (currentMode == "Otomatis") { + + checkWarning( + context, + sensorData, + ambangData, + currentMode + ) + + } else { + + isManualWarningActive = false + manualWarningMessage = "" + + isAutoWarningActive = false + autoWarningMessage = "" + } + + } catch (e: Exception) { + Log.e("API", "Error: ${e.message}") + } + + isLoading = false + delay(5000) + } + } + } + + // ========================= + // CHECK WARNING + // ========================= + private fun checkWarning( + context: Context, + sensor: Sensor, + ambang: AmbangBatas, + mode: String + ) { + + val messages = mutableListOf() + + when { + sensor.kelembaban_tanah < ambang.min_tanah -> + messages.add( + "Kelembapan tanah < ambang (kering).\nPompa hidup otomatis" + ) + + sensor.kelembaban_tanah > ambang.max_tanah -> + messages.add("Kelembapan tanah > ambang (lembab).\nPompa mati otomatis") + } + + when { + sensor.suhu_udara < ambang.min_suhu -> + messages.add("Suhu udara < ambang (dingin).\nKondisi tanaman perlu diperhatikan") + + sensor.suhu_udara > ambang.max_suhu -> + messages.add("Suhu udara > ambang (panas).\nKondisi tanaman perlu diperhatikan") + } + + when { + sensor.kelembaban_udara < ambang.min_udara -> + messages.add("Kelembapan udara < ambang (kering).\nKondisi tanaman perlu diperhatikan") + + sensor.kelembaban_udara > ambang.max_udara -> + messages.add("Kelembapan udara > ambang (lembab).\nKondisi tanaman perlu diperhatikan") + } + + if (messages.isNotEmpty()) { + + SensorNotification.showNotification( + context, + "Peringatan Sensor", + messages.first() + ) + + status = "Monitoring" + keterangan = messages.first() + + if (mode == "Manual") { + + manualWarningMessage = + "${messages.joinToString("\n")}" + + isManualWarningActive = true + + } else { + + autoWarningMessage = + "${messages.joinToString("\n")}" + + isAutoWarningActive = true + + Log.d("POPUP_DEBUG", "AUTO ACTIVE = $isAutoWarningActive") + Log.d("POPUP_DEBUG", "MESSAGE = $autoWarningMessage") + } + + val currentWarnings = messages.toSet() + + (currentWarnings - lastWarningMessages).forEach { + triggerWarning(it, mode) + } + + lastWarningMessages = currentWarnings + + } else { + lastWarningMessages = emptySet() + status = "Aman" + if (mode == "Manual") { + + manualWarningMessage = "" + isManualWarningActive = false + + } else { + + autoWarningMessage = "" + isAutoWarningActive = false + } + keterangan = "Semua kondisi normal" + } + } + + // ========================= + // DELETE (FIX STABLE UI) + // ========================= + fun deleteWarning(item: WarningItem) { + viewModelScope.launch { + + Log.d("DELETE_SINGLE", "CLICK DELETE ID=${item.id}") + + warningList.removeIf { it.id == item.id } + + try { + val response = RetrofitClient.api.deleteLog(item.id) + + Log.d("DELETE_SINGLE", "RESPONSE=${response.code()}") + + } catch (e: Exception) { + Log.e("DELETE_SINGLE", "ERROR=${e.message}") + } + } + } + + fun deleteAllWarnings() { + viewModelScope.launch { + + Log.d("DELETE_ALL", "START deleteAllWarnings") + Log.d("DELETE_ALL", "TOTAL LIST = ${warningList.size}") + + val tempList = warningList.toList() + + if (tempList.isEmpty()) { + Log.d("DELETE_ALL", "LIST KOSONG - STOP") + return@launch + } + + tempList.forEach { item -> + + Log.d("DELETE_CHECK", "ID = ${item.id}") + Log.d("DELETE_ONE", "REQUEST DELETE ID=${item.id}") + Log.d("DELETE_ONE", "MESSAGE=${item.message}") + + try { + val response = RetrofitClient.api.deleteLog(item.id) + + Log.d("DELETE_ONE", "RESPONSE CODE=${response.code()}") + Log.d("DELETE_ONE", "RESPONSE BODY=${response.body()?.string()}") + + if (response.isSuccessful) { + Log.d("DELETE_ONE", "SUCCESS DELETE ID=${item.id}") + } else { + Log.e("DELETE_ONE", "FAILED DELETE ID=${item.id} CODE=${response.code()}") + } + + } catch (e: Exception) { + Log.e("DELETE_ONE", "EXCEPTION ID=${item.id} MSG=${e.message}") + } + } + + warningList.clear() + Log.d("DELETE_ALL", "UI CLEARED") + } + } + + // ========================= + // LOAD + // ========================= + fun loadLogs() { + viewModelScope.launch { + try { + val logs = RetrofitClient.api.getLogs() + + warningList.clear() + warningList.addAll(logs.sortedByDescending { it.time }) + + } catch (e: Exception) { + Log.e("API", "Load error: ${e.message}") + } + } + } + + fun toggleSelection(item: WarningItem) { + selectedItems = if (selectedItems.contains(item.id)) { + selectedItems - item.id + } else { + selectedItems + item.id + } + } + + fun updateFromSocket(sensor: Sensor, context: Context) { + currentAmbang?.let { + if (currentMode == "Otomatis") { + + checkWarning( + context, + sensor, + it, + currentMode + ) + + } else { + + isManualWarningActive = false + manualWarningMessage = "" + + isAutoWarningActive = false + autoWarningMessage = "" + } + } + } + + fun restoreDeleted(items: List) { + warningList.addAll(0, items) + } + + fun deleteSelected() { + viewModelScope.launch { + val idsToDelete = selectedItems.toList() + + idsToDelete.forEach { id -> + + try { + val response = RetrofitClient.api.deleteLog(id) + + Log.d("DELETE_DEBUG", "Response code: ${response.code()}") + Log.d("DELETE_DEBUG", "Body: ${response.body()?.string()}") + + Log.d("DELETE", "Deleted ID=$id response=$response") + + if (response.isSuccessful) { + warningList.removeAll { it.id == id } + } else { + Log.e("DELETE", "Gagal delete ID=$id code=${response.code()}") + } + + } catch (e: Exception) { + Log.e("DELETE", "Error delete ID=$id : ${e.message}") + } + } + + selectedItems = emptySet() + } + } + + private fun sendLogToServer( + messages: List, + sensor: Sensor, + ambang: AmbangBatas + ) { + viewModelScope.launch { + try { + messages.forEach { msg -> + + val (nilai, min, max) = when { + msg.contains("Tanah") -> + Triple(sensor.kelembaban_tanah, ambang.min_tanah, ambang.max_tanah) + + msg.contains("Udara") -> + Triple(sensor.kelembaban_udara, ambang.min_udara, ambang.max_udara) + + else -> + Triple(sensor.suhu_udara, ambang.min_suhu, ambang.max_suhu) + } + + RetrofitClient.api.saveLog( + jenis = "warning", + kategori = "sensor", + pesan = msg, + level = "HIGH", + nilai = nilai.toString(), + min = min.toString(), + max = max.toString() + ) + + loadLogs() + } + } catch (e: Exception) { + Log.e("LOG_API", "Gagal kirim log: ${e.message}") + } + } + } +} + +class PumpViewModel : ViewModel() { + + var pumpStatus by mutableStateOf("") + private set + + var isLoading by mutableStateOf(false) // 🔥 PINDAH KE SINI + private set + + fun setPump(status: String, mode: String) { + viewModelScope.launch { + isLoading = true + + try { + val response = RetrofitClient.api.setPump( + status, + mode + ) + + if (response.isSuccessful) { + val body = response.body() + if (body?.status == "success") { + pumpStatus = status + } + } + + } catch (e: Exception) { + e.printStackTrace() + } + + isLoading = false + } + } +} + +class MainViewModel : ViewModel() { + + fun updatePumpStatus(status: String) { + _pumpStatus.value = status + } + + private var mqtt: MqttManager? = null + + fun initMQTT(context: Context) { + + if (mqtt != null) return + + mqtt = MqttManager(context) + + mqtt?.connect( + + onMessage = { topic, message -> + + if (topic == "smartplant/riwayat") { + + Log.d("AUTO_RIWAYAT", message) + + onAutoPump(context, message) + } + } + ) + } + + + private var lastAlertType: String? = null + + private fun safeAlert(type: String, title: String, message: String) { + + Log.d("ALERT_FLOW", "SAFE ALERT CHECK → type=$type | last=$lastAlertType") + + if (lastAlertType == type) { + Log.d("ALERT_FLOW", "BLOCKED (duplicate type)") + return + } // 🔥 STOP SPAM + + lastAlertType = type + + Log.d("ALERT_FLOW", "ALLOWED → sending alert") + + triggerAlert(title, message) + } + + private val _alertEvent = MutableSharedFlow() + val alertEvent = _alertEvent.asSharedFlow() + + fun triggerAlert(title: String, message: String) { + + Log.d("ALERT_FLOW", "TRIGGER CALLED → title=$title | message=$message | time=${System.currentTimeMillis()}") + + + viewModelScope.launch { + _alertEvent.emit(AlertEvent(title, message)) + } + } + + private val _autoStatus = mutableStateOf("Sistem otomatis aktif") + val autoStatus: String get() = _autoStatus.value + + fun setAutoStatus(value: String) { + _autoStatus.value = value + } + + // ================= STATE ================= + private val _pumpStatus = mutableStateOf("OFF") + val pumpStatus: String get() = _pumpStatus.value + + private val _mode = mutableStateOf("Manual") + val mode: String get() = _mode.value + + private val _isLoading = mutableStateOf(false) + val isLoading: Boolean get() = _isLoading.value + + private val _avgKT = mutableStateOf(0f) + val avgKT: Float get() = _avgKT.value + + private val _avgKU = mutableStateOf(0f) + val avgKU: Float get() = _avgKU.value + + private val _avgSU = mutableStateOf(0f) + val avgSU: Float get() = _avgSU.value + + private val _avgIH = mutableStateOf(0f) + val avgIH: Float get() = _avgIH.value + + + private val _riwayatList = mutableStateListOf() + val riwayatList: List = _riwayatList + + private var isSuhuNormal = true + + private fun Float.inRange(min: Int, max: Int): Boolean { + return this in min.toFloat()..max.toFloat() + } + + private var lastWateringTime = 0L + + fun updateAverageSensor( + kt: Float, + ku: Float, + su: Float, + ih: Float + ) { + _avgKT.value = kt + _avgKU.value = ku + _avgSU.value = su + _avgIH.value = ih + } + + fun loadAverageSensor(context: Context) { + + val url = + "http://kopikaosmartsystem.my.id/get_rata_rata_sensor.php" + + val request = StringRequest( + Request.Method.GET, + url, + { response -> + + try { + + val json = JSONObject(response) + + updateAverageSensor( + json.getDouble("rata_tanah").toFloat(), + json.getDouble("rata_udara").toFloat(), + json.getDouble("rata_suhu").toFloat(), + json.getDouble("rata_hujan").toFloat() + ) + + } catch (e: Exception) { + Log.e("AVG_SENSOR", e.toString()) + } + + }, + { error -> + Log.e("AVG_SENSOR", error.toString()) + } + ) + + Volley.newRequestQueue(context).add(request) + } + + fun setMode(newMode: String) { + _mode.value = newMode + } + + fun deleteRiwayat(item: RiwayatItem) { + _riwayatList.remove(item) + } + + fun deleteRiwayat(context: Context, item: RiwayatItem) { + + val url = "http://kopikaosmartsystem.my.id/delete_riwayat.php" + + val request = object : StringRequest( + Method.POST, + url, + { response -> + + loadRiwayat(context) + + Toast.makeText( + context, + "Riwayat berhasil dihapus", + Toast.LENGTH_SHORT + ).show() + }, + { error -> + Log.e("DELETE", error.toString()) + } + ) { + + override fun getParams(): MutableMap { + + return hashMapOf( + "id" to item.id.toString() + ) + } + } + + Volley.newRequestQueue(context).add(request) + } + + + fun saveToLocal(context: Context, list: List) { + + val db = DatabaseInstance.get(context) + val dao = db.riwayatDao() + + viewModelScope.launch { + + dao.clear() + + dao.insertAll( + list.map { + RiwayatEntity( + waktu = it.waktu, + aksi = it.aksi, + keterangan = it.keterangan, + jeda = it.jeda, + status = it.status, + mode = it.mode + ) + } + ) + } + } + + + // ================= CONTROL POMPA ================= + fun onPumpChange(context: Context, status: String, mode: String) { + + Log.d("PUMP_STATUS", status) + + _isLoading.value = true + _pumpStatus.value = status + + val aksi = if (status == "ON") "Pompa Dinyalakan" else "Pompa Dimatikan" + val stat = if (status == "ON") "Berjalan" else "Selesai" + + // tampil langsung di UI (optimistic update) + tambahRiwayat(stat, aksi, mode) + + mqtt?.publish( + "smartplant/manualPump", + status + ) + + // kirim ke server + kirimRiwayatKeServer(context, aksi, stat, mode) + + saveToLocal(context, _riwayatList) + + viewModelScope.launch { + delay(1000) + _isLoading.value = false + } + } + + + // ================= TAMBAH RIWAYAT LOKAL ================= + private fun tambahRiwayat(status: String, aksi: String, mode: String) { + + _riwayatList.add( + 0, + RiwayatItem( + id = 0, + waktu = "Loading...", + aksi = aksi, + keterangan = mode, + jeda = 0, + status = status, + mode = mode + ) + ) + + // batasi list + if (_riwayatList.size > 20) { + _riwayatList.removeLast() + } + } + + // ================= AUTO PUMP ================= + fun onAutoPump( + context: Context, + type: String + ) { + + val aksi = when (type) { + + "JADWAL" -> "Penyiraman Otomatis" + + "KRITIS" -> "Penyiraman Darurat" + + else -> "Penyiraman Otomatis" + } + + val status = "Berjalan" + + // Tambahkan langsung ke UI + tambahRiwayat( + status = status, + aksi = aksi, + mode = "Otomatis" + ) + + // Simpan ke server + kirimRiwayatKeServer( + context = context, + aksi = aksi, + status = status, + mode = "Otomatis" + ) + + // Simpan ke database lokal + saveToLocal(context, _riwayatList) + } + + // ================= API: SIMPAN ================= + private fun kirimRiwayatKeServer(context: Context, aksi: String, status: String, mode: String) { + + val url = "http://kopikaosmartsystem.my.id/save_riwayat.php" + + val request = object : StringRequest( + Method.POST, + url, + { response -> + Log.d("API", response) + + // reload dari DB biar sinkron + loadRiwayat(context) + }, + { error -> + Log.e("API", error.toString()) + + _riwayatList.add( + 0, + RiwayatItem( + id = 0, + waktu = "Gagal", + aksi = aksi, + keterangan = "Server Error", + jeda = 0, + status = "Error", + mode = mode + ) + ) + } + ) { + override fun getParams(): MutableMap { + return hashMapOf( + "aksi" to aksi, + "status" to status, + "keterangan" to mode, + "mode" to mode + ) + } + } + + Volley.newRequestQueue(context).add(request) + } + + + // ================= API: LOAD ================= + fun loadRiwayat(context: Context) { + + val url = "http://kopikaosmartsystem.my.id/get_riwayat.php" + + val request = StringRequest( + Request.Method.GET, + url, + { response -> + + try { + + val jsonObject = org.json.JSONObject(response) + + val jsonArray = jsonObject.getJSONArray("data") + + val tempList = mutableListOf() + + for (i in 0 until jsonArray.length()) { + + val obj = jsonArray.getJSONObject(i) + + tempList.add( + RiwayatItem( + id = obj.getInt("id"), + waktu = obj.getString("waktu"), + aksi = obj.getString("aksi"), + keterangan = obj.getString("keterangan"), + jeda = 0, + status = obj.getString("status"), + mode = obj.getString("mode") + ) + ) + } + + _riwayatList.clear() + _riwayatList.addAll(tempList) + + saveToLocal(context, tempList) + + } catch (e: Exception) { + + Log.e("JSON_ERROR", e.message.toString()) + } + }, + { error -> + + Log.e("API", error.toString()) + } + ) + + Volley.newRequestQueue(context).add(request) + } + + // data riwayat hari ini, minggu ini, bulan ini + + fun getDataHarian(data: List): List> { + + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + val hariMap = mutableMapOf( + "Sen" to 0, + "Sel" to 0, + "Rab" to 0, + "Kam" to 0, + "Jum" to 0, + "Sab" to 0, + "Min" to 0 + ) + + data.forEach { + + val split = it.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: return@forEach + + val date = sdf.parse(tanggal) ?: return@forEach + + val calData = Calendar.getInstance() + val calNow = Calendar.getInstance() + + calData.time = date + + if (calData.get(Calendar.WEEK_OF_YEAR) == + calNow.get(Calendar.WEEK_OF_YEAR) && + calData.get(Calendar.YEAR) == + calNow.get(Calendar.YEAR) + ) { + + val hari = when (calData.get(Calendar.DAY_OF_WEEK)) { + Calendar.MONDAY -> "Sen" + Calendar.TUESDAY -> "Sel" + Calendar.WEDNESDAY -> "Rab" + Calendar.THURSDAY -> "Kam" + Calendar.FRIDAY -> "Jum" + Calendar.SATURDAY -> "Sab" + Calendar.SUNDAY -> "Min" + else -> "" + } + + hariMap[hari] = hariMap[hari]!! + 1 + } + } + + return hariMap.toList() + } + + fun getDataMingguan(data: List): List> { + + val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + val mingguMap = mutableMapOf() + + data.forEach { + + val split = it.waktu.split(" ") + val tanggal = split.getOrNull(0) ?: return@forEach + + val date = sdf.parse(tanggal) ?: return@forEach + + val calData = Calendar.getInstance() + val calNow = Calendar.getInstance() + + calData.time = date + + if (calData.get(Calendar.MONTH) == + calNow.get(Calendar.MONTH) && + calData.get(Calendar.YEAR) == + calNow.get(Calendar.YEAR) + ) { + + val mingguKe = calData.get(Calendar.WEEK_OF_MONTH) + + mingguMap[mingguKe] = (mingguMap[mingguKe] ?: 0) + 1 + } + } + + for (i in 1..5) { + mingguMap.putIfAbsent(i, 0) + } + + return mingguMap + .toSortedMap() + .map { "M${it.key}" to it.value } + } + + fun setLocale(context: Context, languageCode: String) { + + val locale = Locale(languageCode) + + Locale.setDefault(locale) + + val config = context.resources.configuration + + config.setLocale(locale) + + context.resources.updateConfiguration( + config, + context.resources.displayMetrics + ) + + (context as Activity).recreate() + } +} diff --git a/app/src/main/java/mqtt/MqttManager.kt b/app/src/main/java/mqtt/MqttManager.kt new file mode 100644 index 0000000..6e53b22 --- /dev/null +++ b/app/src/main/java/mqtt/MqttManager.kt @@ -0,0 +1,245 @@ +package com.example.uitugasakhir.mqtt + +import android.content.Context +import android.util.Log +import info.mqtt.android.service.MqttAndroidClient +import org.eclipse.paho.client.mqttv3.* +import javax.net.ssl.SSLSocketFactory + +class MqttManager(private val context: Context) { + + // ===================================================== + // MQTT CONFIG + // ===================================================== + private val serverUri = + "ssl://9feefbc57c4f41f6a4fde7c48e2a35ad.s1.eu.hivemq.cloud:8883" + + private val username = "mqttuser2" + + private val password = "Mqtt12345" + + private val clientId = + "ANDROID_${System.currentTimeMillis()}" + + private val client = + MqttAndroidClient(context, serverUri, clientId) + + // ===================================================== + // CONNECT + // ===================================================== + fun connect( + onMessage: (topic: String, message: String) -> Unit, + onConnected: (() -> Unit)? = null + ) { + + val options = MqttConnectOptions().apply { + + userName = username + + password = this@MqttManager.password.toCharArray() + + isAutomaticReconnect = true + + isCleanSession = false + + connectionTimeout = 10 + + keepAliveInterval = 30 + + socketFactory = + SSLSocketFactory.getDefault() + } + + client.setCallback(object : MqttCallback { + + // ================================================= + // MESSAGE + // ================================================= + override fun messageArrived( + topic: String?, + message: MqttMessage? + ) { + + val msg = message.toString() + + Log.d("MQTT_TOPIC", topic ?: "") + Log.d("MQTT_MESSAGE", msg) + + onMessage(topic ?: "", msg) + } + + // ================================================= + // LOST + // ================================================= + override fun connectionLost(cause: Throwable?) { + + Log.e( + "MQTT", + "CONNECTION LOST", + cause + ) + } + + override fun deliveryComplete(token: IMqttDeliveryToken?) {} + }) + + // ===================================================== + // CONNECT + // ===================================================== + client.connect( + options, + null, + object : IMqttActionListener { + + override fun onSuccess(asyncActionToken: IMqttToken?) { + + Log.d("MQTT", "CONNECTED") + + subscribeTopics() + + onConnected?.invoke() + } + + override fun onFailure( + asyncActionToken: IMqttToken?, + exception: Throwable? + ) { + + Log.e( + "MQTT", + "CONNECT FAILED", + exception + ) + } + } + ) + } + + // ===================================================== + // SUBSCRIBE + // ===================================================== + private fun subscribeTopics() { + + val topics = arrayOf( + + "smartplant/alarm", + "smartplant/pump", + "smartplant/sensor", + "smartplant/mode", + "smartplant/manualPump", + "smartplant/riwayat" + ) + + topics.forEach { topic -> + + client.subscribe( + topic, + 1, + null, + object : IMqttActionListener { + + override fun onSuccess(asyncActionToken: IMqttToken?) { + + Log.d( + "MQTT_SUB", + "SUCCESS -> $topic" + ) + } + + override fun onFailure( + asyncActionToken: IMqttToken?, + exception: Throwable? + ) { + + Log.e( + "MQTT_SUB", + "FAILED -> $topic", + exception + ) + } + } + ) + } + } + + // ===================================================== + // PUBLISH + // ===================================================== + fun publish( + topic: String, + payload: String + ) { + + try { + + val message = + MqttMessage(payload.toByteArray()) + + message.qos = 1 + message.isRetained = false + + client.publish(topic, message) + + Log.d( + "MQTT_PUBLISH", + "$topic -> $payload" + ) + + } catch (e: Exception) { + + Log.e( + "MQTT", + "PUBLISH ERROR", + e + ) + } + } + + // ===================================================== + // MODE + // ===================================================== + fun setMode(mode: String) { + + publish( + "smartplant/mode", + mode + ) + } + + // ===================================================== + // MANUAL PUMP + // ===================================================== + fun setPump(state: Boolean) { + + val payload = if (state) "ON" else "OFF" + + publish( + "smartplant/manualPump", + payload + ) + } + + fun setModeAuto() { + publish("smartplant/mode", "Otomatis") + } + + fun setModeManual() { + publish("smartplant/mode", "Manual") + } + + // ===================================================== + // DISCONNECT + // ===================================================== + fun disconnect() { + + try { + + client.unregisterResources() + client.close() + + } catch (e: Exception) { + + e.printStackTrace() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/notifikasi/sensorUI.kt b/app/src/main/java/notifikasi/sensorUI.kt new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/pengaturan/AkunCard.kt b/app/src/main/java/pengaturan/AkunCard.kt new file mode 100644 index 0000000..9fbc4b5 --- /dev/null +++ b/app/src/main/java/pengaturan/AkunCard.kt @@ -0,0 +1,333 @@ +package com.example.uitugasakhir.pengaturan + +import android.content.Context +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Inria_Regular +import androidx.compose.ui.text.TextStyle + +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import android.widget.Toast +import androidx.compose.foundation.text.BasicTextField +import com.example.app.ui.theme.Inria_Bold +import androidx.compose.ui.res.stringResource +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.MainViewModel + + +@Composable +fun AkunCardSection( + viewModel: MainViewModel +) { + + val context = LocalContext.current + + // SharedPreferences + val sharedPreferences = + context.getSharedPreferences("USER_DATA", Context.MODE_PRIVATE) + + var nama by remember { + mutableStateOf( + sharedPreferences.getString("nama", "Nama User") ?: "" + ) + } + + var email by remember { + mutableStateOf( + sharedPreferences.getString("email", "email@gmail.com") ?: "" + ) + } + + // Popup dialog + var showDialog by remember { mutableStateOf(false) } + + // Input edit + var editNama by remember { mutableStateOf(nama) } + var editEmail by remember { mutableStateOf(email) } + + Text( + text = stringResource(R.string.account), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + + // Nama + tombol ubah + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + text = nama, + fontWeight = FontWeight.Bold + ) + + Text( + text = stringResource(R.string.change), + color = Color(0xFF2F6F4E), + fontSize = 13.sp, + modifier = Modifier.clickable { + editNama = nama + editEmail = email + showDialog = true + } + ) + } + + Spacer(modifier = Modifier.height(13.dp)) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(13.dp)) + + Text( + text = email, + fontSize = 12.sp, + color = Color.Gray + ) + } + } + + // POPUP EDIT + if (showDialog) { + + AlertDialog( + onDismissRequest = { + showDialog = false + }, + + title = { + Text("Edit Akun", + fontSize = 18.sp, + fontFamily = Inria_Bold, + fontWeight = FontWeight.Medium, + color = Color.Black, + ) + }, + + text = { + + Column { + + Text( + "Ubah Nama", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + BasicTextField( + value = editNama, + onValueChange = { + editNama = it + }, + + textStyle = TextStyle( + color = Color.White.copy(alpha = 0.5f), + fontSize = 16.sp + ), + + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + + decorationBox = { innerTextField -> + + if (editNama.isEmpty()) { + + Text( + "Nama", + color = Color.Gray + ) + } + + innerTextField() + } + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + "Ubah Email", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF2F6F4E) + ), + elevation = CardDefaults.cardElevation(4.dp), + ) { + + BasicTextField( + value = editEmail, + onValueChange = { + editEmail = it + }, + + textStyle = TextStyle( + color = Color.White.copy(alpha = 0.5f), + fontSize = 16.sp + ), + + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + + decorationBox = { innerTextField -> + + if (editEmail.isEmpty()) { + + Text( + "Email", + color = Color.Gray + ) + } + + innerTextField() + } + ) + } + } + }, + + confirmButton = { + + TextButton( + onClick = { + + val url = "http://kopikaosmartsystem.my.id/update_profile.php" + + val request = object : StringRequest( + Request.Method.POST, + url, + + { response -> + + Toast.makeText( + context, + response, + Toast.LENGTH_SHORT + ).show() + + if (response.contains("berhasil")) { + + // Simpan SharedPreferences + val editor = sharedPreferences.edit() + + editor.putString("nama", editNama) + editor.putString("email", editEmail) + + editor.apply() + + // Update tampilan + nama = editNama + email = editEmail + + showDialog = false + } + }, + + { error -> + + android.widget.Toast.makeText( + context, + "Error: ${error.message}", + android.widget.Toast.LENGTH_SHORT + ).show() + } + + ) { + + override fun getParams(): MutableMap { + + return hashMapOf( + + // email lama untuk mencari akun + "email_lama" to email, + + // data baru + "nama_baru" to editNama, + "email_baru" to editEmail + ) + } + } + + val queue = Volley.newRequestQueue(context) + queue.add(request) + } + ) { + Text("Simpan") + } + }, + + dismissButton = { + + TextButton( + onClick = { + showDialog = false + } + ) { + Text("Batal") + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/pengaturan/BahasaCard.kt b/app/src/main/java/pengaturan/BahasaCard.kt new file mode 100644 index 0000000..559c51c --- /dev/null +++ b/app/src/main/java/pengaturan/BahasaCard.kt @@ -0,0 +1,110 @@ +package com.example.uitugasakhir.pengaturan + +import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Inria_Regular +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.ui.res.stringResource +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.MainViewModel + + + + +@Composable +fun BahasaCardSection( + viewModel: MainViewModel +) { + var bahasa by remember { mutableStateOf("Indonesia") } + val context = LocalContext.current + + + + Text( + text = stringResource(R.string.language), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 110.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + + Text( + (stringResource(R.string.indonesia)), + modifier = Modifier.clickable { + + bahasa = "Indonesia" + + viewModel.setLocale(context, "id") + + Toast.makeText( + context, + "Bahasa Indonesia dipilih", + Toast.LENGTH_SHORT + ).show() + } + ) + + Spacer(modifier = Modifier.height(13.dp)) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(13.dp)) + + Text( + stringResource(R.string.english), + modifier = Modifier.clickable { + + bahasa = "English" + + viewModel.setLocale(context, "en") + + Toast.makeText( + context, + "English selected", + Toast.LENGTH_SHORT + ).show() + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pengaturan/BantuanCard.kt b/app/src/main/java/pengaturan/BantuanCard.kt new file mode 100644 index 0000000..97e142f --- /dev/null +++ b/app/src/main/java/pengaturan/BantuanCard.kt @@ -0,0 +1,90 @@ +package com.example.uitugasakhir.pengaturan + +import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Inria_Regular +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.MainViewModel + + + + +@Composable +fun BantuanCardSection( + navController: NavController, + viewModel: MainViewModel +) { + + val context = LocalContext.current + + Text( + text = stringResource(R.string.help_support), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 100.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + + Text( + text = stringResource(R.string.guide), + modifier = Modifier.clickable { + navController.navigate("panduan") + } + ) + + Spacer(modifier = Modifier.height(13.dp)) + + HorizontalDivider( + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(13.dp)) + + Text( + text = stringResource(R.string.contact_developer), + modifier = Modifier.clickable { + Toast.makeText(context, "Hubungi developer", Toast.LENGTH_SHORT).show() + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/pengaturan/NotifikasiCard.kt b/app/src/main/java/pengaturan/NotifikasiCard.kt new file mode 100644 index 0000000..101e68e --- /dev/null +++ b/app/src/main/java/pengaturan/NotifikasiCard.kt @@ -0,0 +1,134 @@ +package com.example.uitugasakhir.pengaturan + +import android.widget.Toast +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.example.uitugasakhir.ViewModel.SensorViewModel +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Inria_Regular +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.MainViewModel + + +@Composable +fun NotifikasiCardSection( + viewModel: MainViewModel, + sensorViewModel: SensorViewModel +) { + + val notifTanah = sensorViewModel.notifTanahAktif + val notifHujan = sensorViewModel.notifHujanAktif + val buzzerAktif = sensorViewModel.buzzerAktif + + val context = LocalContext.current + + Text( + text = stringResource(R.string.notification), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .size(width = 333.dp, height = 140.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ) + ) { + Column(modifier = Modifier.padding(16.dp) + .verticalScroll(rememberScrollState())) { + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( text = stringResource(R.string.soil),) + Switch( + checked = notifTanah, + onCheckedChange = { + + sensorViewModel.setNotifTanah(it) + + Toast.makeText( + context, + if (it) "Notif tanah aktif" + else "Notif tanah dimatikan", + Toast.LENGTH_SHORT + ).show() + } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( text = stringResource(R.string.rain),) + Switch( + checked = notifHujan, + onCheckedChange = { + + sensorViewModel.setNotifHujan(it) + + Toast.makeText( + context, + if (it) "Notif hujan aktif" + else "Notif hujan dimatikan", + Toast.LENGTH_SHORT + ).show() + } + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("Buzzer") + Switch( + checked = buzzerAktif, + onCheckedChange = { + + sensorViewModel.setBuzzer(it) + + Toast.makeText( + context, + if (it) "Buzzer aktif" + else "Buzzer dimatikan", + Toast.LENGTH_SHORT + ).show() + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/service/SensorMonitoringService.kt b/app/src/main/java/service/SensorMonitoringService.kt new file mode 100644 index 0000000..0dfbd1d --- /dev/null +++ b/app/src/main/java/service/SensorMonitoringService.kt @@ -0,0 +1,56 @@ +package com.example.uitugasakhir + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class SensorMonitoringService : Service() { + + private val scope = CoroutineScope(Dispatchers.IO) + private var job: Job? = null + + override fun onStartCommand( + intent: Intent?, + flags: Int, + startId: Int + ): Int { + + SensorNotification.createChannel(this) + + job = scope.launch { + + while (true) { + + // Ambil data sensor dari Firebase/API + // lalu cek ambang batas + + val suhu = 35 + + if (suhu > 30) { + + SensorNotification.showNotification( + this@SensorMonitoringService, + "Peringatan Suhu", + "Suhu terlalu tinggi" + ) + } + + delay(15000) + } + } + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + job?.cancel() + } + + override fun onBind(intent: Intent?): IBinder? = null +} \ No newline at end of file diff --git a/app/src/main/java/service/SensorNotification.kt b/app/src/main/java/service/SensorNotification.kt new file mode 100644 index 0000000..aef55c7 --- /dev/null +++ b/app/src/main/java/service/SensorNotification.kt @@ -0,0 +1,77 @@ +package com.example.uitugasakhir + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import android.Manifest +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + + +object SensorNotification { + + private const val CHANNEL_ID = "sensor_alert_channel" + private const val NOTIFICATION_ID = 1001 + + fun createChannel(context: Context) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + val channel = NotificationChannel( + CHANNEL_ID, + "Sensor Alert", + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Notifikasi monitoring sensor" + } + + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + manager.createNotificationChannel(channel) + } + } + + fun showNotification( + context: Context, + title: String, + message: String + ) { + + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra("route", "notifikasi") + } + + val pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val builder = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setContentIntent(pendingIntent) // 🔥 INI PENTING + + + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(context) + .notify(NOTIFICATION_ID, builder.build()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_manual/MonitoringRataRata.kt b/app/src/main/java/ui_manual/MonitoringRataRata.kt new file mode 100644 index 0000000..703e840 --- /dev/null +++ b/app/src/main/java/ui_manual/MonitoringRataRata.kt @@ -0,0 +1,277 @@ +package com.example.uitugasakhir.ui_manual + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.app.ui.theme.Inria_Regular +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.MainViewModel +import kotlinx.coroutines.launch + +@Composable +fun RataRataSection( + snackbarHostState: SnackbarHostState +) { + + val mainViewModel: MainViewModel = viewModel() + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val avgKU = mainViewModel.avgKU + val avgSU = mainViewModel.avgSU + val avgKT = mainViewModel.avgKT + val avgIH = mainViewModel.avgIH + + Text( + text = stringResource(R.string.average_sensor_status), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.a_summary_of_the_latest_status_from_each_sensor), + fontSize = 12.sp, + fontFamily = Inria_Regular, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + .height(190.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Column( + modifier = Modifier.fillMaxSize() + ) { + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(12.dp) + ) { + + item { + SensorCard( + title = "💧 Udara", + value = if(avgKU > 0) "${avgKU.toInt()}%" else "--", + status = if(avgKU > 0) statusKelembaban(avgKU) else "Memuat...", + color = Color(0xFF4FC3F7) + ) + } + + item { + SensorCard( + title = "🌡 Suhu", + value = if(avgSU > 0) "${avgSU.toInt()}°C" else "--", + status = if(avgSU > 0) statusSuhu(avgSU) else "Memuat...", + color = Color(0xFF2E8B57) + ) + } + + item { + SensorCard( + title = "🌱 Tanah", + value = if(avgKT > 0) "${avgKT.toInt()}%" else "--", + status = if(avgKT > 0) statusKelembaban(avgKT) else "Memuat...", + color = Color(0xFF8B5A2B) + ) + } + + item { + SensorCard( + title = "🌧 Hujan", + value = if(avgIH > 0) "${avgIH.toInt()}%" else "--", + status = if(avgIH > 0){ + when{ + avgIH < 40 -> "Ringan" + avgIH < 70 -> "Sedang" + else -> "Deras" + } + }else{ + "Memuat..." + }, + color = Color(0xFF1E3A5F) + ) + } + } + + Spacer( + modifier = Modifier.weight(1f) + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFE8F5E9) + ) + ){ + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ){ + + Text( + text = "📥", + fontSize = 26.sp + ) + + Spacer( + modifier = Modifier.width(10.dp) + ) + + Column( + modifier = Modifier.weight(1f) + ){ + + Text( + text = "Unduh Data Monitoring", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1B5E20) + ) + + Text( + text = "Ekspor data sensor untuk laporan", + fontSize = 11.sp, + color = Color(0xFF388E3C) + ) + } + + Button( + onClick = { + + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse( + "http://kopikaosmartsystem.my.id/download_sensor_excel.php" + ) + ) + + try{ + + context.startActivity(intent) + + scope.launch { + snackbarHostState.showSnackbar( + "Membuka data sensor..." + ) + } + + }catch(e:Exception){ + + scope.launch { + snackbarHostState.showSnackbar( + "Gagal membuka data" + ) + } + + } + + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF4CAF50) + ) + ){ + + Text( + text = "Unduh", + fontSize = 11.sp + ) + + } + + } + + } + + } + + } + +} + + +@Composable +fun SensorCard( + title:String, + value:String, + status:String, + color:Color +){ + + Card( + modifier = Modifier + .width(95.dp) + .height(85.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ), + elevation = CardDefaults.cardElevation(2.dp) + ){ + + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.SpaceBetween + ){ + + Text( + text = title, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + color = color + ) + + Text( + text = value, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Text( + text = status, + fontSize = 9.sp, + color = Color.Gray + ) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ui_manual/MonitoringUI.kt b/app/src/main/java/ui_manual/MonitoringUI.kt new file mode 100644 index 0000000..81554a3 --- /dev/null +++ b/app/src/main/java/ui_manual/MonitoringUI.kt @@ -0,0 +1,218 @@ +package com.example.uitugasakhir.ui_manual + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.app.ui.theme.Inria_Regular +import com.example.uitugasakhir.ViewModel.SensorViewModel +import androidx.compose.ui.platform.LocalContext +import com.example.uitugasakhir.R +import androidx.compose.ui.res.stringResource + + +// ================= STATUS ================= +fun statusKelembaban(value: Float): String { + return when { + value < 40 -> "Kering" + value < 70 -> "Normal" + else -> "Lembab" + } +} + +fun statusSuhu(value: Float): String { + return when { + value < 25 -> "Dingin" + value < 32 -> "Normal" + else -> "Panas" + } +} + +// ================= UI ================= +@Composable +fun SensorSection( +) { + + val context = LocalContext.current + + val sensorVm: SensorViewModel = viewModel() + val data = sensorVm.data + + // 🔥 load sekali saat pertama buka + LaunchedEffect(Unit) { + sensorVm.getData(context) + } + + val ku = data?.kelembaban_udara + val su = data?.suhu_udara + val kt = data?.kelembaban_tanah + val ih = data?.intensitas_hujan + + val progressKU = ku?.div(100f) + val progressSU = su?.div(50f) + val progressKT = kt?.div(100f) + val progressIH = ih?.div(100f) + + Column { + + Text( + text = stringResource(R.string.sensor_monitoring), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.monitor_environmental_conditions_in_real_time), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + + com.example.uitugasakhir.ui_otomatis.SensorCard( + title = "Kel. Udara", + value = if (ku != null) "${ku.toInt()}%" else "--", + status = if (ku != null) com.example.uitugasakhir.ui_otomatis.statusKelembaban( + ku + ) else "--", + progress = progressKU ?: 0f, + color = Color(0xFF4FC3F7) + ) + + com.example.uitugasakhir.ui_otomatis.SensorCard( + title = "Suhu Udara", + value = if (su != null) "${su.toInt()}°C" else "--", + status = if (su != null) com.example.uitugasakhir.ui_otomatis.statusSuhu(su) else "--", + progress = progressSU ?: 0f, + color = Color(0xFF2E8B57) + ) + + com.example.uitugasakhir.ui_otomatis.SensorCard( + title = "Kel. Tanah", + value = if (kt != null) "${kt.toInt()}%" else "--", + status = if (kt != null) com.example.uitugasakhir.ui_otomatis.statusKelembaban( + kt + ) else "--", + progress = progressKT ?: 0f, + color = Color(0xFF8B5A2B) + ) + + com.example.uitugasakhir.ui_otomatis.SensorCard( + title = "Int. Hujan", + value = if (ih != null) "${ih.toInt()}%" else "--", + status = if (ih != null) { + when { + ih < 40 -> "Ringan" + ih < 70 -> "Sedang" + else -> "Deras" + } + } else "--", + progress = progressIH ?: 0f, + color = Color(0xFF1E3A5F) + ) + } + } + } +} + + +@Composable +fun SensorCard( + title: String, + value: String, + status: String, + progress: Float, + color: Color +) { + Card( + modifier = Modifier + .width(75.dp) + .height(120.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ), + elevation = CardDefaults.cardElevation(3.dp) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(50.dp) + ) { + + CircularProgressIndicator( + progress = { 1f }, + color = Color.LightGray, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + CircularProgressIndicator( + progress = { progress }, + color = color, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + Text( + text = value, + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) + } + + Text( + text = title, + fontSize = 9.sp, + fontWeight = FontWeight.Bold + ) + + Text( + text = status, + fontSize = 8.sp, + color = Color.Gray + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_manual/PenyiramanUI.kt b/app/src/main/java/ui_manual/PenyiramanUI.kt new file mode 100644 index 0000000..700d40e --- /dev/null +++ b/app/src/main/java/ui_manual/PenyiramanUI.kt @@ -0,0 +1,169 @@ +package com.example.uitugasakhir.ui_manual + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.animation.animateColorAsState +import android.util.Log +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import com.example.app.ui.theme.Inria_Regular +import com.example.uitugasakhir.R + + +@Composable +fun PenyiramanSection( + mode: String, + pumpStatus: String, + isLoading: Boolean = false, + onPumpChange: (String) -> Unit +) { + val isManual = mode == "Manual" + + val isOn = pumpStatus == "ON" + + val statusColor by animateColorAsState( + targetValue = if (isOn) Color(0xFF2F6F4E) else Color(0xFFD81F23), + label = "" + ) + + Column { + + // ================= TITLE ================= + Text( + text = stringResource(R.string.watering), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.watering_is_performed_manually), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + // ================= STATUS ================= + Text( + text = if (isOn) "🟢 Pompa Aktif" else "🔴 Pompa Mati", + color = statusColor, + fontSize = 14.sp, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // ================= CARD ================= + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + .height(120.dp), + shape = RoundedCornerShape(24.dp), + elevation = CardDefaults.cardElevation(4.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFF5F5F5) + ) + ) { + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + + // ================= SEGMENTED ================= + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(50.dp) + .clip(RoundedCornerShape(50.dp)) + .background(Color(0xFFE0E0E0)) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + // ===== ON ===== + val onColor by animateColorAsState( + targetValue = if (isOn) Color(0xFF2F6F4E) else Color.Transparent, + label = "" + ) + + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(50.dp)) + .background(color = onColor) + .clickable(enabled = isManual && !isLoading) { + + Log.d("DEBUG", "PUMP CHANGE DIPANGGIL") + + onPumpChange("ON") + }, + contentAlignment = Alignment.Center + ) { + Text( + text = if (isLoading && isOn) "Menyalakan..." else "🌱 ON", + color = if (isOn) Color.White else Color.Black + ) + } + + // ===== OFF ===== + val offColor by animateColorAsState( + targetValue = if (!isOn) Color(0xFFD81F23) else Color.Transparent, + label = "" + ) + + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(50.dp)) + .background(color = offColor) + .clickable(enabled = isManual && !isLoading) { + + Log.d("BUTTON", "OFF DIKLIK") + + onPumpChange("OFF") + }, + contentAlignment = Alignment.Center + ) { + Text( + text = if (isLoading && !isOn) "Mematikan..." else "🛑 OFF", + color = if (!isOn) Color.White else Color.Black + ) + } + } + + // ================= LOADING ================= + if (isLoading) { + Spacer(modifier = Modifier.height(10.dp)) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_manual/RiwayatUI.kt b/app/src/main/java/ui_manual/RiwayatUI.kt new file mode 100644 index 0000000..a3e8ad9 --- /dev/null +++ b/app/src/main/java/ui_manual/RiwayatUI.kt @@ -0,0 +1,144 @@ +package com.example.uitugasakhir.ui_manual + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Poppins_Bold +import com.example.app.ui.theme.Poppins_Medium +import com.example.uitugasakhir.R +import com.example.uitugasakhir.model.RiwayatItem + +@Composable +fun RiwayatSection(riwayatList: List) { + + Column { + + Text( + text = stringResource(R.string.history), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.manual_watering_activity_log), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .padding(bottom = 25.dp) + .size(width = 333.dp, height = 214.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 8.dp) + ) { + + items(riwayatList) { item -> + + val bgColor = when (item.status) { + "Berjalan" -> Color(0xFFD6F5DD) + "Selesai" -> Color(0xFFFFE0E0) + "Monitoring" -> Color(0xFFFFF3CD) + "Delay" -> Color(0xFFE3F2FD) + else -> Color.LightGray + } + + val textColor = when (item.status) { + "Berjalan" -> Color(0xFF2E7D32) + "Selesai" -> Color.Red + "Monitoring" -> Color(0xFFB8860B) + "Delay" -> Color(0xFF1565C0) + else -> Color.DarkGray + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Column(modifier = Modifier.weight(1f)) { + + Text( + text = "${item.waktu} • ${item.mode}", + fontSize = 13.sp, + fontFamily = Poppins_Bold, + color = Color.Black.copy(alpha = 0.8f) + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Text( + text = item.aksi, + fontSize = 13.sp, + fontFamily = Poppins_Bold, + color = Color.Black.copy(alpha = 0.8f) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "${item.keterangan} | Jeda: ${item.jeda} menit", + fontSize = 11.sp, + fontFamily = Poppins_Medium, + color = Color.Gray + ) + } + + Box( + modifier = Modifier + .background(bgColor, RoundedCornerShape(12.dp)) + .padding(horizontal = 10.dp, vertical = 5.dp) + ) { + Text( + text = item.status, + fontSize = 10.sp, + fontFamily = Poppins_Medium, + color = textColor + ) + } + } + + HorizontalDivider( + thickness = 0.8.dp, + color = Color.Gray.copy(alpha = 0.3f) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_otomatis/AmbangBatasUI.kt b/app/src/main/java/ui_otomatis/AmbangBatasUI.kt new file mode 100644 index 0000000..329bb9f --- /dev/null +++ b/app/src/main/java/ui_otomatis/AmbangBatasUI.kt @@ -0,0 +1,452 @@ +package com.example.uitugasakhir.ui_otomatis + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import android.widget.Toast +import androidx.compose.ui.platform.LocalContext +import com.example.app.ui.theme.Inria_Regular +import org.eclipse.paho.client.mqttv3.* + +import org.json.JSONObject +import android.util.Log +import com.example.uitugasakhir.R +import com.example.uitugasakhir.model.AmbangBatas +import com.example.uitugasakhir.api.ApiAmbangBatas + +import com.example.uitugasakhir.mqtt.MqttManager + +@Composable +fun AmbangBatasSection() { + + val context = LocalContext.current + + var popupMessage by remember { mutableStateOf("") } + + val mqtt = remember { + MqttManager(context) + } + + var showPopup by remember { mutableStateOf(false) } + var ledState by remember { mutableStateOf(0) } + var buzzerState by remember { mutableStateOf(0) } + + // ================= STATE ================= + + var isLoadingSave by remember { mutableStateOf(false) } + + var showDialog by remember { mutableStateOf(false) } + var currentField by remember { mutableStateOf("") } + + var ambang by remember { mutableStateOf(null) } + var criticalTanah by remember { mutableStateOf("") } + + var minTanah by remember { mutableStateOf("") } + var maxTanah by remember { mutableStateOf("") } + + var minSuhu by remember { mutableStateOf("") } + var maxSuhu by remember { mutableStateOf("") } + + var minUdara by remember { mutableStateOf("") } + var maxUdara by remember { mutableStateOf("") } + + var isSaved by remember { mutableStateOf(false) } + + // ================= LOAD DATA ================= + + LaunchedEffect(Unit) { + ApiAmbangBatas.getAmbangBatas( + context = context, + onSuccess = { + ambang = it + minTanah = it.min_tanah.toString() + maxTanah = it.max_tanah.toString() + minSuhu = it.min_suhu.toString() + maxSuhu = it.max_suhu.toString() + minUdara = it.min_udara.toString() + maxUdara = it.max_udara.toString() + criticalTanah = it.critical_tanah.toString() + isSaved = true + }, + onError = { + Toast.makeText( + context, + it, + Toast.LENGTH_SHORT + ).show() + } + ) + } + + // ================= TITLE ================= + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + + Text( + text = "Atur ambang batas", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.width(15.dp)) + } + + Spacer(modifier = Modifier.height(15.dp)) + + // ================= CARD ================= + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Column { + + SettingItem( + title = "Kelembapan tanah", + minValue = minTanah, + maxValue = maxTanah, + isSaved = true, + iconRes = R.drawable.decagon, + onEditClick = { + currentField = "tanah" + showDialog = true + } + ) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp) + ) + + SettingItem( + title = "Suhu udara", + minValue = minSuhu, + maxValue = maxSuhu, + isSaved = true, + iconRes = R.drawable.thermometer, + onEditClick = { + currentField = "suhu" + showDialog = true + } + ) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + // ================= DIALOG ================= + + if (showDialog) { + + AlertDialog( + onDismissRequest = { + showDialog = false + }, + + title = { + Text("Atur Ambang Batas - $currentField") + }, + + text = { + + Column { + + // MIN + Text("Minimal (%)") + + TextField( + value = when (currentField) { + "tanah" -> minTanah + "suhu" -> minSuhu + else -> minUdara + }, + + onValueChange = { value -> + + when (currentField) { + + "tanah" -> minTanah = value + + "suhu" -> minSuhu = value + + "udara" -> minUdara = value + } + }, + + singleLine = true + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // MAX + Text("Maksimal (%)") + + TextField( + value = when (currentField) { + "tanah" -> maxTanah + "suhu" -> maxSuhu + else -> maxUdara + }, + + onValueChange = { value -> + + when (currentField) { + + "tanah" -> maxTanah = value + + "suhu" -> maxSuhu = value + + "udara" -> maxUdara = value + } + }, + + singleLine = true + ) + + if (currentField == "tanah") { + + Spacer(modifier = Modifier.height(12.dp)) + + Text("Critical (%)") + + TextField( + value = criticalTanah, + onValueChange = { criticalTanah = it }, + singleLine = true + ) + } + } + }, + + confirmButton = { + + TextButton( + onClick = { + // 1. SAVE DATABASE (backup) + ApiAmbangBatas.saveAmbangBatas( + context = context, + minTanah = minTanah.toIntOrNull() ?: 0, + maxTanah = maxTanah.toIntOrNull() ?: 0, + minSuhu = minSuhu.toIntOrNull() ?: 0, + maxSuhu = maxSuhu.toIntOrNull() ?: 0, + minUdara = minUdara.toIntOrNull() ?: 0, + maxUdara = maxUdara.toIntOrNull() ?: 0, + criticalTanah = maxTanah.toIntOrNull() ?: 0, + onSuccess = { + Toast.makeText(context, "Saved DB", Toast.LENGTH_SHORT).show() + }, + onError = { + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + } + ) + + // 2. MQTT REALTIME SEND (INI YANG PENTING) + mqtt.publish( + "iot/config/update", + buildThresholdJson( + minTanah, + maxTanah, + criticalTanah, + minSuhu, + maxSuhu, + minUdara, + maxUdara + ) + ) + + Toast.makeText(context, "Sent to ESP32", Toast.LENGTH_SHORT).show() + + showDialog = false + } + ) { + Text("Simpan") + } + }, + + dismissButton = { + + TextButton( + onClick = { + showDialog = false + } + ) { + Text("Batal") + } + } + ) + } +} + +@Composable +fun SettingItem( + title: String, + minValue: String, + maxValue: String, + isSaved: Boolean, + iconRes: Int, + onEditClick: () -> Unit +) { + + Column( + modifier = Modifier.padding(top = 8.dp) + ) { + + // TITLE + + Row( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + + Row( + modifier = Modifier.width(170.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(18.dp), + colorFilter = ColorFilter.tint(Color(0xFF6B6666)) + ) + + Text( + text = title, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(start = 9.dp) + ) + } + } + + Spacer(modifier = Modifier.height(6.dp)) + + // CONTENT + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 43.dp, end = 15.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Column { + + Text( + text = "Min: $minValue%", + fontSize = 15.sp, + fontWeight = FontWeight.Normal, + color = Color.Black.copy(alpha = 0.7f) + ) + + Text( + text = "Max: $maxValue%", + fontSize = 15.sp, + fontWeight = FontWeight.Normal, + color = Color.Black.copy(alpha = 0.7f) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + text = if (isSaved) + "Tersimpan" + else + "Belum diatur", + + fontSize = 10.sp, + color = Color.Gray + ) + + Spacer(modifier = Modifier.width(5.dp)) + + if (isSaved) { + + Image( + painter = painterResource(id = R.drawable.checked), + contentDescription = null, + modifier = Modifier.size(13.dp) + ) + } + + Spacer(modifier = Modifier.width(5.dp)) + + Box( + modifier = Modifier + .size(30.dp) + .clickable { + onEditClick() + }, + + contentAlignment = Alignment.Center + ) { + + Image( + painter = painterResource(id = R.drawable.edit_text), + contentDescription = "edit", + modifier = Modifier.size(18.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +fun buildThresholdJson( + minTanah: String, + maxTanah: String, + criticalTanah: String, + minSuhu: String, + maxSuhu: String, + minUdara: String, + maxUdara: String +): String { + return """ + { + "type": "threshold", + "tanah": { + "min": ${minTanah.toIntOrNull() ?: 0}, + "max": ${maxTanah.toIntOrNull() ?: 0}, + "critical": ${criticalTanah.toIntOrNull() ?: 0} + }, + "suhu": { + "min": ${minSuhu.toIntOrNull() ?: 0}, + "max": ${maxSuhu.toIntOrNull() ?: 0} + }, + "udara": { + "min": ${minUdara.toIntOrNull() ?: 0}, + "max": ${maxUdara.toIntOrNull() ?: 0} + } + } + """.trimIndent() +} \ No newline at end of file diff --git a/app/src/main/java/ui_otomatis/JadwalUI.kt b/app/src/main/java/ui_otomatis/JadwalUI.kt new file mode 100644 index 0000000..65c199b --- /dev/null +++ b/app/src/main/java/ui_otomatis/JadwalUI.kt @@ -0,0 +1,220 @@ +package com.example.uitugasakhir.ui_otomatis + +import android.app.TimePickerDialog +import android.content.Context +import android.util.Log +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.example.app.ui.theme.Inria_Regular + +import com.example.uitugasakhir.R +import com.example.uitugasakhir.model.JadwalItem + +@Composable +fun JadwalSection( + jadwalList: List, + loadJadwal: () -> Unit, + context: Context, + onDeleteClick: (String) -> Unit +) { + + fun getWaktuStatus(hour: Int, minute: Int): String { + return when { + hour in 5..10 -> "Pagi" + hour in 11..14 -> "Siang" + hour in 15..18 && !(hour == 18 && minute >= 30) -> "Petang" + hour == 18 && minute >= 30 -> "Senja" + hour in 19..23 -> "Malam" + else -> "Dini Hari" + } + } + + Column { + + Text("Atur jadwal", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray,) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 150.dp, max = 350.dp), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(Color(0xFFEDEDED)) + ) { + + LazyColumn { + itemsIndexed(jadwalList) { index, item -> + + Column{ + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Row(verticalAlignment = Alignment.CenterVertically) { + + // 🗑️ DELETE + IconButton( + onClick = { + Log.d("DELETE_CLICK", "ID: ${item.id}") + onDeleteClick(item.id) + } + ) { + Icon( + painter = painterResource(id = R.drawable.trash), + contentDescription = "Hapus", + modifier = Modifier.size(18.dp) + ) + } + + Text("Jadwal ${index + 1}", + fontSize = 15.sp, + fontWeight = FontWeight.Normal) + } + + // ✏️ EDIT + IconButton( + onClick = { + TimePickerDialog( + context, + { _, h, m -> + + val status = getWaktuStatus(h, m) + val waktu = String.format("%02d:%02d", h, m) + + Log.d("EDIT_KIRIM", "ID: ${item.id}") + Log.d("EDIT_KIRIM", "Waktu: $waktu") + Log.d("EDIT_KIRIM", "Status: $status") + + val request = object : StringRequest( + Request.Method.POST, + "http://kopikaosmartsystem.my.id/edit_jadwal.php", + + { response -> + Log.d("EDIT_SUCCESS", "Response: $response") + loadJadwal() + }, + + { error -> + Log.e("EDIT_ERROR", "Error: ${error.message}") + Log.e("EDIT_ERROR", "Detail: ${error.toString()}") + } + ) { + override fun getParams(): MutableMap { + + val params = hashMapOf( + "id" to item.id.toString(), + "waktu" to waktu, + "status" to status + ) + + Log.d("EDIT_PARAMS", params.toString()) + + return params + } + } + + Volley.newRequestQueue(context).add(request) + }, + 0, 0, true + ).show() + } + ) { + Icon( + painter = painterResource(id = R.drawable.edit_text), + contentDescription = "Edit", + modifier = Modifier.size(18.dp) + ) + } + } + + // 🔥 tampilkan data + Text( + text = "${item.waktu} ${item.status}", + modifier = Modifier.padding(start = 48.dp) + .offset(y = -8.dp) + ) + + Spacer(modifier = Modifier.height(6.dp)) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + + // ➕ TAMBAH + item { + TextButton( + onClick = { + TimePickerDialog( + context, + { _, h, m -> + + val status = getWaktuStatus(h, m) + val waktu = String.format("%02d:%02d:00", h, m) + + Log.d("TAMBAH_KIRIM", "Waktu: $waktu") + Log.d("TAMBAH_KIRIM", "Status: $status") + + val request = object : StringRequest( + Request.Method.POST, + "http://kopikaosmartsystem.my.id/tambah_jadwal.php", + + { response -> + Log.d("TAMBAH_SUCCESS", "Response: $response") + loadJadwal() + }, + + { error -> + Log.e("TAMBAH_ERROR", "Error: ${error.message}") + Log.e("TAMBAH_ERROR", "Detail: ${error.toString()}") + } + ) { + override fun getParams(): MutableMap { + + val params = hashMapOf( + "waktu" to waktu, + "status" to status + ) + + Log.d("TAMBAH_PARAMS", params.toString()) + + return params + } + } + + Volley.newRequestQueue(context).add(request) + }, + 0, 0, true + ).show() + } + ) { + Text("+ Tambah Jadwal") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_otomatis/JedaUI.kt b/app/src/main/java/ui_otomatis/JedaUI.kt new file mode 100644 index 0000000..13cc3d2 --- /dev/null +++ b/app/src/main/java/ui_otomatis/JedaUI.kt @@ -0,0 +1,216 @@ +package com.example.uitugasakhir.ui_otomatis + +import android.util.Log +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Inria_Regular +import com.example.app.ui.theme.Poppins_Black +import com.example.uitugasakhir.R +import com.example.uitugasakhir.api.RetrofitClient +import kotlinx.coroutines.launch + +@Composable +fun JedaPenyiramanCard( + onJedaChanged: (Int) -> Unit +) { + + var jeda by remember { mutableStateOf("") } + var selectedUnit by remember { mutableStateOf("Detik") } + var expanded by remember { mutableStateOf(false) } + + val context = LocalContext.current + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + try { + val response = RetrofitClient.api.getJeda() + if (response.isSuccessful) { + + val body = response.body() + + if (body?.success == true) { + + val detik = body.data?.jeda?.toIntOrNull() ?: 0 + + jeda = detik.toString() + + onJedaChanged(detik) + + selectedUnit = "Detik" + } + } + } catch (e: Exception) { + + Log.e("JEDA", e.message.toString()) + } + } + + Column(modifier = Modifier.fillMaxWidth()) { + + Row { + Text( + text = "Atur jeda penyiraman", + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Gray, + ) + + Spacer(modifier = Modifier.width(15.dp)) + + Image( + painter = painterResource(id = R.drawable.clock), + contentDescription = null, + modifier = Modifier.size(14.dp) + .offset(y = 3.dp), + colorFilter = ColorFilter.tint(Color(0xFF6B6666)) + ) + } + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(Color(0xFFEDEDED)) + ) { + + Column( + modifier = Modifier.padding(top = 5.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Row(verticalAlignment = Alignment.CenterVertically) { + + BasicTextField( + value = jeda, + onValueChange = { + if (it.all { c -> c.isDigit() }) { + jeda = it.take(3) + } + }, + singleLine = true, + textStyle = LocalTextStyle.current.copy( + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + fontFamily = Poppins_Black + ), + modifier = Modifier.width(60.dp), + decorationBox = { + if (jeda.isEmpty()) { + Text("00", fontSize = 28.sp) + } + it() + } + ) + + Box { + Text( + text = selectedUnit, + modifier = Modifier.clickable { expanded = true } + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + listOf("Detik", "Menit", "Jam").forEach { + DropdownMenuItem( + text = { Text(it) }, + onClick = { + selectedUnit = it + expanded = false + } + ) + } + } + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp) + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + + // 🔴 HAPUS (set 0) + Button( + onClick = { + scope.launch { + try { + val res = RetrofitClient.api.setJeda("0") + Toast.makeText(context, res.body()?.message ?: "OK", Toast.LENGTH_SHORT).show() + jeda = "" + } catch (e: Exception) { + Log.e("JEDA", e.message.toString()) + } + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors(Color(0xFFD32F2F)) + ) { + Text("Hapus", color = Color.White) + } + + // 🟢 SIMPAN + Button( + onClick = { + if (jeda.isEmpty()) return@Button + + val value = jeda.toInt() + + val detik = when (selectedUnit) { + "Detik" -> value + "Menit" -> value * 60 + "Jam" -> value * 3600 + else -> value + } + + onJedaChanged(detik) + + scope.launch { + try { + val res = RetrofitClient.api.setJeda(detik.toString()) + + Toast.makeText( + context, + res.body()?.message ?: "Berhasil", + Toast.LENGTH_SHORT + ).show() + + } catch (e: Exception) { + Log.e("JEDA", e.message.toString()) + } + } + }, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors(Color(0xFF2F6F4E)) + ) { + Text("Simpan", color = Color.White) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_otomatis/MonitoringOtomatis.kt b/app/src/main/java/ui_otomatis/MonitoringOtomatis.kt new file mode 100644 index 0000000..3f8c45a --- /dev/null +++ b/app/src/main/java/ui_otomatis/MonitoringOtomatis.kt @@ -0,0 +1,212 @@ +package com.example.uitugasakhir.ui_otomatis + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.app.ui.theme.Inria_Regular +import com.example.uitugasakhir.ViewModel.SensorViewModel +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.example.uitugasakhir.R + + +// ================= STATUS ================= +fun statusKelembaban(value: Float): String { + return when { + value < 40 -> "Kering" + value < 70 -> "Normal" + else -> "Lembab" + } +} + +fun statusSuhu(value: Float): String { + return when { + value < 25 -> "Dingin" + value < 32 -> "Normal" + else -> "Panas" + } +} + +// ================= UI ================= +@Composable +fun SensorSectionOtomatis( + viewModel: SensorViewModel = viewModel() +) { + + val context = LocalContext.current + val data = viewModel.data + + // 🔥 load sekali saat pertama buka + LaunchedEffect(Unit) { + viewModel.getData(context) + } + + val ku = data?.kelembaban_udara + val su = data?.suhu_udara + val kt = data?.kelembaban_tanah + val ih = data?.intensitas_hujan + + val progressKU = ku?.div(100f) + val progressSU = su?.div(50f) + val progressKT = kt?.div(100f) + val progressIH = ih?.div(100f) + + Column { + + Text( + text = stringResource(R.string.sensor_monitoring), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.monitor_environmental_conditions_in_real_time), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + + SensorCard( + title = "Kel. Udara", + value = if (ku != null) "${ku.toInt()}%" else "--", + status = if (ku != null) statusKelembaban(ku) else "--", + progress = progressKU ?: 0f, + color = Color(0xFF4FC3F7) + ) + + SensorCard( + title = "Suhu Udara", + value = if (su != null) "${su.toInt()}°C" else "--", + status = if (su != null) statusSuhu(su) else "--", + progress = progressSU ?: 0f, + color = Color(0xFF2E8B57) + ) + + SensorCard( + title = "Kel. Tanah", + value = if (kt != null) "${kt.toInt()}%" else "--", + status = if (kt != null) statusKelembaban(kt) else "--", + progress = progressKT ?: 0f, + color = Color(0xFF8B5A2B) + ) + + SensorCard( + title = "Int. Hujan", + value = if (ih != null) "${ih.toInt()}%" else "--", + status = if (ih != null) { + when { + ih < 40 -> "Ringan" + ih < 70 -> "Sedang" + else -> "Deras" + } + } else "--", + progress = progressIH ?: 0f, + color = Color(0xFF1E3A5F) + ) + } + } + } +} + +@Composable +fun SensorCard( + title: String, + value: String, + status: String, + progress: Float, + color: Color +) { + Card( + modifier = Modifier + .width(75.dp) + .height(120.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ), + elevation = CardDefaults.cardElevation(3.dp) + ) { + + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(50.dp) + ) { + + CircularProgressIndicator( + progress = { 1f }, + color = Color.LightGray, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + CircularProgressIndicator( + progress = { progress }, + color = color, + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + Text( + text = value, + fontSize = 10.sp, + fontWeight = FontWeight.Bold + ) + } + + Text( + text = title, + fontSize = 9.sp, + fontWeight = FontWeight.Bold + ) + + Text( + text = status, + fontSize = 8.sp, + color = Color.Gray + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui_otomatis/MonitoringRataRataOtomatis.kt b/app/src/main/java/ui_otomatis/MonitoringRataRataOtomatis.kt new file mode 100644 index 0000000..433fa07 --- /dev/null +++ b/app/src/main/java/ui_otomatis/MonitoringRataRataOtomatis.kt @@ -0,0 +1,277 @@ +package com.example.uitugasakhir.ui_otomatis + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.example.app.ui.theme.Inria_Regular +import com.example.uitugasakhir.R +import com.example.uitugasakhir.ViewModel.MainViewModel +import kotlinx.coroutines.launch + +@Composable +fun RataRataOtomatisSection( + snackbarHostState: SnackbarHostState +) { + + val mainViewModel: MainViewModel = viewModel() + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val avgKU = mainViewModel.avgKU + val avgSU = mainViewModel.avgSU + val avgKT = mainViewModel.avgKT + val avgIH = mainViewModel.avgIH + + Text( + text = stringResource(R.string.average_sensor_status), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.a_summary_of_the_latest_status_from_each_sensor), + fontSize = 12.sp, + fontFamily = Inria_Regular, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + .height(190.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + Column( + modifier = Modifier.fillMaxSize() + ) { + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(12.dp) + ) { + + item { + SensorCard( + title = "💧 Udara", + value = if(avgKU > 0) "${avgKU.toInt()}%" else "--", + status = if(avgKU > 0) statusKelembaban(avgKU) else "Memuat...", + color = Color(0xFF4FC3F7) + ) + } + + item { + SensorCard( + title = "🌡 Suhu", + value = if(avgSU > 0) "${avgSU.toInt()}°C" else "--", + status = if(avgSU > 0) statusSuhu(avgSU) else "Memuat...", + color = Color(0xFF2E8B57) + ) + } + + item { + SensorCard( + title = "🌱 Tanah", + value = if(avgKT > 0) "${avgKT.toInt()}%" else "--", + status = if(avgKT > 0) statusKelembaban(avgKT) else "Memuat...", + color = Color(0xFF8B5A2B) + ) + } + + item { + SensorCard( + title = "🌧 Hujan", + value = if(avgIH > 0) "${avgIH.toInt()}%" else "--", + status = if(avgIH > 0){ + when{ + avgIH < 40 -> "Ringan" + avgIH < 70 -> "Sedang" + else -> "Deras" + } + }else{ + "Memuat..." + }, + color = Color(0xFF1E3A5F) + ) + } + } + + Spacer( + modifier = Modifier.weight(1f) + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFE8F5E9) + ) + ){ + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ){ + + Text( + text = "📥", + fontSize = 26.sp + ) + + Spacer( + modifier = Modifier.width(10.dp) + ) + + Column( + modifier = Modifier.weight(1f) + ){ + + Text( + text = "Unduh Data Monitoring", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1B5E20) + ) + + Text( + text = "Ekspor data sensor untuk laporan", + fontSize = 11.sp, + color = Color(0xFF388E3C) + ) + } + + Button( + onClick = { + + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse( + "http://kopikaosmartsystem.my.id/download_sensor_excel.php" + ) + ) + + try{ + + context.startActivity(intent) + + scope.launch { + snackbarHostState.showSnackbar( + "Membuka data sensor..." + ) + } + + }catch(e:Exception){ + + scope.launch { + snackbarHostState.showSnackbar( + "Gagal membuka data" + ) + } + + } + + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF4CAF50) + ) + ){ + + Text( + text = "Unduh", + fontSize = 11.sp + ) + + } + + } + + } + + } + + } + +} + + +@Composable +fun SensorCard( + title:String, + value:String, + status:String, + color:Color +){ + + Card( + modifier = Modifier + .width(95.dp) + .height(85.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ), + elevation = CardDefaults.cardElevation(2.dp) + ){ + + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + verticalArrangement = Arrangement.SpaceBetween + ){ + + Text( + text = title, + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + color = color + ) + + Text( + text = value, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Text( + text = status, + fontSize = 9.sp, + color = Color.Gray + ) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/ui_otomatis/RiwayatOtomatis.kt b/app/src/main/java/ui_otomatis/RiwayatOtomatis.kt new file mode 100644 index 0000000..d84f477 --- /dev/null +++ b/app/src/main/java/ui_otomatis/RiwayatOtomatis.kt @@ -0,0 +1,157 @@ +package com.example.uitugasakhir.ui_otomatis + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.app.ui.theme.Poppins_Bold +import com.example.app.ui.theme.Poppins_Medium +import com.example.uitugasakhir.model.RiwayatItem +import android.util.Log +import com.example.app.ui.theme.Inria_Regular +import androidx.compose.runtime.* +import androidx.compose.ui.res.stringResource +import com.example.uitugasakhir.R + +@Composable +fun RiwayatSectionOtomatis(riwayatList: List) { + + Column { + + val riwayatOtomatis = riwayatList.filter { + it.mode.trim().equals("Otomatis", ignoreCase = true) + } + + LaunchedEffect(Unit) { + Log.d("FILTER", "Total = ${riwayatList.size}") + + val hasil = riwayatList.filter { + it.mode.trim().equals("Otomatis", true) + } + + Log.d("FILTER", "Otomatis = ${hasil.size}") + + hasil.forEach { + Log.d("FILTER", "mode=${it.mode}") + } + } + + Text( + text = stringResource(R.string.history), + fontSize = 15.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = stringResource(R.string.manual_watering_activity_log), + fontSize = 12.sp, + fontFamily = Inria_Regular, + fontWeight = FontWeight.Normal, + color = Color.Gray, + modifier = Modifier.padding(horizontal = 28.dp) + ) + + Spacer(modifier = Modifier.height(15.dp)) + + Card( + modifier = Modifier + .padding(horizontal = 25.dp) + .padding(bottom = 25.dp) + .size(width = 333.dp, height = 214.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEDEDED) + ), + elevation = CardDefaults.cardElevation(4.dp) + ) { + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 8.dp) + ) { + + items(riwayatOtomatis) { item -> + + Log.d("MODE_DEBUG", "MODE = ${item.mode}") + + val bgColor = when (item.status) { + "Berjalan" -> Color(0xFFD6F5DD) + "Selesai" -> Color(0xFFFFE0E0) + "Monitoring" -> Color(0xFFFFF3CD) + "Delay" -> Color(0xFFE3F2FD) + else -> Color.LightGray + } + + val textColor = when (item.status) { + "Berjalan" -> Color(0xFF2E7D32) + "Selesai" -> Color.Red + "Monitoring" -> Color(0xFFB8860B) + "Delay" -> Color(0xFF1565C0) + else -> Color.DarkGray + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + + Column(modifier = Modifier.weight(1f)) { + + Text( + text = "${item.waktu} ${item.aksi}", + fontSize = 13.sp, + fontFamily = Poppins_Bold, + color = Color.Black.copy(alpha = 0.8f) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "${item.keterangan} | Jeda: ${item.jeda} menit", + fontSize = 11.sp, + fontFamily = Poppins_Medium, + color = Color.Gray + ) + } + + Box( + modifier = Modifier + .background(bgColor, RoundedCornerShape(12.dp)) + .padding(horizontal = 10.dp, vertical = 5.dp) + ) { + Text( + text = item.status, + fontSize = 10.sp, + fontFamily = Poppins_Medium, + color = textColor + ) + } + } + + HorizontalDivider( + thickness = 0.8.dp, + color = Color.Gray.copy(alpha = 0.3f) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/utils/DatabaseHelper.kt b/app/src/main/java/utils/DatabaseHelper.kt new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/utils/NotificationHelper.kt b/app/src/main/java/utils/NotificationHelper.kt new file mode 100644 index 0000000..aebb15b --- /dev/null +++ b/app/src/main/java/utils/NotificationHelper.kt @@ -0,0 +1,34 @@ +package com.example.uitugasakhir.utils + + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat + +fun showNotification(context: Context, title: String, message: String) { + + val channelId = "alert_channel" + + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // ✅ hanya untuk API 26 ke atas + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + "Alert", + NotificationManager.IMPORTANCE_HIGH + ) + manager.createNotificationChannel(channel) + } + + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + manager.notify(System.currentTimeMillis().toInt(), builder.build()) +} \ No newline at end of file diff --git a/app/src/main/java/viewmodel/MQTTViewModel.kt b/app/src/main/java/viewmodel/MQTTViewModel.kt new file mode 100644 index 0000000..787fbe5 --- /dev/null +++ b/app/src/main/java/viewmodel/MQTTViewModel.kt @@ -0,0 +1,103 @@ +package com.example.uitugasakhir.ViewModel + +import android.app.Application +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import com.example.uitugasakhir.mqtt.MqttManager +import org.json.JSONObject + +data class MQTTState( + val pump: String = "OFF", + val mode: String = "Manual", + val alarm: String = "AMAN" +) + +class MQTTViewModel( + application: Application +) : AndroidViewModel(application) { + + private val mqtt = MqttManager(application) + + var state by mutableStateOf(MQTTState()) + private set + + init { + + mqtt.connect( + + onMessage = { topic, message -> + + Log.d("MQTT_TOPIC", topic) + Log.d("MQTT_MESSAGE", message) + + when (topic) { + + // ========================= + // PUMP REALTIME + // ========================= + "smartplant/pump" -> { + + state = state.copy( + pump = message + ) + } + + // ========================= + // ALARM REALTIME + // ========================= + "smartplant/alarm" -> { + + state = state.copy( + alarm = message + ) + } + + // ========================= + // MODE REALTIME + // ========================= + "smartplant/mode" -> { + + state = state.copy( + mode = message + ) + } + } + } + ) + } + + // ========================= + // CONTROL PUMP + // ========================= + fun setPump(status: Boolean) { + + val msg = if (status) "ON" else "OFF" + + mqtt.publish( + "smartplant/manualPump", + msg + ) + + state = state.copy( + pump = msg + ) + } + + // ========================= + // CONTROL MODE + // ========================= + fun setMode(mode: String) { + + mqtt.publish( + "smartplant/mode", + mode + ) + + state = state.copy( + mode = mode + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/viewmodel/NotificationViewModel.kt b/app/src/main/java/viewmodel/NotificationViewModel.kt new file mode 100644 index 0000000..8ded297 --- /dev/null +++ b/app/src/main/java/viewmodel/NotificationViewModel.kt @@ -0,0 +1,204 @@ +package viewmodel + +import android.content.Context +import androidx.compose.runtime.* +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.uitugasakhir.api.RetrofitClient +import com.example.uitugasakhir.model.Sensor +import com.example.uitugasakhir.model.AmbangBatas +import com.example.uitugasakhir.utils.showNotification +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + + +import com.example.uitugasakhir.api.ApiAmbangBatas.getAmbangBatas + +class NotificationViewModel : ViewModel() { + + // ========================= + // DATA + // ========================= + var sensor by mutableStateOf(null) + private set + + var ambang by mutableStateOf(null) + private set + + // ========================= + // UI STATE + // ========================= + var warningMessage by mutableStateOf("Aman") + private set + + var isWarningActive by mutableStateOf(false) + private set + + private var lastPopupTime = 0L + + // ========================= + // LOAD DATA LOOP + // ========================= + fun loadData(context: Context) { + viewModelScope.launch { + + var notifTanah = false + var notifSuhu = false + var notifUdara = false + + while (true) { + try { + + val sensorResponse = RetrofitClient.api.getSensor() + val ambangResponse = RetrofitClient.api.getAmbangBatas() + + val sensorData = Sensor( + kelembaban_udara = sensorResponse.kelembaban_udara.toFloat(), + suhu_udara = sensorResponse.suhu_udara.toFloat(), + kelembaban_tanah = sensorResponse.kelembaban_tanah.toFloat(), + intensitas_hujan = sensorResponse.intensitas_hujan.toFloat() + ) + + val ambangData = AmbangBatas( + min_tanah = ambangResponse.min_tanah, + max_tanah = ambangResponse.max_tanah, + critical_tanah = ambangResponse.critical_tanah, + min_suhu = ambangResponse.min_suhu, + max_suhu = ambangResponse.max_suhu, + min_udara = ambangResponse.min_udara, + max_udara = ambangResponse.max_udara + ) + + sensor = sensorData + ambang = ambangData + + val warnings = mutableListOf() + + // ========================= + // TANAH + // ========================= + if (sensorData.kelembaban_tanah < ambangData.min_tanah) { + warnings.add("🌱 Tanah terlalu kering") + + if (!notifTanah) { + showNotification( + context, + "Tanah Kering", + "Kelembapan tanah di bawah batas minimum" + ) + notifTanah = true + } + + } else if (sensorData.kelembaban_tanah > ambangData.max_tanah) { + warnings.add("💧 Tanah terlalu basah") + + if (!notifTanah) { + showNotification( + context, + "Tanah Basah", + "Kelembapan tanah melebihi batas maksimum" + ) + notifTanah = true + } + + } else { + notifTanah = false + } + + // ========================= + // SUHU + // ========================= + if (sensorData.suhu_udara < ambangData.min_suhu) { + warnings.add("❄️ Suhu terlalu dingin") + + if (!notifSuhu) { + showNotification( + context, + "Suhu Rendah", + "Suhu udara di bawah batas minimum" + ) + notifSuhu = true + } + + } else if (sensorData.suhu_udara > ambangData.max_suhu) { + warnings.add("🔥 Suhu terlalu tinggi") + + if (!notifSuhu) { + showNotification( + context, + "Suhu Tinggi", + "Suhu udara melebihi batas maksimum" + ) + notifSuhu = true + } + + } else { + notifSuhu = false + } + + // ========================= + // UDARA + // ========================= + if (sensorData.kelembaban_udara < ambangData.min_udara) { + warnings.add("🌬 Udara terlalu kering") + + if (!notifUdara) { + showNotification( + context, + "Udara Kering", + "Kelembapan udara di bawah batas minimum" + ) + notifUdara = true + } + + } else if (sensorData.kelembaban_udara > ambangData.max_udara) { + warnings.add("🌫 Udara terlalu lembab") + + if (!notifUdara) { + showNotification( + context, + "Udara Lembab", + "Kelembapan udara melebihi batas maksimum" + ) + notifUdara = true + } + + } else { + notifUdara = false + } + + // ========================= + // UPDATE POPUP + // ========================= + val now = System.currentTimeMillis() + + if (warnings.isEmpty()) { + warningMessage = "Aman" + isWarningActive = false + } else { + warningMessage = warnings.joinToString("\n") + + if (now - lastPopupTime > 5000) { + isWarningActive = true + lastPopupTime = now + } + } + + } catch (e: Exception) { + e.printStackTrace() + } + + delay(5000) + } + } + } + + fun clearWarning() { + isWarningActive = false + } + + fun updateFromSocket(message: String) { + warningMessage = message + isWarningActive = true + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/alarm.png b/app/src/main/res/drawable/alarm.png new file mode 100644 index 0000000..9057d15 Binary files /dev/null and b/app/src/main/res/drawable/alarm.png differ diff --git a/app/src/main/res/drawable/back.png b/app/src/main/res/drawable/back.png new file mode 100644 index 0000000..a37d629 Binary files /dev/null and b/app/src/main/res/drawable/back.png differ diff --git a/app/src/main/res/drawable/calendar.png b/app/src/main/res/drawable/calendar.png new file mode 100644 index 0000000..104eb9b Binary files /dev/null and b/app/src/main/res/drawable/calendar.png differ diff --git a/app/src/main/res/drawable/ceklist.png b/app/src/main/res/drawable/ceklist.png new file mode 100644 index 0000000..de188b4 Binary files /dev/null and b/app/src/main/res/drawable/ceklist.png differ diff --git a/app/src/main/res/drawable/checked.png b/app/src/main/res/drawable/checked.png new file mode 100644 index 0000000..951ed8f Binary files /dev/null and b/app/src/main/res/drawable/checked.png differ diff --git a/app/src/main/res/drawable/chronometer.png b/app/src/main/res/drawable/chronometer.png new file mode 100644 index 0000000..ec50807 Binary files /dev/null and b/app/src/main/res/drawable/chronometer.png differ diff --git a/app/src/main/res/drawable/clock.png b/app/src/main/res/drawable/clock.png new file mode 100644 index 0000000..563a396 Binary files /dev/null and b/app/src/main/res/drawable/clock.png differ diff --git a/app/src/main/res/drawable/cogwheel.png b/app/src/main/res/drawable/cogwheel.png new file mode 100644 index 0000000..39cdedc Binary files /dev/null and b/app/src/main/res/drawable/cogwheel.png differ diff --git a/app/src/main/res/drawable/danger.gif b/app/src/main/res/drawable/danger.gif new file mode 100644 index 0000000..06ff7ee Binary files /dev/null and b/app/src/main/res/drawable/danger.gif differ diff --git a/app/src/main/res/drawable/decagon.png b/app/src/main/res/drawable/decagon.png new file mode 100644 index 0000000..dd30692 Binary files /dev/null and b/app/src/main/res/drawable/decagon.png differ diff --git a/app/src/main/res/drawable/drop.png b/app/src/main/res/drawable/drop.png new file mode 100644 index 0000000..3e57673 Binary files /dev/null and b/app/src/main/res/drawable/drop.png differ diff --git a/app/src/main/res/drawable/edit_text.png b/app/src/main/res/drawable/edit_text.png new file mode 100644 index 0000000..b040e5a Binary files /dev/null and b/app/src/main/res/drawable/edit_text.png differ diff --git a/app/src/main/res/drawable/email.png b/app/src/main/res/drawable/email.png new file mode 100644 index 0000000..da86c7f Binary files /dev/null and b/app/src/main/res/drawable/email.png differ diff --git a/app/src/main/res/drawable/garis.png b/app/src/main/res/drawable/garis.png new file mode 100644 index 0000000..86d011f Binary files /dev/null and b/app/src/main/res/drawable/garis.png differ diff --git a/app/src/main/res/drawable/ic_eye_closed.png b/app/src/main/res/drawable/ic_eye_closed.png new file mode 100644 index 0000000..62dc80a Binary files /dev/null and b/app/src/main/res/drawable/ic_eye_closed.png differ diff --git a/app/src/main/res/drawable/ic_eye_open.png b/app/src/main/res/drawable/ic_eye_open.png new file mode 100644 index 0000000..4cdb390 Binary files /dev/null and b/app/src/main/res/drawable/ic_eye_open.png differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/jam.png b/app/src/main/res/drawable/jam.png new file mode 100644 index 0000000..fa4aa28 Binary files /dev/null and b/app/src/main/res/drawable/jam.png differ diff --git a/app/src/main/res/drawable/kalender.png b/app/src/main/res/drawable/kalender.png new file mode 100644 index 0000000..7e7cacb Binary files /dev/null and b/app/src/main/res/drawable/kalender.png differ diff --git a/app/src/main/res/drawable/kopikakao.png b/app/src/main/res/drawable/kopikakao.png new file mode 100644 index 0000000..4f77284 Binary files /dev/null and b/app/src/main/res/drawable/kopikakao.png differ diff --git a/app/src/main/res/drawable/kunci.png b/app/src/main/res/drawable/kunci.png new file mode 100644 index 0000000..6f1b851 Binary files /dev/null and b/app/src/main/res/drawable/kunci.png differ diff --git a/app/src/main/res/drawable/logo_kopi.png b/app/src/main/res/drawable/logo_kopi.png new file mode 100644 index 0000000..d31bfed Binary files /dev/null and b/app/src/main/res/drawable/logo_kopi.png differ diff --git a/app/src/main/res/drawable/logokopikao.png b/app/src/main/res/drawable/logokopikao.png new file mode 100644 index 0000000..936616a Binary files /dev/null and b/app/src/main/res/drawable/logokopikao.png differ diff --git a/app/src/main/res/drawable/mail.png b/app/src/main/res/drawable/mail.png new file mode 100644 index 0000000..53013af Binary files /dev/null and b/app/src/main/res/drawable/mail.png differ diff --git a/app/src/main/res/drawable/mata.png b/app/src/main/res/drawable/mata.png new file mode 100644 index 0000000..052a718 Binary files /dev/null and b/app/src/main/res/drawable/mata.png differ diff --git a/app/src/main/res/drawable/nama.png b/app/src/main/res/drawable/nama.png new file mode 100644 index 0000000..de1ab4e Binary files /dev/null and b/app/src/main/res/drawable/nama.png differ diff --git a/app/src/main/res/drawable/notification.png b/app/src/main/res/drawable/notification.png new file mode 100644 index 0000000..ca8db83 Binary files /dev/null and b/app/src/main/res/drawable/notification.png differ diff --git a/app/src/main/res/drawable/password.png b/app/src/main/res/drawable/password.png new file mode 100644 index 0000000..ec69243 Binary files /dev/null and b/app/src/main/res/drawable/password.png differ diff --git a/app/src/main/res/drawable/pensil.png b/app/src/main/res/drawable/pensil.png new file mode 100644 index 0000000..173f436 Binary files /dev/null and b/app/src/main/res/drawable/pensil.png differ diff --git a/app/src/main/res/drawable/right.png b/app/src/main/res/drawable/right.png new file mode 100644 index 0000000..852d630 Binary files /dev/null and b/app/src/main/res/drawable/right.png differ diff --git a/app/src/main/res/drawable/sprinkler_irigation.png b/app/src/main/res/drawable/sprinkler_irigation.png new file mode 100644 index 0000000..57114c0 Binary files /dev/null and b/app/src/main/res/drawable/sprinkler_irigation.png differ diff --git a/app/src/main/res/drawable/tambah.png b/app/src/main/res/drawable/tambah.png new file mode 100644 index 0000000..a5252f5 Binary files /dev/null and b/app/src/main/res/drawable/tambah.png differ diff --git a/app/src/main/res/drawable/thermometer.png b/app/src/main/res/drawable/thermometer.png new file mode 100644 index 0000000..c92138f Binary files /dev/null and b/app/src/main/res/drawable/thermometer.png differ diff --git a/app/src/main/res/drawable/trash.png b/app/src/main/res/drawable/trash.png new file mode 100644 index 0000000..8bb661e Binary files /dev/null and b/app/src/main/res/drawable/trash.png differ diff --git a/app/src/main/res/drawable/user.png b/app/src/main/res/drawable/user.png new file mode 100644 index 0000000..0d3bdd6 Binary files /dev/null and b/app/src/main/res/drawable/user.png differ diff --git a/app/src/main/res/drawable/warning.png b/app/src/main/res/drawable/warning.png new file mode 100644 index 0000000..aba4c27 Binary files /dev/null and b/app/src/main/res/drawable/warning.png differ diff --git a/app/src/main/res/font/inria_regular.ttf b/app/src/main/res/font/inria_regular.ttf new file mode 100644 index 0000000..387d683 Binary files /dev/null and b/app/src/main/res/font/inria_regular.ttf differ diff --git a/app/src/main/res/font/inriasans_bold.ttf b/app/src/main/res/font/inriasans_bold.ttf new file mode 100644 index 0000000..06032d5 Binary files /dev/null and b/app/src/main/res/font/inriasans_bold.ttf differ diff --git a/app/src/main/res/font/inter_medium.otf b/app/src/main/res/font/inter_medium.otf new file mode 100644 index 0000000..ca7bfcd Binary files /dev/null and b/app/src/main/res/font/inter_medium.otf differ diff --git a/app/src/main/res/font/poppins_black.ttf b/app/src/main/res/font/poppins_black.ttf new file mode 100644 index 0000000..71c0f99 Binary files /dev/null and b/app/src/main/res/font/poppins_black.ttf differ diff --git a/app/src/main/res/font/poppins_bold.ttf b/app/src/main/res/font/poppins_bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/app/src/main/res/font/poppins_bold.ttf differ diff --git a/app/src/main/res/font/poppins_medium.ttf b/app/src/main/res/font/poppins_medium.ttf new file mode 100644 index 0000000..6bcdcc2 Binary files /dev/null and b/app/src/main/res/font/poppins_medium.ttf differ diff --git a/app/src/main/res/font/poppins_regular.ttf b/app/src/main/res/font/poppins_regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/app/src/main/res/font/poppins_regular.ttf differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..4dd10b0 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..34f6fa7 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..eecbb4f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..7b6827d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f1f714e Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..69e092d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..62dc3ee Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..9260a99 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..a25ee8d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..52498e0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..0fc639f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b9fc5a6 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..63a680a Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3eceb29 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..8441332 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/raw/peringatan.mp3 b/app/src/main/res/raw/peringatan.mp3 new file mode 100644 index 0000000..f11f8d4 Binary files /dev/null and b/app/src/main/res/raw/peringatan.mp3 differ diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 0000000..b76080a --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,36 @@ + + UI tugas akhir + + Language + English + Indonesian + Account + Change + Notification + Help and Support + Rain + Guide + Contact developer + Out + Setting + Soil + First Step + Main Features + Automatic Watering + Manual Control + Notifications + History and Analytics + Watering Mode + Select A Watering Mode + Sensor Monitoring + Monitor environmental conditions in real time + Average Sensor Status + A summary of the latest status from each sensor + Watering + Watering is Performed Manually + History + Manual watering activity log + System status + Displays the status of the sensors, pump, and watering mode + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..fc98003 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,36 @@ + + Kopikao Smart System + + Bahasa + Inggris + Indonesia + Akun + Ubah + Notifikasi + Bantuan dan Dukungan + Hujan + Panduan + Hubungi Developer + Keluar + Pengaturan + Tanah + Langkah Awal + Penyiraman Otomatis + Penyiraman Manual + Fitur Utama + Notifikasi dan Alarm + Riwayat dan Statistik + Mode Penyiraman + Pilih mode penyiraman + Pemantauan sensor + Pantau kondisi lingkungan secara real-time + Kondisi Rata-rata Sensor + Ringkasan kondisi terbaru dari setiap sensor + Penyiraman + Penyiraman dilakukan secara manual + Riwayat + Catatan aktivitas penyiraman manual + Status sistem + Menampilkan status sensor, pompa, dan mode penyiraman + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..608d839 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +