first commit
This commit is contained in:
parent
926d3e0a14
commit
21cb4efc2a
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -42,6 +42,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@ -51,6 +52,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation("androidx.compose.material:material-icons-extended:1.6.7")
|
||||||
// Location (GPS)
|
// Location (GPS)
|
||||||
implementation("com.google.android.gms:play-services-location:21.0.1")
|
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.location.Location
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
@ -14,12 +15,28 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.layout.*
|
||||||
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
@ -27,248 +44,328 @@ import org.json.JSONObject
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/* ================= UTIL ================= */
|
// =============== MAIN ACTIVITY & NAVIGATION ===============
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
fun bitmapToBase64(bitmap: Bitmap): String {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val outputStream = ByteArrayOutputStream()
|
super.onCreate(savedInstanceState)
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
enableEdgeToEdge()
|
||||||
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
setContent {
|
||||||
|
SistemAkademikTheme {
|
||||||
|
AppNavigation(activity = this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun kirimKeN8n(
|
@Composable
|
||||||
context: ComponentActivity,
|
fun AppNavigation(activity: ComponentActivity) {
|
||||||
latitude: Double,
|
var currentScreen by remember { mutableStateOf("login") }
|
||||||
longitude: Double,
|
var npm by remember { mutableStateOf("") }
|
||||||
foto: Bitmap
|
var nama by remember { mutableStateOf("") }
|
||||||
) {
|
var loginTime by remember { mutableStateOf(0L) }
|
||||||
|
|
||||||
|
when (currentScreen) {
|
||||||
|
"login" -> LoginScreen(onLoginSuccess = { loggedInNpm, loggedInNama, time ->
|
||||||
|
npm = loggedInNpm
|
||||||
|
nama = loggedInNama
|
||||||
|
loginTime = time
|
||||||
|
currentScreen = "menu"
|
||||||
|
})
|
||||||
|
"menu" -> MenuScreen(
|
||||||
|
nama = nama,
|
||||||
|
npm = npm,
|
||||||
|
onAbsenClick = { currentScreen = "absensi" },
|
||||||
|
onRiwayatClick = { currentScreen = "riwayat" },
|
||||||
|
onLogout = {
|
||||||
|
currentScreen = "login"
|
||||||
|
npm = ""
|
||||||
|
nama = ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
"absensi" -> AbsensiScreen(activity = activity, npm = npm, nama = nama, loginTime = loginTime, onNavigateBack = {
|
||||||
|
currentScreen = "menu"
|
||||||
|
})
|
||||||
|
"riwayat" -> RiwayatScreen(onNavigateBack = { currentScreen = "menu" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== LOGIN SCREEN ===============
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(onLoginSuccess: (npm: String, nama: String, loginTime: Long) -> Unit) {
|
||||||
|
var npm by remember { mutableStateOf("") }
|
||||||
|
var nama by remember { mutableStateOf("") }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(32.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("Login Absensi", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(value = npm, onValueChange = { npm = it }, label = { Text("NPM") }, modifier = Modifier.fillMaxWidth(), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number))
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(value = nama, onValueChange = { nama = it }, label = { Text("Nama Lengkap") }, modifier = Modifier.fillMaxWidth(), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text))
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (npm.isNotBlank() && nama.isNotBlank()) {
|
||||||
|
onLoginSuccess(npm, nama, System.currentTimeMillis())
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "NPM dan Nama tidak boleh kosong", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth().height(50.dp)
|
||||||
|
) {
|
||||||
|
Text("LOGIN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== MENU SCREEN ===============
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MenuScreen(nama: String, npm: String, onAbsenClick: () -> Unit, onRiwayatClick: () -> Unit, onLogout: () -> Unit) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Menu Utama", color = Color.White) },
|
||||||
|
navigationIcon = { IconButton(onClick = onLogout) { Icon(Icons.Default.ArrowBack, "Logout", tint = Color.White) } },
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF388E3C))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(innerPadding).fillMaxSize().padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Card(modifier = Modifier.fillMaxWidth(), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("Selamat Datang,", style = MaterialTheme.typography.bodyLarge, color = Color.Gray)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(nama, fontWeight = FontWeight.Bold, fontSize = 22.sp)
|
||||||
|
Text("($npm)", fontSize = 16.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
MenuButton(text = "Absen Kehadiran", icon = Icons.Filled.CameraAlt, onClick = onAbsenClick)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
MenuButton(text = "Riwayat Absensi", icon = Icons.Filled.History, onClick = onRiwayatClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MenuButton(text: String, icon: ImageVector, onClick: () -> Unit) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(55.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(icon, contentDescription = null, modifier = Modifier.size(24.dp))
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(text, fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== RIWAYAT SCREEN (PLACEHOLDER) ===============
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RiwayatScreen(onNavigateBack: () -> Unit) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Riwayat Absensi", color = Color.White) },
|
||||||
|
navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "Kembali", tint = Color.White) } },
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF388E3C))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(modifier = Modifier.padding(innerPadding).fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Halaman Riwayat Absensi (Segera Hadir)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============== ABSENSI SCREEN ===============
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AbsensiScreen(activity: ComponentActivity, npm: String, nama: String, loginTime: Long, onNavigateBack: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var lokasiStatus by remember { mutableStateOf("Mengecek izin lokasi...") }
|
||||||
|
var isLocationReady by remember { mutableStateOf(false) }
|
||||||
|
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var isUploading by remember { mutableStateOf(false) }
|
||||||
|
var userLocation by remember { mutableStateOf<Location?>(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) {
|
||||||
|
lokasiStatus = "Mendapatkan koordinat..."
|
||||||
|
try {
|
||||||
|
fusedLocationClient.lastLocation.addOnSuccessListener { loc ->
|
||||||
|
if (loc != null) {
|
||||||
|
userLocation = loc
|
||||||
|
isLocationReady = true
|
||||||
|
lokasiStatus = "Lokasi berhasil didapatkan"
|
||||||
|
} else {
|
||||||
|
isLocationReady = false
|
||||||
|
lokasiStatus = "Gagal mendapatkan lokasi. Pastikan GPS aktif."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
isLocationReady = false
|
||||||
|
lokasiStatus = "Izin lokasi dicabut."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lokasiStatus = "Izin lokasi ditolak."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { requestPermissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA)) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Absen Kehadiran", color = Color.White) },
|
||||||
|
navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "Kembali", tint = Color.White) } },
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF388E3C))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(innerPadding).fillMaxSize().padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = mataKuliah,
|
||||||
|
onValueChange = { mataKuliah = it },
|
||||||
|
label = { Text("Mata Kuliah") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(if (isLocationReady) Icons.Filled.CheckCircle else Icons.Filled.Close, "Status Lokasi", tint = if (isLocationReady) Color(0xFF2E7D32) else Color.Red, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(lokasiStatus, color = if (isLocationReady) Color.DarkGray else Color.Red, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Box(Modifier.size(200.dp).clip(CircleShape).background(Color.LightGray), contentAlignment = Alignment.Center) {
|
||||||
|
if (foto != null) {
|
||||||
|
Image(foto!!.asImageBitmap(), "Foto Selfie", Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Filled.Person, "Placeholder", modifier = Modifier.size(70.dp), tint = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE)) },
|
||||||
|
enabled = isLocationReady, // Button enabled only when location is ready
|
||||||
|
modifier = Modifier.fillMaxWidth(0.8f).height(50.dp)
|
||||||
|
) {
|
||||||
|
Text("1. AMBIL FOTO")
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
if (isLocationReady && foto != null && mataKuliah.isNotBlank()) {
|
||||||
|
if (isUploading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
isUploading = true
|
||||||
|
kirimKeServer(activity, npm, nama, userLocation?.latitude ?: 0.0, userLocation?.longitude ?: 0.0, foto!!, loginTime, mataKuliah) { success ->
|
||||||
|
if (success) {
|
||||||
|
Toast.makeText(activity, "Absensi Berhasil!", Toast.LENGTH_LONG).show()
|
||||||
|
onNavigateBack()
|
||||||
|
} else {
|
||||||
|
isUploading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(0.8f).height(50.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B5E20))
|
||||||
|
) {
|
||||||
|
Text("2. KIRIM ABSENSI")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============== NETWORK & UTILS ===============
|
||||||
|
|
||||||
|
fun kirimKeServer(activity: Activity, npm: String, nama: String, lat: Double, lon: Double, img: Bitmap, loginTime: Long, mataKuliah: String, onFinish: (Boolean) -> Unit) {
|
||||||
thread {
|
thread {
|
||||||
|
var success = false
|
||||||
try {
|
try {
|
||||||
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
|
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
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
|
||||||
conn.requestMethod = "POST"
|
conn.requestMethod = "POST"
|
||||||
conn.setRequestProperty("Content-Type", "application/json")
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
conn.doOutput = true
|
conn.doOutput = true
|
||||||
|
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("npm", "12345")
|
put("npm", npm)
|
||||||
put("nama","Arif R D")
|
put("nama", nama)
|
||||||
put("latitude", latitude)
|
put("latitude", lat)
|
||||||
put("longitude", longitude)
|
put("longitude", lon)
|
||||||
put("timestamp", System.currentTimeMillis())
|
put("timestamp", loginTime)
|
||||||
put("foto_base64", bitmapToBase64(foto))
|
put("mata_kuliah", mataKuliah)
|
||||||
|
put("foto_base64", bitmapToBase64(img))
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.outputStream.use {
|
conn.outputStream.write(json.toString().toByteArray())
|
||||||
it.write(json.toString().toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseCode = conn.responseCode
|
val code = conn.responseCode
|
||||||
|
success = code == 200
|
||||||
context.runOnUiThread {
|
if (!success) {
|
||||||
Toast.makeText(
|
activity.runOnUiThread {
|
||||||
context,
|
Toast.makeText(activity, "Gagal mengirim absensi. Kode: $code", Toast.LENGTH_LONG).show()
|
||||||
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) {
|
||||||
|
e.printStackTrace()
|
||||||
|
activity.runOnUiThread {
|
||||||
|
Toast.makeText(activity, "Error: Gagal terhubung ke server.", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
} finally {
|
||||||
|
activity.runOnUiThread { onFinish(success) }
|
||||||
/* ================= 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 ================= */
|
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
@Composable
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream)
|
||||||
fun AbsensiScreen(
|
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
activity: ComponentActivity
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var lokasi by remember { mutableStateOf("Koordinat: -") }
|
|
||||||
var latitude by remember { mutableStateOf<Double?>(null) }
|
|
||||||
var longitude by remember { mutableStateOf<Double?>(null) }
|
|
||||||
var foto by remember { mutableStateOf<Bitmap?>(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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user