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) # 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
## 📌 Deskripsi Proyek ## 📌 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: 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 1. Berada pada **lokasi yang telah ditentukan**, dan
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi** 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 ## 🎯 Tujuan Proyek
@ -39,7 +47,7 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den
- 📍 **Koordinat lokasi mahasiswa** - 📍 **Koordinat lokasi mahasiswa**
- 📸 **Foto mahasiswa secara real-time** - 📸 **Foto mahasiswa secara real-time**
5. Sistem melakukan validasi: 5. Sistem melakukan validasi:
- Lokasi berada dalam **radius absensi** - Absensi harus menggunakan foto
- Foto berhasil diambil - Foto berhasil diambil
6. Jika valid → **Absensi berhasil** 6. Jika valid → **Absensi berhasil**
7. Jika tidak valid → **Absensi ditolak** 7. Jika tidak valid → **Absensi ditolak**
@ -75,23 +83,36 @@ Aplikasi memerlukan izin berikut:
- `ACCESS_COARSE_LOCATION` - `ACCESS_COARSE_LOCATION`
- `CAMERA` - `CAMERA`
- `INTERNET` - `INTERNET`
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan) - `WRITE_EXTERNAL_STORAGE` (opsional, jika diperlukan)
--- ---
## 📂 Mockup ## 📂 Mockup
![mockup](Mockup.png) ![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://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) implementation(libs.androidx.compose.material3)
// Location (GPS) // Location (GPS)
implementation("com.google.android.gms:play-services-location:21.0.1") implementation("com.google.android.gms:play-services-location:21.0.1")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -1,35 +1,46 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> package="id.ac.ubharajaya.sistemakademik">
<uses-permission android:name="android.permission.INTERNET"/> <!-- ===== PERMISSIONS ===== -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/> <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 <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="true" />
<!-- ===== APPLICATION ===== -->
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SistemAkademik"> android:theme="@style/Theme.SistemAkademik">
<!-- ===== LOGIN ACTIVITY (LAUNCHER) ===== -->
<activity <activity
android:name=".MainActivity" android:name=".LoginActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:screenOrientation="portrait">
android:theme="@style/Theme.SistemAkademik">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- ===== MAIN ACTIVITY ===== -->
<activity
android:name=".MainActivity"
android:exported="false"
android:screenOrientation="portrait" />
</application> </application>
</manifest> </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.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Base64
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult 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.clickable
import androidx.compose.foundation.horizontalScroll
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.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
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.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
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
import org.json.JSONObject import java.text.SimpleDateFormat
import java.io.ByteArrayOutputStream import java.util.*
import kotlin.concurrent.thread
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import kotlin.concurrent.thread import org.json.JSONObject
/* ================= UTIL ================= */ // ================== UTILS ==================
fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
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
) {
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"
conn.setRequestProperty("Content-Type", "application/json") conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true conn.doOutput = true
val json = absensi.toJson()
val json = JSONObject().apply { conn.outputStream.use { it.write(json.toString().toByteArray()) }
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 responseCode = conn.responseCode val responseCode = conn.responseCode
context.runOnUiThread { context.runOnUiThread {
Toast.makeText( Toast.makeText(
context, context,
if (responseCode == 200) if (responseCode == 200) "Absensi diterima server" else "Absensi ditolak server",
"Absensi diterima server"
else
"Absensi ditolak server",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
conn.disconnect() conn.disconnect()
} catch (_: Exception) { } catch (_: Exception) {
context.runOnUiThread { context.runOnUiThread {
Toast.makeText( Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show()
context,
"Gagal kirim ke server",
Toast.LENGTH_SHORT
).show()
} }
} }
} }
} }
/* ================= ACTIVITY ================= */ // ================== MAIN ACTIVITY ==================
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -104,171 +78,207 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
SistemAkademikTheme { SistemAkademikTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
AbsensiScreen( AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this)
modifier = Modifier.padding(innerPadding),
activity = this
)
} }
} }
} }
} }
} }
/* ================= UI ================= */ // ================== COMPOSABLE UI ==================
@Composable @Composable
fun AbsensiScreen( fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
modifier: Modifier = Modifier,
activity: ComponentActivity
) {
val context = LocalContext.current val context = LocalContext.current
val session = remember { SessionManager(context) }
var lokasi by remember { mutableStateOf("Koordinat: -") } 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 waktuAbsensi by remember { mutableStateOf<String?>(null) }
val absensiList = remember { mutableStateListOf<Absensi>() }
val fusedLocationClient = val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
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 = 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 fusedLocationClient.lastLocation
.addOnSuccessListener { location -> .addOnSuccessListener { location ->
if (location != null) { if (location != null) {
latitude = location.latitude latitude = location.latitude
longitude = location.longitude longitude = location.longitude
lokasi = lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}"
"Lat: ${location.latitude}\nLon: ${location.longitude}"
} else { } else {
lokasi = "Lokasi tidak tersedia" lokasi = "Lokasi tidak tersedia"
} }
} }
.addOnFailureListener { .addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
lokasi = "Gagal mengambil lokasi"
} }
}
} else { } else {
Toast.makeText( Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
context,
"Izin lokasi ditolak",
Toast.LENGTH_SHORT
).show()
} }
} }
/* ===== Kamera ===== */ val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val cameraLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val bitmap = val bitmap: Bitmap? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
result.data?.extras?.getParcelable("data", Bitmap::class.java) result.data?.extras?.getParcelable("data", Bitmap::class.java)
} else {
@Suppress("DEPRECATION")
result.data?.extras?.getParcelable("data")
}
if (bitmap != null) { if (bitmap != null) {
foto = bitmap foto = bitmap
Toast.makeText( Toast.makeText(context, "Foto berhasil diambil", Toast.LENGTH_SHORT).show()
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 = val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraLauncher.launch(intent) cameraLauncher.launch(intent)
} else { } else {
Toast.makeText( Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
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 ==================
Column( Column(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(24.dp), .background(backgroundColor)
verticalArrangement = Arrangement.Center .padding(24.dp)
) { ) {
Text( Text(
text = "Absensi Akademik", text = "Absensi Akademik",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge,
color = primaryColor
) )
Spacer(modifier = Modifier.height(16.dp)) 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)) Spacer(modifier = Modifier.height(16.dp))
Button( Button(
onClick = { onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
cameraPermissionLauncher.launch( modifier = Modifier.fillMaxWidth(),
Manifest.permission.CAMERA colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
) ) { Text("Ambil Foto") }
},
modifier = Modifier.fillMaxWidth()
) {
Text("Ambil Foto")
}
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Button( Button(
onClick = { onClick = {
if (latitude != null && longitude != null && foto != null) { if (latitude != null && longitude != null && foto != null) {
kirimKeN8n( val waktu = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()).format(System.currentTimeMillis())
activity, waktuAbsensi = waktu
latitude!!,
longitude!!, val absensi = Absensi(
foto!! 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 { } else {
Toast.makeText( Toast.makeText(context, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show()
context,
"Lokasi atau foto belum lengkap",
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> <resources>
<string name="app_name">Sistem Akademik</string> <string name="app_name">Absensi Wajah</string>
</resources> </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