diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ 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..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
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 c774502..2227bcc 100644
--- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
@@ -3,7 +3,6 @@ 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.os.Bundle
import android.provider.MediaStore
@@ -14,10 +13,16 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
+import android.annotation.SuppressLint
+import androidx.compose.foundation.Image
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.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -28,66 +33,74 @@ import java.io.ByteArrayOutputStream
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
+import android.content.pm.PackageManager
-/* ================= UTIL ================= */
+
+/* ================= UTIL (TIDAK DIUBAH) ================= */
fun bitmapToBase64(bitmap: Bitmap): String {
- val outputStream = ByteArrayOutputStream()
- bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
- return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
+ val out = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)
+ return Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
+}
+
+fun hitungJarak(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
+ val R = 6371000.0
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLon = Math.toRadians(lon2 - lon1)
+ val a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(Math.toRadians(lat1)) *
+ Math.cos(Math.toRadians(lat2)) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2)
+ val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
+ return R * c
}
fun kirimKeN8n(
- context: ComponentActivity,
- latitude: Double,
- longitude: Double,
- foto: Bitmap
+ activity: ComponentActivity,
+ npm: String,
+ nama: String,
+ matkul: String,
+ lat: Double,
+ lon: Double,
+ foto: Bitmap,
+ status: String
) {
thread {
try {
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
-// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/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", "12345")
- put("nama","Arif R D")
- put("latitude", latitude)
- put("longitude", longitude)
put("timestamp", System.currentTimeMillis())
+ put("ip_addr", "android")
+ put("npm", npm)
+ put("nama", nama)
+ put("latitude", lat)
+ put("longitude", lon)
+ put("mata_kuliah", matkul)
+ put("status", status)
put("foto_base64", bitmapToBase64(foto))
}
- conn.outputStream.use {
- it.write(json.toString().toByteArray())
- }
+ conn.outputStream.use { it.write(json.toString().toByteArray()) }
val responseCode = conn.responseCode
-
- context.runOnUiThread {
+ activity.runOnUiThread {
Toast.makeText(
- context,
- if (responseCode == 200)
- "Absensi diterima server"
- else
- "Absensi ditolak server",
+ activity,
+ if (responseCode == 200) "Absensi berhasil" else "Absensi ditolak",
Toast.LENGTH_SHORT
).show()
}
-
conn.disconnect()
-
- } catch (_: Exception) {
- context.runOnUiThread {
- Toast.makeText(
- context,
- "Gagal kirim ke server",
- Toast.LENGTH_SHORT
- ).show()
+ } catch (e: Exception) {
+ activity.runOnUiThread {
+ Toast.makeText(activity, "Gagal kirim absensi", Toast.LENGTH_SHORT).show()
}
}
}
@@ -96,19 +109,12 @@ fun kirimKeN8n(
/* ================= 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
- )
- }
+ AbsensiScreen(this)
}
}
}
@@ -117,158 +123,141 @@ class MainActivity : ComponentActivity() {
/* ================= UI ================= */
@Composable
-fun AbsensiScreen(
- modifier: Modifier = Modifier,
- activity: ComponentActivity
-) {
+fun AbsensiScreen(activity: ComponentActivity) {
val context = LocalContext.current
- var lokasi by remember { mutableStateOf("Koordinat: -") }
- var latitude by remember { mutableStateOf(null) }
- var longitude by remember { mutableStateOf(null) }
+ var npm by remember { mutableStateOf("") }
+ var nama by remember { mutableStateOf("") }
+ var matkul by remember { mutableStateOf("") }
+ var lat by remember { mutableStateOf(null) }
+ var lon by remember { mutableStateOf(null) }
+ var lokasi by remember { mutableStateOf("Lokasi belum didapat") }
var foto by remember { mutableStateOf(null) }
- val fusedLocationClient =
- LocationServices.getFusedLocationProviderClient(context)
+ val LAT_ABSEN = -6.255
+ val LON_ABSEN = 106.618
+ val RADIUS = 30.0 // meter (REALISTIS)
- /* ===== Permission Lokasi ===== */
+ val fusedClient = LocationServices.getFusedLocationProviderClient(context)
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"
- }
- }
- .addOnFailureListener {
- lokasi = "Gagal mengambil lokasi"
- }
- }
-
- } else {
- Toast.makeText(
+ rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it && ContextCompat.checkSelfPermission(
context,
- "Izin lokasi ditolak",
- Toast.LENGTH_SHORT
- ).show()
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ ambilLokasi(fusedClient) { la, lo ->
+ lat = la
+ lon = lo
+ lokasi = "Lat: $la | Lon: $lo"
+ }
}
}
- /* ===== Kamera ===== */
-
val cameraLauncher =
- rememberLauncherForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { result ->
- if (result.resultCode == Activity.RESULT_OK) {
- val bitmap =
- result.data?.extras?.getParcelable("data", Bitmap::class.java)
- if (bitmap != null) {
- foto = bitmap
- Toast.makeText(
- context,
- "Foto berhasil diambil",
- Toast.LENGTH_SHORT
- ).show()
- }
+ rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
+ foto = it.data?.extras?.get("data") as? Bitmap
}
}
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()
- }
+ rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
}
- /* ===== Request Awal ===== */
-
LaunchedEffect(Unit) {
- locationPermissionLauncher.launch(
- Manifest.permission.ACCESS_FINE_LOCATION
- )
+ 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))
-
- Text(text = lokasi)
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Button(
- onClick = {
- cameraPermissionLauncher.launch(
- Manifest.permission.CAMERA
- )
- },
- modifier = Modifier.fillMaxWidth()
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ shape = RoundedCornerShape(16.dp),
+ elevation = CardDefaults.cardElevation(8.dp)
) {
- Text("Ambil Foto")
- }
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
- Spacer(modifier = Modifier.height(12.dp))
+ Text("Absensi Akademik", style = MaterialTheme.typography.titleLarge)
- Button(
- onClick = {
- if (latitude != null && longitude != null && foto != null) {
- kirimKeN8n(
- activity,
- latitude!!,
- longitude!!,
- foto!!
- )
- } else {
- Toast.makeText(
- context,
- "Lokasi atau foto belum lengkap",
- Toast.LENGTH_SHORT
- ).show()
+ OutlinedTextField(npm, { npm = it }, label = { Text("NPM") }, modifier = Modifier.fillMaxWidth())
+ OutlinedTextField(nama, { nama = it }, label = { Text("Nama") }, modifier = Modifier.fillMaxWidth())
+ OutlinedTextField(matkul, { matkul = it }, label = { Text("Mata Kuliah") }, modifier = Modifier.fillMaxWidth())
+
+ Text(lokasi, style = MaterialTheme.typography.bodySmall)
+
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) }
+ ) {
+ Text("Ambil Foto")
}
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text("Kirim Absensi")
+
+ foto?.let {
+ Image(
+ bitmap = it.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = {
+ if (npm.isBlank() || nama.isBlank() || matkul.isBlank()
+ || lat == null || lon == null || foto == null
+ ) {
+ Toast.makeText(context, "Data belum lengkap", Toast.LENGTH_SHORT).show()
+ return@Button
+ }
+
+ val jarak = hitungJarak(lat!!, lon!!, LAT_ABSEN, LON_ABSEN)
+
+ // 🔴 BLOK TOTAL JIKA DI LUAR RADIUS
+ if (jarak > RADIUS) {
+ Toast.makeText(
+ context,
+ "Anda berada di luar radius absensi",
+ Toast.LENGTH_LONG
+ ).show()
+ return@Button
+ }
+
+ // ✅ HANYA JIKA VALID BARU KIRIM
+ kirimKeN8n(
+ activity,
+ npm,
+ nama,
+ matkul,
+ lat!!,
+ lon!!,
+ foto!!,
+ "VALID"
+ )
+ }
+ ) {
+ Text("Kirim Absensi")
+ }
+ }
}
}
}
+
+@SuppressLint("MissingPermission")
+fun ambilLokasi(
+ client: com.google.android.gms.location.FusedLocationProviderClient,
+ onResult: (Double, Double) -> Unit
+) {
+ client.lastLocation.addOnSuccessListener {
+ if (it != null) onResult(it.latitude, it.longitude)
+ }
+}