Pesan commit kamu

This commit is contained in:
202310715123 FARIS NAUFAL PRIATNA 2026-01-14 18:07:00 +07:00
parent ed435ffbc1
commit 7cd8b241d0
33 changed files with 512 additions and 192 deletions

13
.idea/deviceManager.xml generated Normal file
View 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>

View File

@ -0,0 +1,50 @@
<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>

View File

@ -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
![mockup](Mockup.png)
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
---
## 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

View File

@ -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)

View File

@ -1,35 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
package="id.ac.ubharajaya.sistemakademik">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
<!-- ===== PERMISSIONS ===== -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- ===== FEATURES (OPTIONAL TAPI AMAN) ===== -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
android:required="true" />
<!-- ===== APPLICATION ===== -->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.SistemAkademik">
<!-- ===== LOGIN ACTIVITY (LAUNCHER) ===== -->
<activity
android:name=".MainActivity"
android:name=".LoginActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SistemAkademik">
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- ===== MAIN ACTIVITY ===== -->
<activity
android:name=".MainActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -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)
}
}
}

View File

@ -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")
}
}
}

View File

@ -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<Double?>(null) }
var longitude by remember { mutableStateOf<Double?>(null) }
var foto by remember { mutableStateOf<Bitmap?>(null) }
var waktuAbsensi by remember { mutableStateOf<String?>(null) }
val absensiList = remember { mutableStateListOf<Absensi>() }
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)
)
}
}
}
}
}
}

View File

@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#DE0505</color>
</resources>

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">Sistem Akademik</string>
<string name="app_name">Absensi Wajah</string>
</resources>

View File

@ -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