package id.ac.ubharajaya.sistemakademik import android.Manifest import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.location.Location import android.os.Bundle import android.provider.MediaStore import android.util.Base64 import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import com.google.android.gms.location.LocationServices import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme import org.json.JSONObject import java.io.ByteArrayOutputStream import java.net.HttpURLConnection import java.net.URL import kotlin.concurrent.thread /* ================= KONFIGURASI LOKASI ================= */ const val KAMPUS_LAT = -6.222967558410948 const val KAMPUS_LON = 107.00931291609834 const val MAX_RADIUS = 50 // meter fun hitungJarak( lat1: Double, lon1: Double, lat2: Double, lon2: Double ): Float { val hasil = FloatArray(1) Location.distanceBetween(lat1, lon1, lat2, lon2, hasil) return hasil[0] } /* ================= UTIL ================= */ fun bitmapToBase64(bitmap: Bitmap): String { val outputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) } fun kirimKeN8n( context: ComponentActivity, npm: String, nama: String, mataKuliah: String, latitude: Double, longitude: Double, foto: Bitmap, status: String ) { thread { try { val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254") val conn = url.openConnection() as HttpURLConnection conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") conn.doOutput = true val json = JSONObject().apply { put("npm", npm) put("nama", nama) put("mata_kuliah", mataKuliah) put("latitude", latitude) put("longitude", longitude) put("timestamp", System.currentTimeMillis()) put("status", status) put("foto_base64", bitmapToBase64(foto)) } conn.outputStream.use { it.write(json.toString().toByteArray()) } val responseCode = conn.responseCode context.runOnUiThread { Toast.makeText( context, if (responseCode == 200) "Absensi $status" else "Server menolak absensi", Toast.LENGTH_SHORT ).show() } conn.disconnect() } catch (_: Exception) { context.runOnUiThread { Toast.makeText( context, "Gagal kirim ke server", Toast.LENGTH_SHORT ).show() } } } } /* ================= ACTIVITY ================= */ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { SistemAkademikTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> AbsensiScreen( modifier = Modifier.padding(innerPadding), activity = this ) } } } } } /* ================= UI ================= */ @Composable fun AbsensiScreen( modifier: Modifier = Modifier, activity: ComponentActivity ) { val context = LocalContext.current var npm by remember { mutableStateOf("") } var nama by remember { mutableStateOf("") } var mataKuliah by remember { mutableStateOf("") } var lokasi by remember { mutableStateOf("Koordinat: -") } var latitude by remember { mutableStateOf(null) } var longitude by remember { mutableStateOf(null) } var foto by remember { mutableStateOf(null) } val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) /* ===== Permission Lokasi ===== */ val locationPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { if ( ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { fusedLocationClient.lastLocation .addOnSuccessListener { location -> if (location != null) { latitude = location.latitude longitude = location.longitude lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}" } else { lokasi = "Lokasi tidak tersedia" } } } } else { Toast.makeText( context, "Izin lokasi ditolak", Toast.LENGTH_SHORT ).show() } } /* ===== Kamera ===== */ val cameraLauncher = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == Activity.RESULT_OK) { val bitmap = result.data?.extras?.get("data") as? Bitmap if (bitmap != null) { foto = bitmap Toast.makeText( context, "Foto berhasil diambil", Toast.LENGTH_SHORT ).show() } else { Toast.makeText( context, "Gagal mengambil foto", Toast.LENGTH_SHORT ).show() } } } val cameraPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) { val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) cameraLauncher.launch(intent) } else { Toast.makeText( context, "Izin kamera ditolak", Toast.LENGTH_SHORT ).show() } } LaunchedEffect(Unit) { locationPermissionLauncher.launch( Manifest.permission.ACCESS_FINE_LOCATION ) } /* ===== UI ===== */ Column( modifier = modifier .fillMaxSize() .padding(24.dp), verticalArrangement = Arrangement.Center ) { Text( text = "Absensi Akademik", style = MaterialTheme.typography.titleLarge ) Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( value = npm, onValueChange = { npm = it }, label = { Text("NPM") }, modifier = Modifier.fillMaxWidth() ) OutlinedTextField( value = nama, onValueChange = { nama = it }, label = { Text("Nama") }, modifier = Modifier.fillMaxWidth() ) OutlinedTextField( value = mataKuliah, onValueChange = { mataKuliah = it }, label = { Text("Mata Kuliah") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(12.dp)) Text(text = lokasi) Spacer(modifier = Modifier.height(12.dp)) Button( onClick = { cameraPermissionLauncher.launch( Manifest.permission.CAMERA ) }, modifier = Modifier.fillMaxWidth() ) { Text("Ambil Foto") } /* ===== PREVIEW FOTO ===== */ if (foto != null) { Spacer(modifier = Modifier.height(12.dp)) Image( bitmap = foto!!.asImageBitmap(), contentDescription = "Preview Foto Absensi", modifier = Modifier .fillMaxWidth() .height(220.dp), contentScale = ContentScale.Crop ) } Spacer(modifier = Modifier.height(12.dp)) Button( onClick = { if ( npm.isNotEmpty() && nama.isNotEmpty() && mataKuliah.isNotEmpty() && latitude != null && longitude != null && foto != null ) { val jarak = hitungJarak( latitude!!, longitude!!, KAMPUS_LAT, KAMPUS_LON ) val status = if (jarak <= MAX_RADIUS) "HADIR" else "DITOLAK" if (status == "HADIR") { kirimKeN8n( activity, npm, nama, mataKuliah, latitude!!, longitude!!, foto!!, status ) } else { Toast.makeText( context, "Absensi ditolak (di luar area)", Toast.LENGTH_LONG ).show() } } else { Toast.makeText( context, "Data absensi belum lengkap", Toast.LENGTH_SHORT ).show() } }, modifier = Modifier.fillMaxWidth() ) { Text("Kirim Absensi") } } }