From 741a6a8fd856f89cb17bb60c3cb7d5d94287c0dc Mon Sep 17 00:00:00 2001
From: "202310715060@mhs.ubharajaya.ac.id" <202310715060@mhs.ubharajaya.ac.id>
Date: Tue, 13 Jan 2026 22:10:54 +0700
Subject: [PATCH] EAS-202310715060-MuhammadYusronAmrullah
---
.idea/appInsightsSettings.xml | 26 +
.idea/deploymentTargetSelector.xml | 4 +-
.idea/deviceManager.xml | 21 +
app/build.gradle.kts | 14 +-
app/src/main/AndroidManifest.xml | 5 +-
.../ubharajaya/sistemakademik/MainActivity.kt | 758 +++++++++++++-----
gradle/libs.versions.toml | 8 +
7 files changed, 614 insertions(+), 222 deletions(-)
create mode 100644 .idea/appInsightsSettings.xml
create mode 100644 .idea/deviceManager.xml
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/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b77ef91..1a524ad 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,10 +4,10 @@
-
+
-
+
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..64f56bf
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7d76378..dfe5ec8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,9 +6,7 @@ plugins {
android {
namespace = "id.ac.ubharajaya.sistemakademik"
- compileSdk {
- version = release(36)
- }
+ compileSdk = 36
defaultConfig {
applicationId = "id.ac.ubharajaya.sistemakademik"
@@ -44,15 +42,19 @@ android {
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
- implementation(libs.androidx.activity.compose)
- implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
+
+ // Activity, Navigation, and Icons
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.compose.material.icons.extended)
+
// Location (GPS)
- implementation("com.google.android.gms:play-services-location:21.0.1")
+ implementation(libs.play.services.location)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4619836..e116c31 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,10 @@
-
+
+
+
+
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..521791b 100644
--- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
@@ -14,261 +14,593 @@ 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.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.core.content.ContextCompat
+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 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
-/* ================= UTIL ================= */
+/* ================= DATA CLASS ================= */
+data class AttendanceRecord(val timestamp: Long, val status: String)
+
+/* ================= UTILS ================= */
fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
- bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream)
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
}
-fun kirimKeN8n(
- context: ComponentActivity,
- latitude: Double,
- longitude: Double,
- foto: Bitmap
-) {
+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) {
thread {
+ var isSuccess = false
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("npm", npm)
+ put("nama", nama)
+ put("mata_kuliah", matakuliah) // FIX: Changed from "matakuliah" to "mata_kuliah"
+ put("latitude", lat)
+ put("longitude", lon)
put("timestamp", System.currentTimeMillis())
- put("foto_base64", bitmapToBase64(foto))
+ put("foto_base64", bitmapToBase64(img))
}
- conn.outputStream.use {
- it.write(json.toString().toByteArray())
+ 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)
}
-
- val responseCode = conn.responseCode
-
- context.runOnUiThread {
- Toast.makeText(
- context,
- if (responseCode == 200)
- "Absensi diterima server"
- else
- "Absensi ditolak server",
- 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, "Error: Gagal terhubung ke server.", Toast.LENGTH_SHORT).show()
+ onFinish(false)
}
}
}
}
-/* ================= ACTIVITY ================= */
+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"
-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
+ 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")
+ )
)
}
+ activity.runOnUiThread { onResult(records) }
+ } else {
+ activity.runOnUiThread { onResult(emptyList()) }
}
- }
- }
-}
-
-/* ================= UI ================= */
-
-@Composable
-fun AbsensiScreen(
- modifier: Modifier = Modifier,
- 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 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"
- }
- }
- .addOnFailureListener {
- lokasi = "Gagal mengambil lokasi"
- }
- }
-
- } 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?.getParcelable("data", Bitmap::class.java)
- if (bitmap != null) {
- foto = bitmap
- Toast.makeText(
- context,
- "Foto berhasil diambil",
- 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()
- }
- }
-
- /* ===== Request Awal ===== */
-
- 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))
-
- Text(text = lokasi)
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Button(
- onClick = {
- cameraPermissionLauncher.launch(
- Manifest.permission.CAMERA
- )
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text("Ambil Foto")
- }
-
- Spacer(modifier = Modifier.height(12.dp))
-
- 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()
- }
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text("Kirim Absensi")
+ } catch (e: Exception) {
+ activity.runOnUiThread { onResult(emptyList()) }
}
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7d255c8..f002c13 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,10 @@ espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2024.09.00"
+activityComposeVersion = "1.12.2"
+playServicesLocation = "21.3.0"
+navigationCompose = "2.9.6"
+materialIconsExtended = "1.6.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -24,6 +28,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityComposeVersion" }
+play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "materialIconsExtended" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }