Compare commits
No commits in common. "b78dfc17ec814669e67c09e1a936741706ea808a" and "9cafdd40a93d771b5604c77e93a7cb3be220912c" have entirely different histories.
b78dfc17ec
...
9cafdd40a9
13
.idea/deviceManager.xml
generated
13
.idea/deviceManager.xml
generated
@ -1,13 +0,0 @@
|
|||||||
<?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>
|
|
||||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
50
.idea/inspectionProfiles/Project_Default.xml
generated
@ -1,50 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
|
||||||
<option name="composableFile" value="true" />
|
|
||||||
</inspection_tool>
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
10
README.md
10
README.md
@ -1,14 +1,10 @@
|
|||||||
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
||||||
|
|
||||||
|
|
||||||
**NAMA :** **Syahril Achmad Fahrezi**
|
Nama Pengembang : **Syahril Achmad Fahrezi**
|
||||||
|
NPM : **202310715211**
|
||||||
**NPM :** **202310715211**
|
|
||||||
|
|
||||||
**MATA KULIAH** : **PERMROGRAMAN PERANGKAT BERGERAK**
|
|
||||||
|
|
||||||
**- INI DIKEMBANGKAN BUKAN BUAT DARI AWAL -**
|
|
||||||
|
|
||||||
|
## 📌 Deskripsi Proyek
|
||||||
Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang bertujuan untuk membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**.
|
Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang bertujuan untuk membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**.
|
||||||
|
|
||||||
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
|
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
|
||||||
|
|||||||
@ -5,7 +5,6 @@ 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
|
||||||
@ -15,26 +14,11 @@ 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.animation.*
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
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.scale
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
@ -47,22 +31,6 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
/* ================= UTIL ================= */
|
/* ================= UTIL ================= */
|
||||||
|
|
||||||
// Koordinat Kampus Universitas Bhayangkara Jakarta Raya
|
|
||||||
const val KAMPUS_LATITUDE = -6.2642
|
|
||||||
const val KAMPUS_LONGITUDE = 107.0008
|
|
||||||
const val RADIUS_METER = 100.0
|
|
||||||
|
|
||||||
fun hitungJarak(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Float {
|
|
||||||
val results = FloatArray(1)
|
|
||||||
Location.distanceBetween(lat1, lon1, lat2, lon2, results)
|
|
||||||
return results[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cekDalamRadius(latitude: Double, longitude: Double): Boolean {
|
|
||||||
val jarak = hitungJarak(latitude, longitude, KAMPUS_LATITUDE, KAMPUS_LONGITUDE)
|
|
||||||
return jarak <= RADIUS_METER
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bitmapToBase64(bitmap: Bitmap): String {
|
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||||
@ -73,13 +41,12 @@ fun kirimKeN8n(
|
|||||||
context: ComponentActivity,
|
context: ComponentActivity,
|
||||||
latitude: Double,
|
latitude: Double,
|
||||||
longitude: Double,
|
longitude: Double,
|
||||||
foto: Bitmap,
|
foto: Bitmap
|
||||||
onSuccess: () -> Unit,
|
|
||||||
onError: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
thread {
|
thread {
|
||||||
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"
|
||||||
@ -88,9 +55,7 @@ fun kirimKeN8n(
|
|||||||
|
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("npm", "202310715211")
|
put("npm", "202310715211")
|
||||||
|
put("nama","Syahril Achmad F")
|
||||||
put("nama", "SyahrilAchmadFAHREZI")
|
|
||||||
|
|
||||||
put("latitude", latitude)
|
put("latitude", latitude)
|
||||||
put("longitude", longitude)
|
put("longitude", longitude)
|
||||||
put("timestamp", System.currentTimeMillis())
|
put("timestamp", System.currentTimeMillis())
|
||||||
@ -104,13 +69,6 @@ fun kirimKeN8n(
|
|||||||
val responseCode = conn.responseCode
|
val responseCode = conn.responseCode
|
||||||
|
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
<<<<<<< HEAD
|
|
||||||
if (responseCode == 200) {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
onError()
|
|
||||||
}
|
|
||||||
=======
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
if (responseCode == 200)
|
if (responseCode == 200)
|
||||||
@ -125,7 +83,11 @@ fun kirimKeN8n(
|
|||||||
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
onError()
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Gagal kirim ke server",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,441 +123,152 @@ fun AbsensiScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var lokasi by remember { mutableStateOf("Koordinat: -") }
|
||||||
var latitude by remember { mutableStateOf<Double?>(null) }
|
var latitude by remember { mutableStateOf<Double?>(null) }
|
||||||
var longitude by remember { mutableStateOf<Double?>(null) }
|
var longitude by remember { mutableStateOf<Double?>(null) }
|
||||||
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
var dalamRadius by remember { mutableStateOf(false) }
|
|
||||||
var jarak by remember { mutableStateOf<Float?>(null) }
|
|
||||||
var isLoading by remember { mutableStateOf(false) }
|
|
||||||
var showSuccess by remember { mutableStateOf(false) }
|
|
||||||
var expandedMataKuliah by remember { mutableStateOf(false) }
|
|
||||||
var selectedMataKuliah by remember { mutableStateOf("Pilih Mata Kuliah") }
|
|
||||||
|
|
||||||
val mataKuliahList = listOf(
|
val fusedLocationClient =
|
||||||
"Pemrograman Perangkat Bergerak",
|
LocationServices.getFusedLocationProviderClient(context)
|
||||||
"Deep Learning",
|
|
||||||
"Keamanan Siber"
|
|
||||||
)
|
|
||||||
|
|
||||||
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (showSuccess) 1f else 0f,
|
|
||||||
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), label = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
/* ===== Permission Lokasi ===== */
|
/* ===== Permission Lokasi ===== */
|
||||||
|
|
||||||
val locationPermissionLauncher =
|
val locationPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted ->
|
||||||
if (granted) {
|
if (granted) {
|
||||||
if (ContextCompat.checkSelfPermission(
|
|
||||||
|
if (
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
context,
|
context,
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fusedLocationClient.lastLocation
|
fusedLocationClient.lastLocation
|
||||||
.addOnSuccessListener { location ->
|
.addOnSuccessListener { location ->
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
latitude = location.latitude
|
latitude = location.latitude
|
||||||
longitude = location.longitude
|
longitude = location.longitude
|
||||||
dalamRadius = cekDalamRadius(location.latitude, location.longitude)
|
lokasi =
|
||||||
jarak = hitungJarak(
|
"Lat: ${location.latitude}\nLon: ${location.longitude}"
|
||||||
location.latitude,
|
|
||||||
location.longitude,
|
|
||||||
KAMPUS_LATITUDE,
|
|
||||||
KAMPUS_LONGITUDE
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
|
lokasi = "Lokasi tidak tersedia"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
lokasi = "Gagal mengambil lokasi"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Izin lokasi ditolak",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Kamera ===== */
|
/* ===== Kamera ===== */
|
||||||
|
|
||||||
val cameraLauncher =
|
val cameraLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
val bitmap = result.data?.extras?.get("data") as? Bitmap
|
val bitmap =
|
||||||
|
result.data?.extras?.getParcelable("data", Bitmap::class.java)
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
foto = bitmap
|
foto = bitmap
|
||||||
Toast.makeText(context, "Foto berhasil diambil", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Foto berhasil diambil",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val cameraPermissionLauncher =
|
val cameraPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted ->
|
||||||
if (granted) {
|
if (granted) {
|
||||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
val intent =
|
||||||
|
Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
cameraLauncher.launch(intent)
|
cameraLauncher.launch(intent)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Izin kamera ditolak",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Request Awal ===== */
|
/* ===== Request Awal ===== */
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
locationPermissionLauncher.launch(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== UI ===== */
|
/* ===== UI ===== */
|
||||||
Box(
|
|
||||||
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(
|
.padding(24.dp),
|
||||||
Brush.verticalGradient(
|
verticalArrangement = Arrangement.Center
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF1a1a2e),
|
|
||||||
Color(0xFF16213e),
|
|
||||||
Color(0xFF0f3460)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
|
||||||
|
|
||||||
// Header
|
Text(
|
||||||
Text(
|
text = "Absensi Akademik",
|
||||||
text = "Absensi Akademik",
|
style = MaterialTheme.typography.titleLarge
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
)
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
text = "Universitas Bhayangkara Jakarta Raya",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = Color.White.copy(alpha = 0.9f)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Text(text = lokasi)
|
||||||
|
|
||||||
// Status Lokasi Card
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = Color(0xFF0a0a0f).copy(alpha = 0.7f)
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(8.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(20.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
// Icon Status
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(80.dp)
|
|
||||||
.background(
|
|
||||||
color = if (dalamRadius) Color(0xFF10B981).copy(alpha = 0.2f)
|
|
||||||
else Color(0xFFEF4444).copy(alpha = 0.2f),
|
|
||||||
shape = RoundedCornerShape(40.dp)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (dalamRadius) "✓" else "✗",
|
|
||||||
style = MaterialTheme.typography.displayMedium,
|
|
||||||
color = if (dalamRadius) Color(0xFF10B981) else Color(0xFFEF4444),
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Button(
|
||||||
|
onClick = {
|
||||||
Text(
|
cameraPermissionLauncher.launch(
|
||||||
text = if (dalamRadius) "Dalam Area Kampus" else "Di Luar Area Kampus",
|
Manifest.permission.CAMERA
|
||||||
fontSize = 20.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontStyle = FontStyle.Normal,
|
|
||||||
color = if (dalamRadius) Color(0xFF9D4EDD) else Color(0xFFEF4444)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
if (jarak != null) {
|
|
||||||
Text(
|
|
||||||
text = "Jarak: ${String.format("%.1f", jarak)} meter",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = Color(0xFFB8B8D0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latitude != null && longitude != null) {
|
|
||||||
Text(
|
|
||||||
text = "Lat: ${String.format("%.6f", latitude)}, Lon: ${String.format("%.6f", longitude)}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = Color(0xFF8B8BA0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Foto Preview
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = foto != null,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically()
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.size(200.dp),
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
elevation = CardDefaults.cardElevation(8.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
foto?.let {
|
|
||||||
Image(
|
|
||||||
bitmap = it.asImageBitmap(),
|
|
||||||
contentDescription = "Foto Absensi",
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
// Dropdown Mata Kuliah
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { expandedMataKuliah = !expandedMataKuliah },
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = Color(0xFF0a0a0f).copy(alpha = 0.7f)
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(4.dp)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = selectedMataKuliah,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = if (selectedMataKuliah == "Pilih Mata Kuliah")
|
|
||||||
Color(0xFF8B8BA0) else Color.White,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = if (expandedMataKuliah) "▲" else "▼",
|
|
||||||
color = Color(0xFF9D4EDD),
|
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(visible = expandedMataKuliah) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 12.dp)
|
|
||||||
) {
|
|
||||||
mataKuliahList.forEach { matkul ->
|
|
||||||
Text(
|
|
||||||
text = matkul,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
selectedMataKuliah = matkul
|
|
||||||
expandedMataKuliah = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
fontSize = 15.sp,
|
|
||||||
color = Color(0xFFB8B8D0),
|
|
||||||
fontWeight = FontWeight.Normal
|
|
||||||
)
|
|
||||||
if (matkul != mataKuliahList.last()) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(1.dp)
|
|
||||||
.background(Color(0xFF3a3a4e))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// Tombol Ambil Foto
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(56.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Color(0xFF7B2CBF),
|
|
||||||
contentColor = Color.White
|
|
||||||
),
|
|
||||||
elevation = ButtonDefaults.buttonElevation(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "📷 ${if (foto == null) "Ambil Foto" else "Ambil Ulang Foto"}",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
// Tombol Kirim Absensi
|
) {
|
||||||
Button(
|
Text("Ambil Foto")
|
||||||
onClick = {
|
|
||||||
when {
|
|
||||||
!dalamRadius -> {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Anda di luar area kampus!",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
foto == null -> {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Silakan ambil foto terlebih dahulu!",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
selectedMataKuliah == "Pilih Mata Kuliah" -> {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Silakan pilih mata kuliah terlebih dahulu!",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
latitude == null || longitude == null -> {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Lokasi belum terdeteksi!",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
isLoading = true
|
|
||||||
kirimKeN8n(
|
|
||||||
activity,
|
|
||||||
latitude!!,
|
|
||||||
longitude!!,
|
|
||||||
foto!!,
|
|
||||||
onSuccess = {
|
|
||||||
isLoading = false
|
|
||||||
showSuccess = true
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Absensi berhasil dikirim!",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
isLoading = false
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Gagal mengirim absensi!",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(56.dp),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = if (dalamRadius && foto != null && selectedMataKuliah != "Pilih Mata Kuliah")
|
|
||||||
Color(0xFF9D4EDD) else Color(0xFF3a3a4e),
|
|
||||||
contentColor = Color.White
|
|
||||||
),
|
|
||||||
elevation = ButtonDefaults.buttonElevation(8.dp),
|
|
||||||
enabled = !isLoading
|
|
||||||
) {
|
|
||||||
if (isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
color = Color.White,
|
|
||||||
strokeWidth = 3.dp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text("Mengirim...")
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = "📤 Kirim Absensi",
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success Animation
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
if (showSuccess) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Black.copy(alpha = 0.5f)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.scale(scale)
|
|
||||||
.padding(40.dp),
|
|
||||||
shape = RoundedCornerShape(24.dp),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF0a0a0f))
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(32.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "✓",
|
|
||||||
style = MaterialTheme.typography.displayLarge,
|
|
||||||
color = Color(0xFF9D4EDD),
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = "Absensi Berhasil!",
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
Button(
|
||||||
kotlinx.coroutines.delay(2000)
|
onClick = {
|
||||||
showSuccess = false
|
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