From f4788298db93da55a32a547d1d5a89b195f74844 Mon Sep 17 00:00:00 2001
From: "MUHAMMAD.RAFI" <202310715191@mhs.ubharajaya.ac.id>
Date: Wed, 14 Jan 2026 11:04:07 +0700
Subject: [PATCH] EAS-Muhammad-Rafi
---
.idea/deviceManager.xml | 13 ++
.../ubharajaya/sistemakademik/MainActivity.kt | 140 +++++++++++++++---
2 files changed, 129 insertions(+), 24 deletions(-)
create mode 100644 .idea/deviceManager.xml
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/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
index c774502..3429401 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
@@ -14,10 +15,13 @@ 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
@@ -29,6 +33,21 @@ 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 {
@@ -39,14 +58,17 @@ fun bitmapToBase64(bitmap: Bitmap): String {
fun kirimKeN8n(
context: ComponentActivity,
+ npm: String,
+ nama: String,
+ mataKuliah: String,
latitude: Double,
longitude: Double,
- foto: Bitmap
+ 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"
@@ -54,11 +76,13 @@ fun kirimKeN8n(
conn.doOutput = true
val json = JSONObject().apply {
- put("npm", "12345")
- put("nama","Arif R D")
+ 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))
}
@@ -72,9 +96,9 @@ fun kirimKeN8n(
Toast.makeText(
context,
if (responseCode == 200)
- "Absensi diterima server"
+ "Absensi $status"
else
- "Absensi ditolak server",
+ "Server menolak absensi",
Toast.LENGTH_SHORT
).show()
}
@@ -123,6 +147,10 @@ fun AbsensiScreen(
) {
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) }
@@ -138,14 +166,12 @@ fun AbsensiScreen(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
-
if (
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
-
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
@@ -157,11 +183,7 @@ fun AbsensiScreen(
lokasi = "Lokasi tidak tersedia"
}
}
- .addOnFailureListener {
- lokasi = "Gagal mengambil lokasi"
- }
}
-
} else {
Toast.makeText(
context,
@@ -178,8 +200,7 @@ fun AbsensiScreen(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
- val bitmap =
- result.data?.extras?.getParcelable("data", Bitmap::class.java)
+ val bitmap = result.data?.extras?.get("data") as? Bitmap
if (bitmap != null) {
foto = bitmap
Toast.makeText(
@@ -187,6 +208,12 @@ fun AbsensiScreen(
"Foto berhasil diambil",
Toast.LENGTH_SHORT
).show()
+ } else {
+ Toast.makeText(
+ context,
+ "Gagal mengambil foto",
+ Toast.LENGTH_SHORT
+ ).show()
}
}
}
@@ -196,8 +223,7 @@ fun AbsensiScreen(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
- val intent =
- Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraLauncher.launch(intent)
} else {
Toast.makeText(
@@ -208,8 +234,6 @@ fun AbsensiScreen(
}
}
- /* ===== Request Awal ===== */
-
LaunchedEffect(Unit) {
locationPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
@@ -232,9 +256,32 @@ fun AbsensiScreen(
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(16.dp))
+ Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
@@ -247,21 +294,66 @@ fun AbsensiScreen(
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 (latitude != null && longitude != null && foto != null) {
- kirimKeN8n(
- activity,
+ if (
+ npm.isNotEmpty() &&
+ nama.isNotEmpty() &&
+ mataKuliah.isNotEmpty() &&
+ latitude != null &&
+ longitude != null &&
+ foto != null
+ ) {
+
+ val jarak = hitungJarak(
latitude!!,
longitude!!,
- foto!!
+ 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,
- "Lokasi atau foto belum lengkap",
+ "Data absensi belum lengkap",
Toast.LENGTH_SHORT
).show()
}