Aplikasi Absen
This commit is contained in:
parent
ed435ffbc1
commit
b1cb5cdc1a
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="PLACEHOLDER" />
|
||||||
|
<option name="mobileSdkAppId" value="" />
|
||||||
|
<option name="projectId" value="" />
|
||||||
|
<option name="projectNumber" value="" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -14,10 +14,17 @@ 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.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@ -41,12 +48,12 @@ fun kirimKeN8n(
|
|||||||
context: ComponentActivity,
|
context: ComponentActivity,
|
||||||
latitude: Double,
|
latitude: Double,
|
||||||
longitude: Double,
|
longitude: Double,
|
||||||
foto: Bitmap
|
foto: Bitmap,
|
||||||
|
selectedMatkul: String
|
||||||
) {
|
) {
|
||||||
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"
|
||||||
@ -54,8 +61,9 @@ fun kirimKeN8n(
|
|||||||
conn.doOutput = true
|
conn.doOutput = true
|
||||||
|
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("npm", "12345")
|
put("npm", "202310715002")
|
||||||
put("nama","Arif R D")
|
put("nama", "Rafi Fattan Fitriardi")
|
||||||
|
put("matkul", selectedMatkul) // <-- hanya mata kuliah yang dipilih
|
||||||
put("latitude", latitude)
|
put("latitude", latitude)
|
||||||
put("longitude", longitude)
|
put("longitude", longitude)
|
||||||
put("timestamp", System.currentTimeMillis())
|
put("timestamp", System.currentTimeMillis())
|
||||||
@ -80,7 +88,6 @@ fun kirimKeN8n(
|
|||||||
}
|
}
|
||||||
|
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
@ -93,22 +100,23 @@ fun kirimKeN8n(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================= MODEL ================= */
|
||||||
|
|
||||||
|
data class Absensi(
|
||||||
|
val matkul: String,
|
||||||
|
val waktu: String
|
||||||
|
)
|
||||||
|
|
||||||
/* ================= ACTIVITY ================= */
|
/* ================= ACTIVITY ================= */
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
AbsensiScreen(this)
|
||||||
AbsensiScreen(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
activity = this
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -116,159 +124,178 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
/* ================= UI ================= */
|
/* ================= UI ================= */
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AbsensiScreen(
|
fun AbsensiScreen(activity: ComponentActivity) {
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
activity: ComponentActivity
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var lokasi by remember { mutableStateOf("Koordinat: -") }
|
val context = LocalContext.current
|
||||||
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||||
|
|
||||||
|
// ===== DATA & STATE =====
|
||||||
|
val matkulList = listOf(
|
||||||
|
"Pemrograman Perangkat Bergerak",
|
||||||
|
"Pembelajaran Mesin",
|
||||||
|
"Keamanan Siber",
|
||||||
|
"Kecerdasan Buatan",
|
||||||
|
"Interaksi Manusia dan Komputer",
|
||||||
|
"Manajemen Proyek Perangkat Lunak"
|
||||||
|
)
|
||||||
|
|
||||||
|
var selectedMatkul by remember { mutableStateOf(matkulList[0]) }
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
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) }
|
||||||
|
val absensiList = remember { mutableStateListOf<Absensi>() }
|
||||||
|
|
||||||
val fusedLocationClient =
|
// ===== PERMISSION LOKASI =====
|
||||||
LocationServices.getFusedLocationProviderClient(context)
|
|
||||||
|
|
||||||
/* ===== Permission Lokasi ===== */
|
|
||||||
|
|
||||||
val locationPermissionLauncher =
|
val locationPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { granted ->
|
|
||||||
if (granted) {
|
if (granted) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
if (
|
== PackageManager.PERMISSION_GRANTED
|
||||||
ContextCompat.checkSelfPermission(
|
|
||||||
context,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
) {
|
||||||
|
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
|
||||||
fusedLocationClient.lastLocation
|
latitude = location?.latitude
|
||||||
.addOnSuccessListener { location ->
|
longitude = location?.longitude
|
||||||
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 ===== */
|
// ===== KAMERA =====
|
||||||
|
|
||||||
val cameraLauncher =
|
val cameraLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
val bitmap =
|
foto = result.data?.extras?.getParcelable("data", Bitmap::class.java)
|
||||||
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 =
|
val cameraPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { granted ->
|
|
||||||
if (granted) {
|
if (granted) {
|
||||||
val intent =
|
cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
|
||||||
Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
||||||
cameraLauncher.launch(intent)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Izin kamera ditolak",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Request Awal ===== */
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
locationPermissionLauncher.launch(
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== UI ===== */
|
// ===== UI =====
|
||||||
|
Box(
|
||||||
Column(
|
modifier = Modifier
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(24.dp),
|
.background(
|
||||||
verticalArrangement = Arrangement.Center
|
Brush.verticalGradient(
|
||||||
) {
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.15f),
|
||||||
Text(
|
MaterialTheme.colorScheme.background
|
||||||
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",
|
Scaffold(
|
||||||
Toast.LENGTH_SHORT
|
containerColor = Color.Transparent,
|
||||||
).show()
|
topBar = {
|
||||||
|
TopAppBar(title = { Text("Absensi Akademik") })
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
|
||||||
|
// ===== CARD UTAMA =====
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
elevation = CardDefaults.cardElevation(6.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text("Mata Kuliah", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = selectedMatkul,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
matkulList.forEach {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(it) },
|
||||||
|
onClick = {
|
||||||
|
selectedMatkul = it
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text("Riwayat Absensi", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
LazyColumn(modifier = Modifier.height(120.dp)) {
|
||||||
|
items(absensiList) {
|
||||||
|
Text("• ${it.matkul} - ${it.waktu}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
// ===== PREVIEW FOTO =====
|
||||||
) {
|
if (foto != null) {
|
||||||
Text("Kirim Absensi")
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(220.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(6.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = foto!!.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// ===== TOMBOL =====
|
||||||
|
Button(
|
||||||
|
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Ambil Foto")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (latitude != null && longitude != null && foto != null) {
|
||||||
|
kirimKeN8n(activity, latitude!!, longitude!!, foto!!, selectedMatkul)
|
||||||
|
absensiList.add(Absensi(selectedMatkul, System.currentTimeMillis().toString()))
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Lokasi atau foto belum lengkap", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32)),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Kirim Absensi")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user