Compare commits
10 Commits
49d9ce215a
...
741a6a8fd8
| Author | SHA1 | Date | |
|---|---|---|---|
| 741a6a8fd8 | |||
| 926d3e0a14 | |||
| cddaf87d88 | |||
| c9cc99baa2 | |||
| 2a00b834c7 | |||
| 4d7fc844e2 | |||
| d4d1b27209 | |||
| 3e66ebcf9e | |||
| 46b74d7099 | |||
| cbe7e50b96 |
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="PLACEHOLDER" />
|
||||||
|
<option name="mobileSdkAppId" value="" />
|
||||||
|
<option name="projectId" value="" />
|
||||||
|
<option name="projectNumber" value="" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -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-13T12:44:47.478355400Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=y5zxozrgyhcafun7" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
21
.idea/deviceManager.xml
generated
Normal file
21
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Type" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="API" />
|
||||||
|
<option name="order" value="DESCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@ -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>
|
||||||
|
|||||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@ -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
6
.idea/studiobot.xml
generated
Normal 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
BIN
Mockup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
96
README.md
96
README.md
@ -1,3 +1,95 @@
|
|||||||
# Aplikasi Sistem Akademik Mobile
|
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
||||||
|
|
||||||
Untuk
|
## 📌 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**.
|
||||||
|
|
||||||
|
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
|
||||||
|
1. Berada pada **lokasi yang telah ditentukan**, dan
|
||||||
|
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Tujuan Proyek
|
||||||
|
- Mengimplementasikan **Location-Based Service (LBS)** pada aplikasi mobile
|
||||||
|
- Mengintegrasikan **kamera perangkat** untuk dokumentasi absensi
|
||||||
|
- Mencegah kecurangan absensi (titip absen)
|
||||||
|
- Mengembangkan aplikasi mobile akademik berbasis Android
|
||||||
|
- Melatih kemampuan perancangan dan implementasi aplikasi mobile
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Fitur Utama
|
||||||
|
- 🔐 **Login Pengguna (Mahasiswa)**
|
||||||
|
- 📍 **Pengambilan Koordinat Lokasi (Latitude & Longitude)**
|
||||||
|
- 🏫 **Validasi Lokasi Absensi (Radius Area)**
|
||||||
|
- 📸 **Pengambilan Foto Mahasiswa Saat Absensi**
|
||||||
|
- 🕒 **Pencatatan Waktu Absensi**
|
||||||
|
- 📄 **Riwayat Kehadiran Mahasiswa**
|
||||||
|
- ⚠️ **Notifikasi Absensi Ditolak jika Tidak Valid**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗺️ Mekanisme Absensi Berbasis Lokasi dan Foto
|
||||||
|
1. Mahasiswa melakukan **login**
|
||||||
|
2. Memilih menu **Absensi**
|
||||||
|
3. Sistem meminta:
|
||||||
|
- Izin **akses lokasi**
|
||||||
|
- Izin **akses kamera**
|
||||||
|
4. Aplikasi mengambil:
|
||||||
|
- 📍 **Koordinat lokasi mahasiswa**
|
||||||
|
- 📸 **Foto mahasiswa secara real-time**
|
||||||
|
5. Sistem melakukan validasi:
|
||||||
|
- Lokasi berada dalam **radius absensi**
|
||||||
|
- Foto berhasil diambil
|
||||||
|
6. Jika valid → **Absensi berhasil**
|
||||||
|
7. Jika tidak valid → **Absensi ditolak**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 Pengambilan Foto Saat Absensi
|
||||||
|
- Foto diambil menggunakan **kamera depan (selfie)**
|
||||||
|
- Foto hanya dapat diambil **saat proses absensi**
|
||||||
|
- Foto disimpan sebagai **bukti kehadiran**
|
||||||
|
- Foto dapat digunakan untuk:
|
||||||
|
- Verifikasi manual oleh dosen
|
||||||
|
- Dokumentasi akademik
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Teknologi yang Digunakan
|
||||||
|
- **Platform** : Android
|
||||||
|
- **Bahasa Pemrograman** : Kotlin / Java
|
||||||
|
- **Location Service** :
|
||||||
|
- Google Maps API
|
||||||
|
- Fused Location Provider
|
||||||
|
- **Camera API** : CameraX / Camera2
|
||||||
|
- **Database** : Firebase / SQLite / MySQL
|
||||||
|
- **Storage** : Firebase Storage / Local Storage
|
||||||
|
- **IDE** : Android Studio
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Izin Aplikasi (Permissions)
|
||||||
|
Aplikasi memerlukan izin berikut:
|
||||||
|
- `ACCESS_FINE_LOCATION`
|
||||||
|
- `ACCESS_COARSE_LOCATION`
|
||||||
|
- `CAMERA`
|
||||||
|
- `INTERNET`
|
||||||
|
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Mockup
|
||||||
|

