diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 9871f13..c19821e 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,20 @@
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
## 📌 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 Akhir EAS (Evaluasi Akhir Semester)** yang dikembangkan oleh:
+
+- **Nama** : Faris Naufal Priatna
+- **NPM** : 202310715123
+
+Tujuan proyek ini adalah 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:
+
1. Berada pada **lokasi yang telah ditentukan**, dan
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
+Aplikasi ini **dibantu dikembangkan oleh AI ChatGPT** untuk memberikan saran teknis, struktur kode, dan pengembangan fitur tambahan.
+
---
## 🎯 Tujuan Proyek
@@ -39,7 +47,7 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den
- 📍 **Koordinat lokasi mahasiswa**
- 📸 **Foto mahasiswa secara real-time**
5. Sistem melakukan validasi:
- - Lokasi berada dalam **radius absensi**
+ - Absensi harus menggunakan foto
- Foto berhasil diambil
6. Jika valid → **Absensi berhasil**
7. Jika tidak valid → **Absensi ditolak**
@@ -75,23 +83,36 @@ Aplikasi memerlukan izin berikut:
- `ACCESS_COARSE_LOCATION`
- `CAMERA`
- `INTERNET`
-- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
+- `WRITE_EXTERNAL_STORAGE` (opsional, jika diperlukan)
---
## 📂 Mockup

