Compare commits

..

10 Commits

Author SHA1 Message Date
7cd8b241d0 Pesan commit kamu 2026-01-14 18:07:00 +07:00
ed435ffbc1 update readme 2026-01-13 15:51:52 +07:00
926d3e0a14 add n8n workflow script 2026-01-13 14:37:01 +07:00
cddaf87d88 update readme 2026-01-13 13:59:42 +07:00
c9cc99baa2 update readme 2026-01-13 09:50:58 +07:00
2a00b834c7 real time location 2026-01-13 09:37:52 +07:00
4d7fc844e2 real time location 2026-01-13 09:37:39 +07:00
d4d1b27209 real time location 2026-01-13 09:34:37 +07:00
3e66ebcf9e mockup 2026-01-12 22:07:16 +07:00
46b74d7099 mockup 2026-01-12 15:32:34 +07:00
39 changed files with 862 additions and 49 deletions

View File

@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-01-13T05:20:56.137492Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8TA08RD8Z" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

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>

1
.idea/gradle.xml generated
View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

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>

1
.idea/misc.xml generated
View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

6
.idea/studiobot.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

BIN
Mockup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

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,8 +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)
--- ---
## 📂 Struktur Proyek (Contoh) ## 📂 Mockup
![mockup](Mockup.png)
---
## 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/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
---
## 📄 Identitas Pengembang
- **Nama** : Faris Naufal Priatna
- **NPM** : 202310715123
- **Jenis Proyek** : Tugas Akhir EAS
- **Bantuan Pengembangan** : AI ChatGPT
Output Aplikasi yang sudah jadi

View File

@ -45,11 +45,14 @@ dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
// Location (GPS)
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,27 +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">
<!-- ===== 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="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

@ -1,47 +1,284 @@
package id.ac.ubharajaya.sistemakademik package id.ac.ubharajaya.sistemakademik
import android.Manifest
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.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
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.compose.foundation.layout.fillMaxSize import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.Image
import androidx.compose.material3.Scaffold import androidx.compose.foundation.background
import androidx.compose.material3.Text import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.tooling.preview.Preview 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 id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONObject
// ================== UTILS ==================
fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
thread {
try {
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/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 = 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",
Toast.LENGTH_SHORT
).show()
}
conn.disconnect()
} catch (_: Exception) {
context.runOnUiThread {
Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show()
}
}
}
}
// ================== MAIN ACTIVITY ==================
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
SistemAkademikTheme { SistemAkademikTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting( AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this)
name = "Android",
modifier = Modifier.padding(innerPadding)
)
} }
} }
} }
} }
} }
// ================== COMPOSABLE UI ==================
@Composable @Composable
fun Greeting(name: String, modifier: Modifier = Modifier) { fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
Text( val context = LocalContext.current
text = "Hello $name!", val session = remember { SessionManager(context) }
modifier = modifier
)
}
@Preview(showBackground = true) var lokasi by remember { mutableStateOf("Koordinat: -") }
@Composable var latitude by remember { mutableStateOf<Double?>(null) }
fun GreetingPreview() { var longitude by remember { mutableStateOf<Double?>(null) }
SistemAkademikTheme { var foto by remember { mutableStateOf<Bitmap?>(null) }
Greeting("Android") var waktuAbsensi by remember { mutableStateOf<String?>(null) }
val absensiList = remember { mutableStateListOf<Absensi>() }
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
// ================== 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 ->
if (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}"
} else {
lokasi = "Lokasi tidak tersedia"
}
}
.addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
}
} else {
Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
}
}
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 {
@Suppress("DEPRECATION")
result.data?.extras?.getParcelable("data")
}
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)
} 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()
.background(backgroundColor)
.padding(24.dp)
) {
Text(
text = "Absensi Akademik",
style = MaterialTheme.typography.titleLarge,
color = primaryColor
)
Spacer(modifier = Modifier.height(16.dp))
// 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(),
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
) { Text("Ambil Foto") }
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
if (latitude != null && longitude != null && foto != null) {
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, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show()
}
},
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("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

225
n8n-workflow-EAS.json Normal file
View File

@ -0,0 +1,225 @@
{
"name": "EAS",
"nodes": [
{
"parameters": {
"method": "POST",
"url": "https://ntfy.ubharajaya.ac.id/EAS",
"sendBody": true,
"contentType": "raw",
"body": "=Absensi: {{ $json.body.nama }} NPM: {{ $json.body.npm }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
-272,
-240
],
"id": "83504eec-6d20-46d7-9ea1-509ae4ee8660",
"name": "NTFY HTTP Request"
},
{
"parameters": {
"httpMethod": "POST",
"path": "23c6993d-1792-48fb-ad1c-ffc78a3e6254",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-864,
-112
],
"id": "9ed3d2db-2d50-40b5-8408-7404edd48442",
"name": "Webhook Absensi",
"webhookId": "23c6993d-1792-48fb-ad1c-ffc78a3e6254"
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "Absensi",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"latitude": "={{ $json.body.latitude }}",
"longitude": "={{ $json.body.longitude }}",
"timestamp": "={{ $json.body.timestamp }}",
"foto_base64": "={{ $json.body.foto_base64 }}",
"nama": "={{ $json.body.nama }}",
"npm": "={{ $json.body.npm }}"
},
"matchingColumns": [],
"schema": [
{
"id": "timestamp",
"displayName": "timestamp",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "npm",
"displayName": "npm",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "nama",
"displayName": "nama",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "latitude",
"displayName": "latitude",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "longitude",
"displayName": "longitude",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "photo",
"displayName": "photo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "status",
"displayName": "status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "foto_base64",
"displayName": "foto_base64",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-272,
-32
],
"id": "cd83a9fa-ea00-4a20-aa31-846bfe044aeb",
"name": "Append row in sheet",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "hNVNhkTQbqkJ3C56",
"name": "Google Sheets account"
}
}
},
{
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-528,
-240
],
"id": "4ed9edf6-4562-41b6-afd0-89c96991454a",
"name": "Code in JavaScript"
}
],
"pinData": {},
"connections": {
"Webhook Absensi": {
"main": [
[
{
"node": "Append row in sheet",
"type": "main",
"index": 0
},
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"NTFY HTTP Request": {
"main": [
[]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "NTFY HTTP Request",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"versionId": "49466b31-67ce-49b7-af37-33cd28d7092d",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "b8ffac81bb85d267c3296e074b3e692ecef11caeef79fa72af892085548f350a"
},
"id": "E_gxZpNrN3G5ibejHcTFS",
"tags": []
}