|
||||||
|
|
||||||
|
## Catatan:
|
||||||
|
- Kembangkan project dari starter yang sudah disediakan, tidak membuat dari awal.
|
||||||
|
- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS.
|
||||||
|
|
||||||
|
## Pengecekan:
|
||||||
|
- https://ntfy.ubharajaya.ac.id/EAS
|
||||||
|
- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/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
|
||||||
@ -6,9 +6,7 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "id.ac.ubharajaya.sistemakademik"
|
namespace = "id.ac.ubharajaya.sistemakademik"
|
||||||
compileSdk {
|
compileSdk = 36
|
||||||
version = release(36)
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "id.ac.ubharajaya.sistemakademik"
|
applicationId = "id.ac.ubharajaya.sistemakademik"
|
||||||
@ -44,12 +42,20 @@ android {
|
|||||||
dependencies {
|
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(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)
|
||||||
|
|
||||||
|
// Activity, Navigation, and Icons
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
|
|
||||||
|
// Location (GPS)
|
||||||
|
implementation(libs.play.services.location)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
<?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">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
<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"/>
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@ -1,17 +1,99 @@
|
|||||||
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.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.util.Base64
|
||||||
|
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.layout.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.google.android.gms.location.LocationServices
|
||||||
|
import com.google.android.gms.location.Priority
|
||||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
|
import org.json.JSONArray
|
||||||
|
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.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/* ================= DATA CLASS ================= */
|
||||||
|
data class AttendanceRecord(val timestamp: Long, val status: String)
|
||||||
|
|
||||||
|
/* ================= UTILS ================= */
|
||||||
|
|
||||||
|
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream)
|
||||||
|
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
object LocationUtils {
|
||||||
|
private const val KAMPUS_LATITUDE = -6.2576
|
||||||
|
private const val KAMPUS_LONGITUDE = 106.9746
|
||||||
|
private const val MAX_RADIUS_METERS = 100.0 // Radius 100 meter
|
||||||
|
|
||||||
|
fun isWithinCampusRadius(lat: Double, lon: Double): Boolean {
|
||||||
|
val earthRadius = 6371000.0 // in meters
|
||||||
|
val dLat = Math.toRadians(lat - KAMPUS_LATITUDE)
|
||||||
|
val dLon = Math.toRadians(lon - KAMPUS_LONGITUDE)
|
||||||
|
val lat1 = Math.toRadians(KAMPUS_LATITUDE)
|
||||||
|
val lat2 = Math.toRadians(lat)
|
||||||
|
|
||||||
|
val a = sin(dLat / 2) * sin(dLat / 2) +
|
||||||
|
sin(dLon / 2) * sin(dLon / 2) * cos(lat1) * cos(lat2)
|
||||||
|
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||||
|
|
||||||
|
return (earthRadius * c) <= MAX_RADIUS_METERS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================= MAIN ACTIVITY ================= */
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -19,29 +101,506 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
AppNavigation(activity = this)
|
||||||
Greeting(
|
}
|
||||||
name = "Android",
|
}
|
||||||
modifier = Modifier.padding(innerPadding)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= NAVIGATION ================= */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppNavigation(activity: ComponentActivity) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
NavHost(navController = navController, startDestination = "login") {
|
||||||
|
composable("login") {
|
||||||
|
LoginScreen(navController = navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "menu/{npm}/{nama}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("npm") { type = NavType.StringType },
|
||||||
|
navArgument("nama") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val npm = backStackEntry.arguments?.getString("npm") ?: "N/A"
|
||||||
|
val nama = backStackEntry.arguments?.getString("nama") ?: "N/A"
|
||||||
|
MenuScreen(navController = navController, npm = npm, nama = nama)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "absensi/{npm}/{nama}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("npm") { type = NavType.StringType },
|
||||||
|
navArgument("nama") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) { backStackEntry ->
|
||||||
|
val npm = backStackEntry.arguments?.getString("npm") ?: "N/A"
|
||||||
|
val nama = backStackEntry.arguments?.getString("nama") ?: "N/A"
|
||||||
|
AbsensiScreen(
|
||||||
|
activity = activity,
|
||||||
|
navController = navController,
|
||||||
|
npm = npm,
|
||||||
|
nama = nama
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = "history/{npm}",
|
||||||
|
arguments = listOf(navArgument("npm") { type = NavType.StringType })
|
||||||
|
) { backStackEntry ->
|
||||||
|
val npm = backStackEntry.arguments?.getString("npm") ?: "N/A"
|
||||||
|
HistoryScreen(navController = navController, npm = npm, activity = activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= UI SCREENS ================= */
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(navController: NavController) {
|
||||||
|
var npm by remember { mutableStateOf("") }
|
||||||
|
var nama by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Login Absensi", color = Color.White, fontSize = 18.sp) },
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF2E7D32))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF8FBF8))
|
||||||
|
.padding(horizontal = 32.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Person, contentDescription = null, modifier = Modifier.size(100.dp), tint = Color(0xFF2E7D32))
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = npm,
|
||||||
|
onValueChange = { npm = it },
|
||||||
|
label = { Text("NPM") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = nama,
|
||||||
|
onValueChange = { nama = it },
|
||||||
|
label = { Text("Nama Lengkap") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (npm.isNotBlank() && nama.isNotBlank()) {
|
||||||
|
navController.navigate("menu/$npm/$nama")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth().height(50.dp),
|
||||||
|
enabled = npm.isNotBlank() && nama.isNotBlank(),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B5E20)),
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
) {
|
||||||
|
Text("LOGIN", fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MenuScreen(navController: NavController, npm: String, nama: String) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Menu Utama", color = Color.White, fontSize = 18.sp) },
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF2E7D32)),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Logout", tint = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF8FBF8))
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("Selamat Datang,", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text("$nama ($npm)", fontWeight = FontWeight.Bold, fontSize = 20.sp)
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Tombol Absensi
|
||||||
|
Button(
|
||||||
|
onClick = { navController.navigate("absensi/$npm/$nama") },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.CameraAlt, contentDescription = null, modifier = Modifier.size(24.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Absen Kehadiran", fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Tombol Riwayat
|
||||||
|
Button(
|
||||||
|
onClick = { navController.navigate("history/$npm") },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.History, contentDescription = null, modifier = Modifier.size(24.dp))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Riwayat Absensi", fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AbsensiScreen(activity: ComponentActivity, navController: NavController, npm: String, nama: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var lokasiStatus by remember { mutableStateOf("Mengecek Lokasi...") }
|
||||||
|
var locationAvailable by remember { mutableStateOf(false) }
|
||||||
|
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var isUploading by remember { mutableStateOf(false) }
|
||||||
|
var latitude by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var longitude by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var matakuliah by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
|
||||||
|
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val bitmap = result.data?.extras?.get("data") as? Bitmap
|
||||||
|
if (bitmap != null) foto = bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestPermissionLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissions ->
|
||||||
|
if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true) {
|
||||||
|
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
|
||||||
|
.addOnSuccessListener { loc ->
|
||||||
|
if (loc != null) {
|
||||||
|
latitude = loc.latitude
|
||||||
|
longitude = loc.longitude
|
||||||
|
locationAvailable = true
|
||||||
|
lokasiStatus = if (LocationUtils.isWithinCampusRadius(loc.latitude, loc.longitude)) {
|
||||||
|
"Di Dalam Area Kampus"
|
||||||
|
} else {
|
||||||
|
"Di Luar Area Kampus"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lokasiStatus = "Gagal Mendapatkan Lokasi"
|
||||||
|
locationAvailable = false
|
||||||
|
}
|
||||||
|
}.addOnFailureListener {
|
||||||
|
lokasiStatus = "Gagal Mendapatkan Lokasi"
|
||||||
|
locationAvailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lokasiStatus = "Izin lokasi ditolak."
|
||||||
|
locationAvailable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
requestPermissionLauncher.launch(
|
||||||
|
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Absen Kehadiran", color = Color.White, fontSize = 18.sp) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Kembali", tint = Color.White)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color(0xFF2E7D32))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF8FBF8))
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text("Selamat Datang,", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text("$nama ($npm)", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = matakuliah,
|
||||||
|
onValueChange = { matakuliah = it },
|
||||||
|
label = { Text("Mata Kuliah") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Status Lokasi: $lokasiStatus ",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (locationAvailable) Color(0xFF2E7D32) else Color.Red
|
||||||
|
)
|
||||||
|
if (locationAvailable) {
|
||||||
|
Icon(Icons.Default.CheckCircle, "OK", tint = Color(0xFF2E7D32), modifier = Modifier.size(16.dp))
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Close, "Error", tint = Color.Red, modifier = Modifier.size(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
Text("Ambil Foto Selfie", fontWeight = FontWeight.SemiBold, fontSize = 16.sp)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(220.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(0xFFE0E0E0)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (foto != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = foto!!.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Person, null, modifier = Modifier.size(80.dp), tint = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
cameraLauncher.launch(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(0.7f).height(50.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50)),
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
) {
|
||||||
|
Text("AMBIL FOTO", fontWeight = FontWeight.Bold, color = Color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
isUploading = true
|
||||||
|
if (latitude != null && longitude != null && foto != null) {
|
||||||
|
kirimKeServer(activity, npm, nama, matakuliah, latitude!!, longitude!!, foto!!) {
|
||||||
|
isUploading = false
|
||||||
|
if (it) navController.popBackStack()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isUploading = false
|
||||||
|
Toast.makeText(context, "Lokasi atau foto belum siap.", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(0.7f).height(50.dp),
|
||||||
|
enabled = foto != null && locationAvailable && matakuliah.isNotBlank() && !isUploading,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1B5E20)),
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
) {
|
||||||
|
Text("KIRIM ABSENSI", fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (isUploading) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
CircularProgressIndicator(color = Color(0xFF2E7D32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun HistoryScreen(navController: NavController, npm: String, activity: Activity) {
|
||||||
|
var historyList by remember { mutableStateOf<List<AttendanceRecord>>(emptyList()) }
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(npm) {
|
||||||
|
fetchHistoryFromServer(npm, activity) {
|
||||||
|
historyList = it
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text("Riwayat Absensi", color = Color.White) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(Icons.Default.ArrowBack, "Kembali", tint = Color.White)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(Color(0xFF2E7D32))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
if (isLoading) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else if (historyList.isEmpty()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Tidak ada riwayat absensi.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
items(historyList) { record ->
|
||||||
|
val date = SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale.getDefault()).format(Date(record.timestamp))
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(2.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = date, fontWeight = FontWeight.Medium)
|
||||||
|
Text(
|
||||||
|
text = record.status,
|
||||||
|
color = if (record.status == "Sukses") Color(0xFF2E7D32) else Color.Red,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
/* ================= NETWORK LOGIC ================= */
|
||||||
Text(
|
|
||||||
text = "Hello $name!",
|
fun kirimKeServer(activity: Activity, npm: String, nama: String, matakuliah: String, lat: Double, lon: Double, img: Bitmap, onFinish: (Boolean) -> Unit) {
|
||||||
modifier = modifier
|
thread {
|
||||||
)
|
var isSuccess = false
|
||||||
|
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 = JSONObject().apply {
|
||||||
|
put("npm", npm)
|
||||||
|
put("nama", nama)
|
||||||
|
put("mata_kuliah", matakuliah) // FIX: Changed from "matakuliah" to "mata_kuliah"
|
||||||
|
put("latitude", lat)
|
||||||
|
put("longitude", lon)
|
||||||
|
put("timestamp", System.currentTimeMillis())
|
||||||
|
put("foto_base64", bitmapToBase64(img))
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.outputStream.write(json.toString().toByteArray())
|
||||||
|
|
||||||
|
val code = conn.responseCode
|
||||||
|
isSuccess = code == 200
|
||||||
|
activity.runOnUiThread {
|
||||||
|
Toast.makeText(activity, if(isSuccess) "Absensi Sukses!" else "Absensi Gagal: Server Error $code", Toast.LENGTH_LONG).show()
|
||||||
|
onFinish(isSuccess)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
activity.runOnUiThread {
|
||||||
|
Toast.makeText(activity, "Error: Gagal terhubung ke server.", Toast.LENGTH_SHORT).show()
|
||||||
|
onFinish(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
fun fetchHistoryFromServer(npm: String, activity: Activity, onResult: (List<AttendanceRecord>) -> Unit) {
|
||||||
@Composable
|
thread {
|
||||||
fun GreetingPreview() {
|
try {
|
||||||
SistemAkademikTheme {
|
// PENTING: URL ini hanya contoh. Ganti dengan URL API Anda untuk mengambil riwayat.
|
||||||
Greeting("Android")
|
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/riwayat-absensi?npm=$npm")
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
|
||||||
|
if (conn.responseCode == 200) {
|
||||||
|
val response = conn.inputStream.bufferedReader().use { it.readText() }
|
||||||
|
val jsonArray = JSONArray(response)
|
||||||
|
val records = mutableListOf<AttendanceRecord>()
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val jsonObject = jsonArray.getJSONObject(i)
|
||||||
|
records.add(
|
||||||
|
AttendanceRecord(
|
||||||
|
timestamp = jsonObject.getLong("timestamp"),
|
||||||
|
status = jsonObject.getString("status")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
activity.runOnUiThread { onResult(records) }
|
||||||
|
} else {
|
||||||
|
activity.runOnUiThread { onResult(emptyList()) }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
activity.runOnUiThread { onResult(emptyList()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,10 @@ espressoCore = "3.7.0"
|
|||||||
lifecycleRuntimeKtx = "2.9.4"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
composeBom = "2024.09.00"
|
composeBom = "2024.09.00"
|
||||||
|
activityComposeVersion = "1.12.2"
|
||||||
|
playServicesLocation = "21.3.0"
|
||||||
|
navigationCompose = "2.9.6"
|
||||||
|
materialIconsExtended = "1.6.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@ -24,6 +28,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
|||||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
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-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityComposeVersion" }
|
||||||
|
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
|
||||||
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
|
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "materialIconsExtended" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
225
n8n-workflow-EAS.json
Normal file
225
n8n-workflow-EAS.json
Normal 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": []
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user