-gambar mockup dibuat oleh AI
-## Catatan:
-- Starter project ini dibuat berbantukan AI
-- Kembangkan project dari starter yang sudah disediakan, jangan membuat dari awal.
-- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS.
-
-## Pengecekan:
+---
+
+## Catatan
+- Kembangkan project dari starter yang sudah disediakan, **tidak membuat dari awal**.
+- Koordinat bisa ditambah/kurangi sedikit agar tidak memunculkan lokasi rumah masing-masing, data awal tetap diambil dari GPS.
+
+---
+
+## Pengecekan
- https://ntfy.ubharajaya.ac.id/EAS
-- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0
+- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0
-## Webhook:
-- test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254
-- production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254
\ No newline at end of file
+---
+
+## Webhook
+- Test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254
+- Production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254
+
+---
+
+## 📄 Identitas Pengembang
+- **Nama** : Faris Naufal Priatna
+- **NPM** : 202310715123
+- **Jenis Proyek** : Tugas Akhir EAS
+- **Bantuan Pengembangan** : AI ChatGPT
+Output Aplikasi yang sudah jadi
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7d76378..5cf2bcb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -53,7 +53,6 @@ dependencies {
implementation(libs.androidx.compose.material3)
// Location (GPS)
implementation("com.google.android.gms:play-services-location:21.0.1")
-
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4619836..18a085f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,35 +1,46 @@
+ package="id.ac.ubharajaya.sistemakademik">
-
-
-
-
+
+
+
+
+
+
+
+
+
+ android:required="true" />
+
+
+
+ android:screenOrientation="portrait">
-
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..f865d23
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
new file mode 100644
index 0000000..13ef788
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
@@ -0,0 +1,45 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.graphics.Bitmap
+import android.util.Base64
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+
+/**
+ * Data class untuk menyimpan informasi absensi mahasiswa
+ */
+data class Absensi(
+ val npm: String,
+ val nama: String,
+ val latitude: Double,
+ val longitude: Double,
+ val waktu: String,
+ val foto: Bitmap
+) {
+ /**
+ * Convert objek Absensi ini menjadi JSONObject
+ * Siap untuk dikirim ke server
+ */
+ fun toJson(): JSONObject {
+ val json = JSONObject()
+ json.put("npm", npm)
+ json.put("nama", nama)
+ json.put("latitude", latitude)
+ json.put("longitude", longitude)
+ json.put("timestamp", System.currentTimeMillis())
+ json.put("waktu", waktu)
+ json.put("foto_base64", bitmapToBase64(foto))
+ return json
+ }
+
+ companion object {
+ /**
+ * Helper function untuk convert Bitmap ke Base64
+ */
+ fun bitmapToBase64(bitmap: Bitmap): String {
+ val outputStream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+ return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
+ }
+ }
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt
new file mode 100644
index 0000000..b091c10
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt
@@ -0,0 +1,108 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
+
+class LoginActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ SistemAkademikTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ LoginScreen(modifier = Modifier.padding(innerPadding))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun LoginScreen(modifier: Modifier = Modifier) {
+ val context = LocalContext.current
+ var npm by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+
+ // LOGO KAMPUS
+ Image(
+ painter = painterResource(R.drawable.logo_absensi), // logo kamu
+ contentDescription = "Logo Kampus",
+ modifier = Modifier.size(120.dp)
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // Judul Login
+ Text(
+ text = "Login Mahasiswa",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Input NPM
+ OutlinedTextField(
+ value = npm,
+ onValueChange = { npm = it },
+ label = { Text("NPM") },
+ shape = RoundedCornerShape(12.dp),
+ modifier = Modifier.fillMaxWidth(),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Input Password
+ OutlinedTextField(
+ value = password,
+ onValueChange = { password = it },
+ label = { Text("Password") },
+ shape = RoundedCornerShape(12.dp),
+ modifier = Modifier.fillMaxWidth(),
+ visualTransformation = PasswordVisualTransformation()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Tombol Login
+ Button(
+ onClick = {
+ if (npm == "202310715123" && password == "123") {
+ val intent = Intent(context, MainActivity::class.java)
+ context.startActivity(intent)
+ } else {
+ Toast.makeText(context, "NPM atau password salah", Toast.LENGTH_SHORT).show()
+ }
+ },
+ shape = RoundedCornerShape(12.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("Login")
+ }
+ }
+}
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..1417d18 100644
--- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
@@ -5,96 +5,70 @@ import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
+import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
-import android.util.Base64
import android.widget.Toast
import androidx.activity.ComponentActivity
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.clickable
+import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.android.gms.location.LocationServices
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
-import org.json.JSONObject
-import java.io.ByteArrayOutputStream
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.concurrent.thread
import java.net.HttpURLConnection
import java.net.URL
-import kotlin.concurrent.thread
+import org.json.JSONObject
-/* ================= UTIL ================= */
-
-fun bitmapToBase64(bitmap: Bitmap): String {
- val outputStream = ByteArrayOutputStream()
- bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
- return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
-}
-
-fun kirimKeN8n(
- context: ComponentActivity,
- latitude: Double,
- longitude: Double,
- foto: Bitmap
-) {
+// ================== UTILS ==================
+fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
thread {
try {
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
-// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
val conn = url.openConnection() as HttpURLConnection
-
conn.requestMethod = "POST"
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("timestamp", System.currentTimeMillis())
- put("foto_base64", bitmapToBase64(foto))
- }
-
- conn.outputStream.use {
- it.write(json.toString().toByteArray())
- }
-
+ val json = absensi.toJson()
+ conn.outputStream.use { it.write(json.toString().toByteArray()) }
val responseCode = conn.responseCode
-
context.runOnUiThread {
Toast.makeText(
context,
- if (responseCode == 200)
- "Absensi diterima server"
- else
- "Absensi ditolak server",
+ 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()
+ Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show()
}
}
}
}
-/* ================= ACTIVITY ================= */
-
+// ================== MAIN ACTIVITY ==================
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -104,171 +78,207 @@ class MainActivity : ComponentActivity() {
setContent {
SistemAkademikTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- AbsensiScreen(
- modifier = Modifier.padding(innerPadding),
- activity = this
- )
+ AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this)
}
}
}
}
}
-/* ================= UI ================= */
-
+// ================== COMPOSABLE UI ==================
@Composable
-fun AbsensiScreen(
- modifier: Modifier = Modifier,
- activity: ComponentActivity
-) {
+fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
val context = LocalContext.current
+ val session = remember { SessionManager(context) }
var lokasi by remember { mutableStateOf("Koordinat: -") }
var latitude by remember { mutableStateOf(null) }
var longitude by remember { mutableStateOf(null) }
var foto by remember { mutableStateOf(null) }
+ var waktuAbsensi by remember { mutableStateOf(null) }
+ val absensiList = remember { mutableStateListOf() }
- val fusedLocationClient =
- LocationServices.getFusedLocationProviderClient(context)
+ val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
- /* ===== Permission Lokasi ===== */
+ // ================== STATE WARNA ==================
+ var primaryColor by remember { mutableStateOf(Color(0xFF6200EE)) }
+ var backgroundColor by remember { mutableStateOf(Color(0xFFF2F2F2)) }
+ val colors = listOf(
+ Color.Red, Color.Green, Color.Blue, Color.Magenta, Color.Cyan,
+ Color.Yellow, Color.Gray, Color.DarkGray, Color.Black, Color(0xFFFF9800),
+ Color(0xFF9C27B0), Color(0xFF4CAF50), Color(0xFF03A9F4), Color(0xFFE91E63)
+ )
+
+ // ===== Permission & Kamera =====
val locationPermissionLauncher =
- rememberLauncherForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { granted ->
+ rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
-
- if (
- ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_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}"
+ lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}"
} else {
lokasi = "Lokasi tidak tersedia"
}
}
- .addOnFailureListener {
- lokasi = "Gagal mengambil lokasi"
- }
+ .addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
}
-
} else {
- Toast.makeText(
- context,
- "Izin lokasi ditolak",
- Toast.LENGTH_SHORT
- ).show()
+ 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)
+ val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val bitmap: Bitmap? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ result.data?.extras?.getParcelable("data", Bitmap::class.java)
} else {
- Toast.makeText(
- context,
- "Izin kamera ditolak",
- Toast.LENGTH_SHORT
- ).show()
+ @Suppress("DEPRECATION")
+ result.data?.extras?.getParcelable("data")
+ }
+
+ if (bitmap != null) {
+ foto = bitmap
+ Toast.makeText(context, "Foto berhasil diambil", Toast.LENGTH_SHORT).show()
}
}
-
- /* ===== Request Awal ===== */
-
- LaunchedEffect(Unit) {
- locationPermissionLauncher.launch(
- Manifest.permission.ACCESS_FINE_LOCATION
- )
}
- /* ===== UI ===== */
+ 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()
+ }
+ }
+ LaunchedEffect(Unit) {
+ locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+
+ // ================== UI ==================
Column(
modifier = modifier
.fillMaxSize()
- .padding(24.dp),
- verticalArrangement = Arrangement.Center
+ .background(backgroundColor)
+ .padding(24.dp)
) {
-
Text(
text = "Absensi Akademik",
- style = MaterialTheme.typography.titleLarge
+ style = MaterialTheme.typography.titleLarge,
+ color = primaryColor
)
-
Spacer(modifier = Modifier.height(16.dp))
- Text(text = lokasi)
+ // PALETTE WARNA BULETAN
+ Text("Pilih Warna Tema:", style = MaterialTheme.typography.bodyMedium, color = primaryColor)
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
+ colors.forEach { color ->
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .padding(4.dp)
+ .background(color, shape = CircleShape)
+ .clickable {
+ primaryColor = color
+ backgroundColor = color.copy(alpha = 0.1f)
+ }
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(text = lokasi, color = primaryColor)
Spacer(modifier = Modifier.height(16.dp))
Button(
- onClick = {
- cameraPermissionLauncher.launch(
- Manifest.permission.CAMERA
- )
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text("Ambil Foto")
- }
+ onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
+ ) { Text("Ambil Foto") }
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
if (latitude != null && longitude != null && foto != null) {
- kirimKeN8n(
- activity,
- latitude!!,
- longitude!!,
- foto!!
+ val waktu = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()).format(System.currentTimeMillis())
+ waktuAbsensi = waktu
+
+ val absensi = Absensi(
+ npm = session.getUserNpm() ?: "Faris Naufal Priatna",
+ nama = session.getUserName() ?: "202310715123",
+ latitude = latitude!!,
+ longitude = longitude!!,
+ waktu = waktu,
+ foto = foto!!
)
+ kirimKeN8n(activity, absensi)
+ absensiList.add(absensi)
} else {
- Toast.makeText(
- context,
- "Lokasi atau foto belum lengkap",
- Toast.LENGTH_SHORT
- ).show()
+ Toast.makeText(context, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show()
}
},
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
+ ) { Text("Kirim Absensi") }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ waktuAbsensi?.let { Text("Waktu Absensi: $it", color = primaryColor) }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // ================== LOGOUT BUTTON ==================
+ Button(
+ onClick = {
+ session.logout()
+ context.startActivity(Intent(context, LoginActivity::class.java))
+ (context as ComponentActivity).finish()
+ },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
- Text("Kirim Absensi")
+ Text("Logout")
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ Text("Riwayat Kehadiran", color = primaryColor)
+ Spacer(modifier = Modifier.height(12.dp))
+
+ if (absensiList.isEmpty()) Text("Belum ada absensi", color = primaryColor)
+ else LazyColumn {
+ items(absensiList) { item ->
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Column(modifier = Modifier.padding(12.dp)) {
+ Text("Nama: ${item.nama}", color = primaryColor)
+ Text("NPM: ${item.npm}", color = primaryColor)
+ Text("Waktu: ${item.waktu}", color = primaryColor)
+ Text("Lat: ${item.latitude}, Lon: ${item.longitude}", color = primaryColor)
+ Spacer(modifier = Modifier.height(8.dp))
+ Image(
+ bitmap = item.foto.asImageBitmap(),
+ contentDescription = "Foto Absensi",
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(150.dp)
+ )
+ }
+ }
+ }
}
}
}
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/SessionManager.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SessionManager.kt
new file mode 100644
index 0000000..f09ab98
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SessionManager.kt
@@ -0,0 +1,33 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class SessionManager(context: Context) {
+ private val prefs: SharedPreferences =
+ context.getSharedPreferences("user_session", Context.MODE_PRIVATE)
+
+ companion object {
+ private const val KEY_IS_LOGGED_IN = "is_logged_in"
+ private const val KEY_NPM = "npm"
+ private const val KEY_NAME = "name"
+ }
+
+ fun saveLogin(npm: String, name: String) {
+ prefs.edit().apply {
+ putBoolean(KEY_IS_LOGGED_IN, true)
+ putString(KEY_NPM, npm)
+ putString(KEY_NAME, name)
+ apply()
+ }
+ }
+
+ fun isLoggedIn(): Boolean = prefs.getBoolean(KEY_IS_LOGGED_IN, false)
+
+ fun logout() {
+ prefs.edit().clear().apply()
+ }
+
+ fun getUserNpm(): String? = prefs.getString(KEY_NPM, null)
+ fun getUserName(): String? = prefs.getString(KEY_NAME, null)
+}
diff --git a/app/src/main/res/drawable/logo_absensi.png b/app/src/main/res/drawable/logo_absensi.png
new file mode 100644
index 0000000..b8a56f6
Binary files /dev/null and b/app/src/main/res/drawable/logo_absensi.png differ
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index c209e78..287ae78 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..397bf65
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index b2dfe3d..1ec916d 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 4f0f1d6..230f135 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..3483bdb
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 62b611d..30c64eb 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index 948a307..9c5af0d 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..1998c86
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 1b9a695..42dc467 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 28d4b77..fe76fbb 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..f6bd572
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 9287f50..c03b225 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index aa7d642..a78910e 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..a622f77
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 9126ae3..83f20bf 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..6082ce8
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #DE0505
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index de92dbc..d63641b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- Sistem Akademik
+ Absensi Wajah
\ No newline at end of file
diff --git a/er.name Faris Naufal Priatna b/er.name Faris Naufal Priatna
new file mode 100644
index 0000000..63f3faa
--- /dev/null
+++ b/er.name Faris Naufal Priatna
@@ -0,0 +1,28 @@
+diff.astextplain.textconv=astextplain
+filter.lfs.clean=git-lfs clean -- %f
+filter.lfs.smudge=git-lfs smudge -- %f
+filter.lfs.process=git-lfs filter-process
+filter.lfs.required=true
+http.sslbackend=openssl
+http.sslcainfo=C:/Program Files/Git/mingw64/etc/ssl/certs/ca-bundle.crt
+core.autocrlf=true
+core.fscache=true
+core.symlinks=false
+pull.rebase=false
+credential.helper=manager
+credential.https://dev.azure.com.usehttppath=true
+init.defaultbranch=master
+credential.https://git.lab.ubharajaya.ac.id.provider=generic
+user.name=Faris Naufal Priatna
+user.email=202310715123@mhs.ubharajaya.ac.id
+core.repositoryformatversion=0
+core.filemode=false
+core.bare=false
+core.logallrefupdates=true
+core.symlinks=false
+core.ignorecase=true
+submodule.active=.
+remote.origin.url=https://git.lab.ubharajaya.ac.id/administrator/Starter-EAS-2025-2026.git
+remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
+branch.main.remote=origin
+branch.main.merge=refs/heads/main