commit e199d75042175b149dde70892c7de7368291a823
Author: 202310715277-LALU-MUHAMMAD-ANGGANA-SUBHAN <202310715277@mhs.ubharajaya.ac.id>
Date: Wed Jan 14 21:24:01 2026 +0700
Init project with jecpack compose
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..836cb8b
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Sistem Akademik
\ No newline at end of file
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b77ef91
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..b2c751a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml
new file mode 100644
index 0000000..539e3b8
--- /dev/null
+++ b/.idea/studiobot.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mockup.png b/Mockup.png
new file mode 100644
index 0000000..4690bf3
Binary files /dev/null and b/Mockup.png differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bdb0074
--- /dev/null
+++ b/README.md
@@ -0,0 +1,269 @@
+# π Sistem Absensi Akademik - Universitas Bhayangkara Jakarta Raya
+
+## π Deskripsi
+Aplikasi mobile berbasis Android untuk sistem absensi mahasiswa dengan validasi lokasi GPS dan verifikasi foto.
+
+---
+
+## π Fitur Utama
+
+### β
Sudah Diimplementasikan:
+- π **Login Pengguna** - Autentikasi mahasiswa dengan NPM dan Password
+- π **Pengambilan Koordinat Lokasi** - GPS Latitude & Longitude otomatis
+- π« **Validasi Lokasi Absensi** - Validasi radius area kampus (100 meter)
+- πΈ **Pengambilan Foto Mahasiswa** - Foto wajib saat absensi
+- π **Pencatatan Waktu Absensi** - Timestamp otomatis
+- π **Riwayat Kehadiran** - History absensi mahasiswa
+- β οΈ **Notifikasi Ditolak** - Alert jika lokasi di luar radius atau error
+
+---
+
+## π Struktur File
+
+```
+id.ac.ubharajaya.sistemakademik/
+βββ LoginActivity.kt # Halaman login
+βββ MainActivity.kt # Halaman absensi utama
+βββ HistoryActivity.kt # Halaman riwayat kehadiran
+βββ utils/
+ βββ SessionManager.kt # Manajemen session login
+```
+
+---
+
+## π§ Setup & Instalasi
+
+### 1. **Buat File-File Berikut:**
+
+**Struktur folder:**
+```
+app/src/main/java/id/ac/ubharajaya/sistemakademik/
+βββ LoginActivity.kt
+βββ MainActivity.kt
+βββ HistoryActivity.kt
+βββ utils/
+ βββ SessionManager.kt
+```
+
+### 2. **Update AndroidManifest.xml**
+Ganti file AndroidManifest.xml dengan yang sudah disediakan.
+
+### 3. **Dependencies (build.gradle)**
+Pastikan dependencies berikut sudah ada di `build.gradle.kts (Module: app)`:
+
+```kotlin
+dependencies {
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+
+ // Google Location Services
+ implementation("com.google.android.gms:play-services-location:21.1.0")
+}
+```
+
+### 4. **Koordinat Kampus**
+Di file `MainActivity.kt`, ganti koordinat kampus sesuai lokasi asli:
+
+```kotlin
+val kampusLat = -6.200000 // Latitude kampus
+val kampusLon = 106.816666 // Longitude kampus
+val maxRadius = 100.0 // Radius dalam meter
+```
+
+---
+
+## π Endpoint API yang Dibutuhkan
+
+### 1. **Login Endpoint**
+```
+POST https://n8n.lab.ubharajaya.ac.id/webhook/login-mahasiswa
+
+Request Body:
+{
+ "npm": "12345",
+ "password": "password123"
+}
+
+Response Success:
+{
+ "success": true,
+ "nama": "Arif R D",
+ "prodi": "Teknik Informatika"
+}
+
+Response Failed:
+{
+ "success": false,
+ "message": "NPM atau Password salah"
+}
+```
+
+### 2. **Absensi Endpoint** (Sudah Ada)
+```
+POST https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254
+
+Request Body:
+{
+ "npm": "12345",
+ "nama": "Arif R D",
+ "latitude": -6.200000,
+ "longitude": 106.816666,
+ "jarak_dari_kampus": 50,
+ "timestamp": 1704067200000,
+ "waktu": "14/01/2026 10:30:00",
+ "foto_base64": "base64_encoded_image"
+}
+```
+
+### 3. **Riwayat Absensi Endpoint**
+```
+GET https://n8n.lab.ubharajaya.ac.id/webhook/riwayat-absensi?npm=12345
+
+Response:
+[
+ {
+ "id": "1",
+ "npm": "12345",
+ "nama": "Arif R D",
+ "waktu": "14/01/2026 10:30:00",
+ "latitude": -6.200000,
+ "longitude": 106.816666,
+ "jarak_dari_kampus": 50,
+ "foto_base64": "..."
+ }
+]
+```
+
+---
+
+## π± Cara Menggunakan Aplikasi
+
+### **Login**
+1. Buka aplikasi
+2. Masukkan NPM dan Password
+3. Klik tombol "Login"
+4. Jika berhasil, akan masuk ke halaman absensi
+
+### **Melakukan Absensi**
+1. Pastikan GPS aktif
+2. Aplikasi otomatis mengambil lokasi
+3. Klik "Ambil Foto" untuk foto selfie
+4. Klik "Kirim Absensi"
+5. Sistem akan validasi:
+ - β
**Valid** jika dalam radius 100m dari kampus
+ - β **Ditolak** jika di luar radius
+
+### **Lihat Riwayat**
+1. Klik icon History (jam) di toolbar
+2. Lihat daftar riwayat absensi
+3. Status ditampilkan:
+ - β
Valid (hijau)
+ - β Ditolak (merah)
+
+### **Logout**
+- Klik icon Logout di toolbar
+
+---
+
+## π Keamanan
+
+- **Session Management**: Login session tersimpan di SharedPreferences
+- **Auto Login**: Otomatis login jika session masih aktif
+- **Location Validation**: Validasi radius GPS otomatis
+- **Photo Required**: Foto wajib untuk setiap absensi
+
+---
+
+## βοΈ Pengaturan Validasi
+
+**Validasi Radius (di MainActivity.kt):**
+```kotlin
+val maxRadius = 100.0 // Ubah sesuai kebutuhan (dalam meter)
+```
+
+**Koordinat Kampus:**
+```kotlin
+val kampusLat = -6.200000 // Ganti dengan koordinat kampus
+val kampusLon = 106.816666 // Ganti dengan koordinat kampus
+```
+
+---
+
+## π Troubleshooting
+
+### **GPS Tidak Terdeteksi**
+- Pastikan GPS/Location aktif di HP
+- Coba klik "Refresh Lokasi"
+- Pastikan permission lokasi sudah diberikan
+
+### **Absensi Ditolak**
+- Cek jarak dari kampus di pesan error
+- Pastikan berada dalam radius yang ditentukan
+- Koordinat kampus mungkin salah, cek lagi
+
+### **Login Gagal**
+- Cek koneksi internet
+- Pastikan server n8n aktif
+- Cek endpoint login sudah benar
+
+### **Foto Tidak Muncul**
+- Pastikan permission kamera sudah diberikan
+- Coba ambil foto ulang
+
+---
+
+## π Testing Checklist
+
+- [ ] Login dengan NPM dan Password valid
+- [ ] Login gagal dengan kredensial salah
+- [ ] Auto-login setelah restart app
+- [ ] Absensi berhasil dalam radius kampus
+- [ ] Absensi ditolak di luar radius
+- [ ] Refresh lokasi berfungsi
+- [ ] Ambil foto berfungsi
+- [ ] Preview foto tampil
+- [ ] Riwayat absensi tampil
+- [ ] Status valid/ditolak sesuai
+- [ ] Logout berfungsi
+
+---
+
+## π― Future Improvements
+
+- [ ] Face Recognition untuk validasi foto
+- [ ] Notifikasi push untuk jadwal absensi
+- [ ] QR Code scanning
+- [ ] Offline mode dengan sync
+- [ ] Export riwayat ke PDF
+- [ ] Multi-language support
+- [ ] Dark mode
+- [ ] Fingerprint authentication
+
+---
+
+## π¨βπ» Developer
+
+**Arif R D**
+NPM: 12345
+Universitas Bhayangkara Jakarta Raya
+
+---
+
+## π License
+
+Copyright Β© 2026 Universitas Bhayangkara Jakarta Raya
+
+---
+
+## π Support
+
+Jika ada pertanyaan atau bug, hubungi tim IT:
+- Email: it@ubharajaya.ac.id
+- Phone: (021) xxx-xxxx
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..610d8db
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "id.ac.ubharajaya.sistemakademik"
+ compileSdk {
+ version = release(36)
+ }
+
+ defaultConfig {
+ applicationId = "id.ac.ubharajaya.sistemakademik"
+ minSdk = 28
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.compose.material:material-icons-extended:1.6.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+
+ // Google Location Services
+ implementation("com.google.android.gms:play-services-location:21.1.0")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/id/ac/ubharajaya/sistemakademik/ExampleInstrumentedTest.kt b/app/src/androidTest/java/id/ac/ubharajaya/sistemakademik/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..ef22af9
--- /dev/null
+++ b/app/src/androidTest/java/id/ac/ubharajaya/sistemakademik/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package id.ac.ubharajaya.sistemakademik
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("id.ac.ubharajaya.sistemakademik", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a403d36
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/HistoryActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/HistoryActivity.kt
new file mode 100644
index 0000000..57b6e05
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/HistoryActivity.kt
@@ -0,0 +1,176 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
+import id.ac.ubharajaya.sistemakademik.utils.SessionManager
+import kotlinx.coroutines.launch
+import org.json.JSONArray
+import java.net.HttpURLConnection
+import java.net.URL
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.TimeZone
+
+/* ================= MODEL ================= */
+data class RiwayatAbsensi(
+ val waktu: String,
+ val lokasi: String,
+ val jarak: Int,
+ val status: String
+)
+
+/* ================= ACTIVITY ================= */
+class HistoryActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ setContent {
+ SistemAkademikTheme {
+ HistoryScreen(this)
+ }
+ }
+ }
+}
+
+/* ================= UI ================= */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HistoryScreen(activity: HistoryActivity) {
+ val context = LocalContext.current
+ val sessionManager = remember { SessionManager(context) }
+ val npm = sessionManager.getNpm()
+
+ var list by remember { mutableStateOf>(emptyList()) }
+ var loading by remember { mutableStateOf(true) }
+ val scope = rememberCoroutineScope()
+
+ fun loadRiwayat() {
+ loading = true
+ scope.launch {
+ try {
+ val url =
+ URL("https://n8n.lab.ubharajaya.ac.id/webhook/riwayat-absensi?npm=$npm")
+ val conn = url.openConnection() as HttpURLConnection
+ conn.requestMethod = "GET"
+ conn.connectTimeout = 10000
+ conn.readTimeout = 10000
+
+ if (conn.responseCode == 200) {
+ val json = conn.inputStream.bufferedReader().readText()
+ val arr = JSONArray(json)
+
+ list = List(arr.length()) { i ->
+ val o = arr.getJSONObject(i)
+ RiwayatAbsensi(
+ waktu = formatWaktuApi(o.getString("waktu")),
+ lokasi = "${o.getDouble("latitude")}, ${o.getDouble("longitude")}",
+ jarak = o.getInt("jarak_dari_kampus"),
+ status = "Valid"
+ )
+ }
+ } else {
+ list = dummyRiwayat()
+ }
+ } catch (e: Exception) {
+ list = dummyRiwayat()
+ }
+ loading = false
+ }
+ }
+
+ LaunchedEffect(Unit) { loadRiwayat() }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Riwayat Kehadiran") },
+ navigationIcon = {
+ IconButton(onClick = { activity.finish() }) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Kembali")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ if (loading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ contentAlignment = androidx.compose.ui.Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(list) { item ->
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer
+ )
+ ) {
+ Column(Modifier.padding(16.dp)) {
+ Text(
+ text = "π ${item.waktu}",
+ fontWeight = FontWeight.Bold
+ )
+ Text("π ${item.lokasi}")
+ Text("π ${item.jarak} m")
+ Text("β
${item.status}")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/* ================= FORMAT WAKTU ================= */
+fun formatWaktuApi(waktuApi: String): String {
+ return try {
+ val inputFormat =
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
+ inputFormat.timeZone = TimeZone.getTimeZone("UTC")
+
+ val outputFormat = SimpleDateFormat(
+ "EEEE, dd MMMM yyyy HH:mm:ss",
+ Locale("id", "ID")
+ )
+
+ val date = inputFormat.parse(waktuApi)
+ outputFormat.format(date!!)
+ } catch (e: Exception) {
+ waktuApi
+ }
+}
+
+/* ================= DUMMY FALLBACK ================= */
+fun dummyRiwayat() = listOf(
+ RiwayatAbsensi(
+ "Selasa, 14 Januari 2025 17:30:15",
+ "-6.2001, 106.8167",
+ 45,
+ "Valid"
+ )
+)
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt
new file mode 100644
index 0000000..1d9b50c
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt
@@ -0,0 +1,152 @@
+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.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material.icons.filled.Person
+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.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
+import id.ac.ubharajaya.sistemakademik.utils.SessionManager
+
+class LoginActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ val sessionManager = SessionManager(this)
+ if (sessionManager.isLoggedIn()) {
+ startActivity(Intent(this, MainActivity::class.java))
+ finish()
+ return
+ }
+
+ setContent {
+ SistemAkademikTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ LoginScreen(
+ modifier = Modifier.padding(innerPadding),
+ activity = this
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun LoginScreen(
+ modifier: Modifier = Modifier,
+ activity: LoginActivity
+) {
+ val context = LocalContext.current
+ val sessionManager = remember { SessionManager(context) }
+
+ var npm by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+
+ fun performLogin() {
+ if (npm.isBlank() || password.isBlank()) {
+ Toast.makeText(context, "NPM dan Password harus diisi", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ // LOGIN MANUAL (FIX)
+ if (npm == "202310715277" && password == "12345") {
+
+ val nama = "Lalu Muhammad Anggana Subhan"
+ val prodi = "Informatika"
+
+ sessionManager.saveLoginSession(npm, nama, prodi)
+
+ Toast.makeText(context, "Login berhasil", Toast.LENGTH_SHORT).show()
+ activity.startActivity(Intent(context, MainActivity::class.java))
+ activity.finish()
+
+ } else {
+ Toast.makeText(context, "NPM atau Password salah", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("π", fontSize = 64.sp)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = "Sistem Absensi",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+
+ Text(
+ text = "Universitas Bhayangkara Jakarta Raya",
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ OutlinedTextField(
+ value = npm,
+ onValueChange = { npm = it },
+ label = { Text("NPM") },
+ leadingIcon = {
+ Icon(Icons.Default.Person, contentDescription = null)
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ OutlinedTextField(
+ value = password,
+ onValueChange = { password = it },
+ label = { Text("Password") },
+ leadingIcon = {
+ Icon(Icons.Default.Lock, contentDescription = null)
+ },
+ visualTransformation = PasswordVisualTransformation(),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Button(
+ onClick = { performLogin() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ ) {
+ Text("Login", fontSize = 16.sp)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
new file mode 100644
index 0000000..595d878
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
@@ -0,0 +1,441 @@
+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.location.Location
+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.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.List
+import androidx.compose.material.icons.filled.ExitToApp
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+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.utils.SessionManager
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.text.SimpleDateFormat
+import java.util.*
+import kotlin.concurrent.thread
+import kotlin.math.*
+
+/* ================= UTIL ================= */
+
+fun bitmapToBase64(bitmap: Bitmap): String {
+ val outputStream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+ return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
+}
+
+// Hitung jarak antara 2 koordinat (dalam meter)
+fun hitungJarak(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
+ val R = 6371000.0 // Radius bumi dalam meter
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLon = Math.toRadians(lon2 - lon1)
+ val a = sin(dLat / 2) * sin(dLat / 2) +
+ cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
+ sin(dLon / 2) * sin(dLon / 2)
+ val c = 2 * atan2(sqrt(a), sqrt(1 - a))
+ return R * c
+}
+
+fun kirimKeN8n(
+ context: ComponentActivity,
+ npm: String,
+ nama: String,
+ latitude: Double,
+ longitude: Double,
+ foto: Bitmap,
+ onResult: (Boolean, String) -> Unit
+) {
+ thread {
+ try {
+ // Koordinat kampus (contoh: ganti dengan koordinat kampus asli)
+ val kampusLat = -6.200000 // Ganti dengan koordinat kampus
+ val kampusLon = 106.816666 // Ganti dengan koordinat kampus
+ val maxRadius = 99999999.0 // Radius 100 meter
+
+ val jarak = hitungJarak(latitude, longitude, kampusLat, kampusLon)
+
+ // Validasi lokasi
+ if (jarak > maxRadius) {
+ context.runOnUiThread {
+ onResult(false, "β οΈ Lokasi Anda ${jarak.toInt()}m dari kampus. Absensi harus dilakukan dalam radius ${maxRadius.toInt()}m!")
+ }
+ return@thread
+ }
+
+ 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
+ conn.connectTimeout = 15000
+ conn.readTimeout = 15000
+
+ val timestamp = System.currentTimeMillis()
+ val dateFormat = SimpleDateFormat(
+ "EEEE, dd MMMM yyyy HH:mm:ss",
+ Locale("id", "ID")
+ )
+ val waktu = dateFormat.format(Date(timestamp))
+
+ val json = JSONObject().apply {
+ put("npm", npm)
+ put("nama", nama)
+ put("latitude", latitude)
+ put("longitude", longitude)
+ put("jarak_dari_kampus", jarak.toInt())
+ put("timestamp", timestamp)
+ put("waktu", waktu)
+ put("foto_base64", bitmapToBase64(foto))
+ }
+
+ conn.outputStream.use {
+ it.write(json.toString().toByteArray())
+ }
+
+ val responseCode = conn.responseCode
+
+ context.runOnUiThread {
+ if (responseCode == 200) {
+ onResult(true, "β
Absensi berhasil dicatat!\nJarak dari kampus: ${jarak.toInt()}m")
+ } else {
+ onResult(false, "β Server menolak absensi (Error: $responseCode)")
+ }
+ }
+
+ conn.disconnect()
+
+ } catch (e: Exception) {
+ context.runOnUiThread {
+ onResult(false, "β Gagal kirim ke server: ${e.message}")
+ }
+ }
+ }
+}
+
+/* ================= ACTIVITY ================= */
+
+class MainActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ // Cek session login
+ val sessionManager = SessionManager(this)
+ if (!sessionManager.isLoggedIn()) {
+ startActivity(Intent(this, LoginActivity::class.java))
+ finish()
+ return
+ }
+
+ setContent {
+ SistemAkademikTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ AbsensiScreen(
+ modifier = Modifier.padding(innerPadding),
+ activity = this
+ )
+ }
+ }
+ }
+ }
+}
+
+/* ================= UI ================= */
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AbsensiScreen(
+ modifier: Modifier = Modifier,
+ activity: ComponentActivity
+) {
+ val context = LocalContext.current
+ val sessionManager = remember { SessionManager(context) }
+
+ val npm = sessionManager.getNpm()
+ val nama = sessionManager.getNama()
+ val prodi = sessionManager.getProdi()
+
+ var lokasi by remember { mutableStateOf("π Mengambil lokasi...") }
+ var latitude by remember { mutableStateOf(null) }
+ var longitude by remember { mutableStateOf(null) }
+ var foto by remember { mutableStateOf(null) }
+ var isLoading by remember { mutableStateOf(false) }
+ var statusMessage by remember { mutableStateOf("") }
+ var waktuSekarang by remember { mutableStateOf(getWaktuRealtime()) }
+ LaunchedEffect(Unit) {
+ while (true) {
+ waktuSekarang = getWaktuRealtime()
+ kotlinx.coroutines.delay(1000)
+ }
+ }
+
+
+
+ val fusedLocationClient = remember {
+ LocationServices.getFusedLocationProviderClient(context)
+ }
+
+ fun refreshLocation() {
+ if (ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ fusedLocationClient.lastLocation
+ .addOnSuccessListener { location: Location? ->
+ if (location != null) {
+ latitude = location.latitude
+ longitude = location.longitude
+ lokasi = "π Lat: ${String.format("%.6f", location.latitude)}\nπ Lon: ${String.format("%.6f", location.longitude)}"
+ } else {
+ lokasi = "β οΈ Lokasi tidak tersedia"
+ }
+ }
+ .addOnFailureListener {
+ lokasi = "β Gagal mengambil lokasi"
+ }
+ }
+ }
+
+ /* ===== Permission Lokasi ===== */
+
+ val locationPermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) {
+ refreshLocation()
+ } else {
+ 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")
+ 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()
+ }
+ }
+
+ /* ===== Request Awal ===== */
+
+ LaunchedEffect(Unit) {
+ locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
+ }
+
+ /* ===== UI ===== */
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Absensi Akademik") },
+ actions = {
+ IconButton(onClick = {
+ val intent = Intent(context, HistoryActivity::class.java)
+ context.startActivity(intent)
+ }) {
+ Icon(Icons.Filled.List, contentDescription = "Riwayat")
+ }
+ IconButton(onClick = {
+ sessionManager.logout()
+ val intent = Intent(context, LoginActivity::class.java)
+ context.startActivity(intent)
+ (context as? ComponentActivity)?.finish()
+ }) {
+ Icon(Icons.Filled.ExitToApp, contentDescription = "Logout")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(
+ text = nama,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ Text(text = "NPM: $npm")
+ Text(text = "Prodi: $prodi")
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "π $waktuSekarang",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+
+
+ // Lokasi Info
+ Card(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(
+ text = "Lokasi Anda",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = lokasi)
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(
+ onClick = { refreshLocation() },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("π Refresh Lokasi")
+ }
+ }
+ }
+
+ // Foto Preview
+ if (foto != null) {
+ Card(
+ modifier = Modifier
+ .size(150.dp)
+ ) {
+ Image(
+ bitmap = foto!!.asImageBitmap(),
+ contentDescription = "Foto Absensi",
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+
+ // Ambil Foto Button
+ Button(
+ onClick = {
+ cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text("πΈ Ambil Foto")
+ }
+
+ // Kirim Absensi Button
+ Button(
+ onClick = {
+ if (latitude != null && longitude != null && foto != null) {
+ isLoading = true
+ statusMessage = "β³ Mengirim absensi..."
+
+ kirimKeN8n(
+ activity,
+ npm,
+ nama,
+ latitude!!,
+ longitude!!,
+ foto!!
+ ) { success, message ->
+ isLoading = false
+ statusMessage = message
+ Toast.makeText(context, message, Toast.LENGTH_LONG).show()
+
+ if (success) {
+ foto = null // Reset foto setelah sukses
+ }
+ }
+ } else {
+ Toast.makeText(
+ context,
+ "β οΈ Lokasi atau foto belum lengkap",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !isLoading
+ ) {
+ if (isLoading) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ color = MaterialTheme.colorScheme.onPrimary
+ )
+ } else {
+ Text("β
Kirim Absensi")
+ }
+ }
+
+ // Status Message
+ if (statusMessage.isNotEmpty()) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = if (statusMessage.contains("β
"))
+ MaterialTheme.colorScheme.tertiaryContainer
+ else
+ MaterialTheme.colorScheme.errorContainer
+ )
+ ) {
+ Text(
+ text = statusMessage,
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ }
+ }
+ }
+}
+fun getWaktuRealtime(): String {
+ val sdf = SimpleDateFormat(
+ "EEEE, dd MMMM yyyy HH:mm:ss",
+ Locale("id", "ID")
+ )
+ return sdf.format(Date())
+}
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Color.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Color.kt
new file mode 100644
index 0000000..c75702c
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package id.ac.ubharajaya.sistemakademik.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Theme.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Theme.kt
new file mode 100644
index 0000000..1b2db88
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package id.ac.ubharajaya.sistemakademik.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun SistemAkademikTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Type.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Type.kt
new file mode 100644
index 0000000..e2982e7
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package id.ac.ubharajaya.sistemakademik.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/utils/SessionManager.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/utils/SessionManager.kt
new file mode 100644
index 0000000..9c0ecf5
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/utils/SessionManager.kt
@@ -0,0 +1,46 @@
+package id.ac.ubharajaya.sistemakademik.utils
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class SessionManager(context: Context) {
+ private val prefs: SharedPreferences =
+ context.getSharedPreferences("AbsensiSession", Context.MODE_PRIVATE)
+
+ companion object {
+ private const val KEY_IS_LOGGED_IN = "isLoggedIn"
+ private const val KEY_NPM = "npm"
+ private const val KEY_NAMA = "nama"
+ private const val KEY_PRODI = "prodi"
+ }
+
+ fun saveLoginSession(npm: String, nama: String, prodi: String) {
+ prefs.edit().apply {
+ putBoolean(KEY_IS_LOGGED_IN, true)
+ putString(KEY_NPM, npm)
+ putString(KEY_NAMA, nama)
+ putString(KEY_PRODI, prodi)
+ apply()
+ }
+ }
+
+ fun isLoggedIn(): Boolean {
+ return prefs.getBoolean(KEY_IS_LOGGED_IN, false)
+ }
+
+ fun getNpm(): String {
+ return prefs.getString(KEY_NPM, "") ?: ""
+ }
+
+ fun getNama(): String {
+ return prefs.getString(KEY_NAMA, "") ?: ""
+ }
+
+ fun getProdi(): String {
+ return prefs.getString(KEY_PRODI, "") ?: ""
+ }
+
+ fun logout() {
+ prefs.edit().clear().apply()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..de92dbc
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Sistem Akademik
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..d515270
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/id/ac/ubharajaya/sistemakademik/ExampleUnitTest.kt b/app/src/test/java/id/ac/ubharajaya/sistemakademik/ExampleUnitTest.kt
new file mode 100644
index 0000000..6bdefc7
--- /dev/null
+++ b/app/src/test/java/id/ac/ubharajaya/sistemakademik/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package id.ac.ubharajaya.sistemakademik
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..952b930
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..7d255c8
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,32 @@
+[versions]
+agp = "8.13.2"
+kotlin = "2.0.21"
+coreKtx = "1.17.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+lifecycleRuntimeKtx = "2.9.4"
+activityCompose = "1.11.0"
+composeBom = "2024.09.00"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..6c01a8b
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Mon Jan 12 10:23:56 WIB 2026
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..ef07e01
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright Β© 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions Β«$varΒ», Β«${var}Β», Β«${var:-default}Β», Β«${var+SET}Β»,
+# Β«${var#prefix}Β», Β«${var%suffix}Β», and Β«$( cmd )Β»;
+# * compound commands having a testable exit status, especially Β«caseΒ»;
+# * various built-in commands including Β«commandΒ», Β«setΒ», and Β«ulimitΒ».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/n8n-workflow-EAS.json b/n8n-workflow-EAS.json
new file mode 100644
index 0000000..cf26602
--- /dev/null
+++ b/n8n-workflow-EAS.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..99b1b95
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Sistem Akademik"
+include(":app")
+
\ No newline at end of file