From 8326d5097931e460e96b1dc9c16bd59d352d15b3 Mon Sep 17 00:00:00 2001 From: "202310715060@mhs.ubharajaya.ac.id" <202310715060@mhs.ubharajaya.ac.id> Date: Wed, 14 Jan 2026 11:18:51 +0700 Subject: [PATCH] EAS-202310715060-MuhammadYusronAmrullah --- .../ubharajaya/sistemakademik/MainActivity.kt | 839 +++++++----------- 1 file changed, 300 insertions(+), 539 deletions(-) diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt index 521791b..ad91cb6 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -5,6 +5,7 @@ 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 @@ -15,534 +16,62 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image -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.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* 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.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.app.ActivityCompat -import androidx.navigation.NavController -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.core.content.ContextCompat import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.Priority import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme -import org.json.JSONArray import org.json.JSONObject import java.io.ByteArrayOutputStream import java.net.HttpURLConnection import java.net.URL -import java.text.SimpleDateFormat -import java.util.* import kotlin.concurrent.thread -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt -/* ================= DATA CLASS ================= */ -data class AttendanceRecord(val timestamp: Long, val status: String) +/* ================= KONFIGURASI LOKASI ================= */ -/* ================= UTILS ================= */ +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, 70, outputStream) + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) } -object LocationUtils { - private const val KAMPUS_LATITUDE = -6.2576 - private const val KAMPUS_LONGITUDE = 106.9746 - private const val MAX_RADIUS_METERS = 100.0 // Radius 100 meter - - fun isWithinCampusRadius(lat: Double, lon: Double): Boolean { - val earthRadius = 6371000.0 // in meters - val dLat = Math.toRadians(lat - KAMPUS_LATITUDE) - val dLon = Math.toRadians(lon - KAMPUS_LONGITUDE) - val lat1 = Math.toRadians(KAMPUS_LATITUDE) - val lat2 = Math.toRadians(lat) - - val a = sin(dLat / 2) * sin(dLat / 2) + - sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos(lat2) - val c = 2 * atan2(sqrt(a), sqrt(1 - a)) - - return (earthRadius * c) <= MAX_RADIUS_METERS - } -} - - -/* ================= MAIN ACTIVITY ================= */ - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - SistemAkademikTheme { - AppNavigation(activity = this) - } - } - } -} - -/* ================= NAVIGATION ================= */ - -@Composable -fun AppNavigation(activity: ComponentActivity) { - val navController = rememberNavController() - - NavHost(navController = navController, startDestination = "login") { - composable("login") { - LoginScreen(navController = navController) - } - - composable( - route = "menu/{npm}/{nama}", - arguments = listOf( - navArgument("npm") { type = NavType.StringType }, - navArgument("nama") { type = NavType.StringType } - ) - ) { backStackEntry -> - val npm = backStackEntry.arguments?.getString("npm") ?: "N/A" - val nama = backStackEntry.arguments?.getString("nama") ?: "N/A" - MenuScreen(navController = navController, npm = npm, nama = nama) - } - - composable( - route = "absensi/{npm}/{nama}", - arguments = listOf( - navArgument("npm") { type = NavType.StringType }, - navArgument("nama") { type = NavType.StringType } - ) - ) { backStackEntry -> - val npm = backStackEntry.arguments?.getString("npm") ?: "N/A" - val nama = backStackEntry.arguments?.getString("nama") ?: "N/A" - AbsensiScreen( - activity = activity, - navController = navController, - npm = npm, - nama = nama - ) - } - - composable( - route = "history/{npm}", - arguments = listOf(navArgument("npm") { type = NavType.StringType }) - ) { backStackEntry -> - val npm = backStackEntry.arguments?.getString("npm") ?: "N/A" - HistoryScreen(navController = navController, npm = npm, activity = activity) - } - } -} - -/* ================= UI SCREENS ================= */ - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LoginScreen(navController: NavController) { - var npm by remember { mutableStateOf("") } - var nama by remember { mutableStateOf("") } - - Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { Text("Login Absensi", color = Color.White, fontSize = 18.sp) }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF2E7D32)) - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .background(Color(0xFFF8FBF8)) - .padding(horizontal = 32.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon(Icons.Default.Person, contentDescription = null, modifier = Modifier.size(100.dp), tint = Color(0xFF2E7D32)) - Spacer(modifier = Modifier.height(40.dp)) - - OutlinedTextField( - value = npm, - onValueChange = { npm = it }, - label = { Text("NPM") }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedTextField( - value = nama, - onValueChange = { nama = it }, - label = { Text("Nama Lengkap") }, - singleLine = true, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(24.dp)) - - Button( - onClick = { - if (npm.isNotBlank() && nama.isNotBlank()) { - navController.navigate("menu/$npm/$nama") - } - }, - modifier = Modifier.fillMaxWidth().height(50.dp), - enabled = npm.isNotBlank() && nama.isNotBlank(), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B5E20)), - shape = RoundedCornerShape(10.dp) - ) { - Text("LOGIN", fontWeight = FontWeight.Bold, color = Color.White) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MenuScreen(navController: NavController, npm: String, nama: String) { - Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { Text("Menu Utama", color = Color.White, fontSize = 18.sp) }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF2E7D32)), - navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Default.ArrowBack, contentDescription = "Logout", tint = Color.White) - } - } - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .background(Color(0xFFF8FBF8)) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Selamat Datang,", style = MaterialTheme.typography.bodyLarge) - Text("$nama ($npm)", fontWeight = FontWeight.Bold, fontSize = 20.sp) - Spacer(modifier = Modifier.height(32.dp)) - - // Tombol Absensi - Button( - onClick = { navController.navigate("absensi/$npm/$nama") }, - modifier = Modifier - .fillMaxWidth() - .height(60.dp), - shape = RoundedCornerShape(12.dp) - ) { - Icon(Icons.Filled.CameraAlt, contentDescription = null, modifier = Modifier.size(24.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text("Absen Kehadiran", fontSize = 16.sp) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Tombol Riwayat - Button( - onClick = { navController.navigate("history/$npm") }, - modifier = Modifier - .fillMaxWidth() - .height(60.dp), - shape = RoundedCornerShape(12.dp) - ) { - Icon(Icons.Default.History, contentDescription = null, modifier = Modifier.size(24.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text("Riwayat Absensi", fontSize = 16.sp) - } - } - } -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AbsensiScreen(activity: ComponentActivity, navController: NavController, npm: String, nama: String) { - val context = LocalContext.current - - var lokasiStatus by remember { mutableStateOf("Mengecek Lokasi...") } - var locationAvailable by remember { mutableStateOf(false) } - var foto by remember { mutableStateOf(null) } - var isUploading by remember { mutableStateOf(false) } - var latitude by remember { mutableStateOf(null) } - var longitude by remember { mutableStateOf(null) } - var matakuliah by remember { mutableStateOf("") } - - val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } - - 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 - } - } - - val requestPermissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true) { - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null) - .addOnSuccessListener { loc -> - if (loc != null) { - latitude = loc.latitude - longitude = loc.longitude - locationAvailable = true - lokasiStatus = if (LocationUtils.isWithinCampusRadius(loc.latitude, loc.longitude)) { - "Di Dalam Area Kampus" - } else { - "Di Luar Area Kampus" - } - } else { - lokasiStatus = "Gagal Mendapatkan Lokasi" - locationAvailable = false - } - }.addOnFailureListener { - lokasiStatus = "Gagal Mendapatkan Lokasi" - locationAvailable = false - } - } - } else { - lokasiStatus = "Izin lokasi ditolak." - locationAvailable = false - } - } - - LaunchedEffect(Unit) { - requestPermissionLauncher.launch( - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA) - ) - } - - Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { Text("Absen Kehadiran", color = Color.White, fontSize = 18.sp) }, - navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Default.ArrowBack, contentDescription = "Kembali", tint = Color.White) - } - }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF2E7D32)) - ) - } - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .background(Color(0xFFF8FBF8)) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(16.dp)) - Text("Selamat Datang,", style = MaterialTheme.typography.bodyLarge) - Text("$nama ($npm)", fontWeight = FontWeight.Bold, fontSize = 18.sp) - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedTextField( - value = matakuliah, - onValueChange = { matakuliah = it }, - label = { Text("Mata Kuliah") }, - singleLine = true, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - - - Row( - modifier = Modifier.padding(top = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Status Lokasi: $lokasiStatus ", - style = MaterialTheme.typography.bodyMedium, - color = if (locationAvailable) Color(0xFF2E7D32) else Color.Red - ) - if (locationAvailable) { - Icon(Icons.Default.CheckCircle, "OK", tint = Color(0xFF2E7D32), modifier = Modifier.size(16.dp)) - } else { - Icon(Icons.Default.Close, "Error", tint = Color.Red, modifier = Modifier.size(16.dp)) - } - } - - Spacer(modifier = Modifier.height(20.dp)) - Text("Ambil Foto Selfie", fontWeight = FontWeight.SemiBold, fontSize = 16.sp) - Spacer(modifier = Modifier.height(16.dp)) - - Box( - modifier = Modifier - .size(220.dp) - .clip(CircleShape) - .background(Color(0xFFE0E0E0)), - contentAlignment = Alignment.Center - ) { - if (foto != null) { - Image( - bitmap = foto!!.asImageBitmap(), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } else { - Icon(Icons.Default.Person, null, modifier = Modifier.size(80.dp), tint = Color.White) - } - } - - Spacer(modifier = Modifier.height(40.dp)) - - Button( - onClick = { - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - cameraLauncher.launch(intent) - }, - modifier = Modifier.fillMaxWidth(0.7f).height(50.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50)), - shape = RoundedCornerShape(10.dp) - ) { - Text("AMBIL FOTO", fontWeight = FontWeight.Bold, color = Color.White) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { - isUploading = true - if (latitude != null && longitude != null && foto != null) { - kirimKeServer(activity, npm, nama, matakuliah, latitude!!, longitude!!, foto!!) { - isUploading = false - if (it) navController.popBackStack() - } - } else { - isUploading = false - Toast.makeText(context, "Lokasi atau foto belum siap.", Toast.LENGTH_LONG).show() - } - }, - modifier = Modifier.fillMaxWidth(0.7f).height(50.dp), - enabled = foto != null && locationAvailable && matakuliah.isNotBlank() && !isUploading, - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B5E20)), - shape = RoundedCornerShape(10.dp) - ) { - Text("KIRIM ABSENSI", fontWeight = FontWeight.Bold) - } - - - if (isUploading) { - Spacer(modifier = Modifier.height(16.dp)) - CircularProgressIndicator(color = Color(0xFF2E7D32)) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun HistoryScreen(navController: NavController, npm: String, activity: Activity) { - var historyList by remember { mutableStateOf>(emptyList()) } - var isLoading by remember { mutableStateOf(true) } - - LaunchedEffect(npm) { - fetchHistoryFromServer(npm, activity) { - historyList = it - isLoading = false - } - } - - Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { Text("Riwayat Absensi", color = Color.White) }, - navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(Icons.Default.ArrowBack, "Kembali", tint = Color.White) - } - }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors(Color(0xFF2E7D32)) - ) - } - ) { innerPadding -> - if (isLoading) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } else if (historyList.isEmpty()) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Tidak ada riwayat absensi.") - } - } else { - LazyColumn( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .padding(16.dp) - ) { - items(historyList) { record -> - val date = SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale.getDefault()).format(Date(record.timestamp)) - Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - elevation = CardDefaults.cardElevation(2.dp) - ) { - Row( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(text = date, fontWeight = FontWeight.Medium) - Text( - text = record.status, - color = if (record.status == "Sukses") Color(0xFF2E7D32) else Color.Red, - fontWeight = FontWeight.Bold - ) - } - } - } - } - } - } -} - - -/* ================= NETWORK LOGIC ================= */ - -fun kirimKeServer(activity: Activity, npm: String, nama: String, matakuliah: String, lat: Double, lon: Double, img: Bitmap, onFinish: (Boolean) -> Unit) { +fun kirimKeN8n( + context: ComponentActivity, + npm: String, + nama: String, + mataKuliah: String, + latitude: Double, + longitude: Double, + foto: Bitmap, + status: String +) { thread { - var isSuccess = false 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 @@ -550,57 +79,289 @@ fun kirimKeServer(activity: Activity, npm: String, nama: String, matakuliah: Str val json = JSONObject().apply { put("npm", npm) put("nama", nama) - put("mata_kuliah", matakuliah) // FIX: Changed from "matakuliah" to "mata_kuliah" - put("latitude", lat) - put("longitude", lon) + put("mata_kuliah", mataKuliah) + put("latitude", latitude) + put("longitude", longitude) put("timestamp", System.currentTimeMillis()) - put("foto_base64", bitmapToBase64(img)) + put("status", status) + put("foto_base64", bitmapToBase64(foto)) } - conn.outputStream.write(json.toString().toByteArray()) - - val code = conn.responseCode - isSuccess = code == 200 - activity.runOnUiThread { - Toast.makeText(activity, if(isSuccess) "Absensi Sukses!" else "Absensi Gagal: Server Error $code", Toast.LENGTH_LONG).show() - onFinish(isSuccess) + conn.outputStream.use { + it.write(json.toString().toByteArray()) } - } catch (e: Exception) { - activity.runOnUiThread { - Toast.makeText(activity, "Error: Gagal terhubung ke server.", Toast.LENGTH_SHORT).show() - onFinish(false) + + 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() } } } } -fun fetchHistoryFromServer(npm: String, activity: Activity, onResult: (List) -> Unit) { - thread { - try { - // PENTING: URL ini hanya contoh. Ganti dengan URL API Anda untuk mengambil riwayat. - val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/riwayat-absensi?npm=$npm") - val conn = url.openConnection() as HttpURLConnection - conn.requestMethod = "GET" +/* ================= ACTIVITY ================= */ - if (conn.responseCode == 200) { - val response = conn.inputStream.bufferedReader().use { it.readText() } - val jsonArray = JSONArray(response) - val records = mutableListOf() - for (i in 0 until jsonArray.length()) { - val jsonObject = jsonArray.getJSONObject(i) - records.add( - AttendanceRecord( - timestamp = jsonObject.getLong("timestamp"), - status = jsonObject.getString("status") - ) +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 ) } - activity.runOnUiThread { onResult(records) } - } else { - activity.runOnUiThread { onResult(emptyList()) } } - } catch (e: Exception) { - activity.runOnUiThread { onResult(emptyList()) } + } + } +} + +/* ================= 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") } } }