Compare commits
10 Commits
cbe7e50b96
...
d4619f44e7
| Author | SHA1 | Date | |
|---|---|---|---|
| d4619f44e7 | |||
| ed435ffbc1 | |||
| 926d3e0a14 | |||
| cddaf87d88 | |||
| c9cc99baa2 | |||
| 2a00b834c7 | |||
| 4d7fc844e2 | |||
| d4d1b27209 | |||
| 3e66ebcf9e | |||
| 46b74d7099 |
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-13T05:20:56.137492Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8TA08RD8Z" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
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>
|
||||||
45
.kotlin/errors/errors-1768377432549.log
Normal file
45
.kotlin/errors/errors-1768377432549.log
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: Failed connecting to the daemon in 4 retries
|
||||||
|
|
||||||
|
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
|
||||||
|
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
|
||||||
|
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
|
||||||
|
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
|
||||||
|
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
|
||||||
|
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76)
|
||||||
|
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
|
||||||
|
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
|
||||||
|
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
|
||||||
|
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
|
||||||
|
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
|
||||||
|
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
|
||||||
|
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
|
||||||
|
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
|
||||||
|
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
|
||||||
|
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
|
||||||
|
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
|
||||||
|
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||||
|
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
|
||||||
|
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
|
||||||
|
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
|
||||||
|
at org.gradle.internal.Factories$1.create(Factories.java:31)
|
||||||
|
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
|
||||||
|
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
|
||||||
|
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
|
||||||
|
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
|
||||||
|
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
|
||||||
|
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
|
||||||
|
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||||
|
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
|
||||||
|
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||||
|
at java.base/java.lang.Thread.run(Unknown Source)
|
||||||
|
|
||||||
|
|
||||||
BIN
Mockup.png
Normal file
BIN
Mockup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
95
README.md
95
README.md
@ -1,3 +1,6 @@
|
|||||||
|
Nama : Jeremia Sebastian Marpaung
|
||||||
|
NPM : 202310715096
|
||||||
|
|
||||||
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
||||||
|
|
||||||
## 📌 Deskripsi Proyek
|
## 📌 Deskripsi Proyek
|
||||||
@ -19,52 +22,34 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Fitur Utama
|
## 🚀 Fitur Utama
|
||||||
- 🔐 **Login Pengguna (Mahasiswa)**
|
- ✍️ **Input Data Mahasiswa:** Memasukkan NPM dan Nama Lengkap.
|
||||||
- 📍 **Pengambilan Koordinat Lokasi (Latitude & Longitude)**
|
- 📚 **Pilihan Mata Kuliah:** Memilih mata kuliah dari daftar yang tersedia.
|
||||||
- 🏫 **Validasi Lokasi Absensi (Radius Area)**
|
- 📍 **Pengambilan Koordinat Lokasi:** Menggunakan GPS untuk mendapatkan lokasi presensi.
|
||||||
- 📸 **Pengambilan Foto Mahasiswa Saat Absensi**
|
- 📸 **Pengambilan Foto Langsung:** Mengambil foto selfie sebagai bukti kehadiran.
|
||||||
- 🕒 **Pencatatan Waktu Absensi**
|
- 🚀 **Pengiriman Data Absensi:** Mengirim semua data (profil, lokasi, foto) ke server secara *real-time*.
|
||||||
- 📄 **Riwayat Kehadiran Mahasiswa**
|
- ✅ **Validasi Kelengkapan Data:** Tombol kirim hanya aktif jika semua data telah terisi lengkap.
|
||||||
- ⚠️ **Notifikasi Absensi Ditolak jika Tidak Valid**
|
- ✨ **Antarmuka Modern:** Tampilan yang bersih dengan latar belakang gradien dan layout berbasis kartu.
|
||||||
|
- 🎉 **Konfirmasi Keberhasilan:** Menampilkan layar konfirmasi setelah absensi berhasil dikirim.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🗺️ Mekanisme Absensi Berbasis Lokasi dan Foto
|
## 🗺️ Mekanisme Absensi
|
||||||
1. Mahasiswa melakukan **login**
|
1. Mahasiswa **membuka aplikasi**.
|
||||||
2. Memilih menu **Absensi**
|
2. Mengisi **NPM, Nama Lengkap, dan memilih Mata Kuliah** pada kartu profil.
|
||||||
3. Sistem meminta:
|
3. Aplikasi secara otomatis meminta **izin akses lokasi** dan menampilkan koordinat.
|
||||||
- Izin **akses lokasi**
|
4. Mahasiswa menekan tombol **"Ambil Foto"** untuk mengambil gambar diri (selfie).
|
||||||
- Izin **akses kamera**
|
5. Setelah semua data lengkap, mahasiswa menekan tombol **"Kirim Absensi"**.
|
||||||
4. Aplikasi mengambil:
|
6. Data (profil, mata kuliah, foto, lokasi, dan waktu) dikirim ke server untuk divalidasi.
|
||||||
- 📍 **Koordinat lokasi mahasiswa**
|
7. Jika berhasil, aplikasi akan menampilkan **layar konfirmasi**.
|
||||||
- 📸 **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
|
## 🛠️ Teknologi yang Digunakan
|
||||||
- **Platform** : Android
|
- **Platform** : Android
|
||||||
- **Bahasa Pemrograman** : Kotlin / Java
|
- **Bahasa Pemrograman** : Kotlin
|
||||||
- **Location Service** :
|
- **UI Toolkit**: Jetpack Compose
|
||||||
- Google Maps API
|
- **Location Service** : Fused Location Provider
|
||||||
- Fused Location Provider
|
|
||||||
- **Camera API** : CameraX / Camera2
|
- **Camera API** : CameraX / Camera2
|
||||||
- **Database** : Firebase / SQLite / MySQL
|
|
||||||
- **Storage** : Firebase Storage / Local Storage
|
|
||||||
- **IDE** : Android Studio
|
- **IDE** : Android Studio
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -72,11 +57,41 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den
|
|||||||
## 🔐 Izin Aplikasi (Permissions)
|
## 🔐 Izin Aplikasi (Permissions)
|
||||||
Aplikasi memerlukan izin berikut:
|
Aplikasi memerlukan izin berikut:
|
||||||
- `ACCESS_FINE_LOCATION`
|
- `ACCESS_FINE_LOCATION`
|
||||||
- `ACCESS_COARSE_LOCATION`
|
|
||||||
- `CAMERA`
|
- `CAMERA`
|
||||||
- `INTERNET`
|
- `INTERNET`
|
||||||
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📂 Struktur Proyek (Contoh)
|
## 📂 Mockup
|
||||||
|

|
||||||
|
gambar mockup dibuat oleh AI
|
||||||
|
|
||||||
|
## Catatan:
|
||||||
|
- Starter project ini dibuat berbantukan AI
|
||||||
|
- Kembangkan project dari starter yang sudah disediakan, jangan membuat dari awal.
|
||||||
|
- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS.
|
||||||
|
|
||||||
|
## Pengecekan:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Changelog
|
||||||
|
|
||||||
|
### Versi 1.1 - Penyempurnaan UI/UX & Fitur
|
||||||
|
- **UI Refresh & Latar Belakang Gradien:** Mengubah latar belakang menjadi gradien dinamis yang lebih menarik secara visual, dari `tertiaryContainer` ke `primaryContainer`.
|
||||||
|
- **Layar Konfirmasi Absensi:** Menambahkan layar konfirmasi setelah absensi berhasil, memberikan umpan balik yang lebih jelas kepada pengguna.
|
||||||
|
- **Fitur Pilihan Mata Kuliah:** Menambahkan dropdown untuk memilih mata kuliah, dan data mata kuliah kini ikut dikirim ke server.
|
||||||
|
- **Peningkatan Tata Letak:** Menggunakan `Card` untuk mengelompokkan elemen UI, membuat antarmuka lebih terstruktur dan modern.
|
||||||
|
|
||||||
|
### Versi 1.0 - Rilis Awal
|
||||||
|
- **Fungsionalitas Dasar:** Implementasi fitur absensi dengan pengambilan lokasi GPS dan foto.
|
||||||
|
- **Antarmuka Awal:** Desain antarmuka pengguna awal menggunakan Jetpack Compose.
|
||||||
|
- **Integrasi Server:** Mengirim data absensi ke server n8n.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -45,11 +45,16 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation("androidx.activity:activity-compose:1.9.0")
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation("androidx.compose.material:material-icons-extended-android:1.6.8")
|
||||||
|
// Location (GPS)
|
||||||
|
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@ -2,6 +2,14 @@
|
|||||||
<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.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"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|||||||
@ -1,27 +1,160 @@
|
|||||||
package id.ac.ubharajaya.sistemakademik
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.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.animation.AnimatedContent
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
|
import androidx.compose.material.icons.filled.CameraAlt
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.LocationOn
|
||||||
|
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.Brush
|
||||||
|
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.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.android.gms.location.LocationServices
|
||||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
/* ================= UTIL ================= */
|
||||||
|
|
||||||
|
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||||
|
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun kirimKeN8n(
|
||||||
|
context: ComponentActivity,
|
||||||
|
npm: String,
|
||||||
|
nama: String,
|
||||||
|
mataKuliah: String,
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
foto: Bitmap,
|
||||||
|
onResult: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
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; utf-8")
|
||||||
|
conn.setRequestProperty("Accept", "application/json")
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
val json = JSONObject().apply {
|
||||||
|
put("npm", npm)
|
||||||
|
put("nama", nama)
|
||||||
|
put("mata_kuliah", mataKuliah)
|
||||||
|
put("latitude", latitude)
|
||||||
|
put("longitude", longitude)
|
||||||
|
put("timestamp", System.currentTimeMillis())
|
||||||
|
put("foto_base64", bitmapToBase64(foto))
|
||||||
|
}
|
||||||
|
val jsonString = json.toString()
|
||||||
|
|
||||||
|
conn.outputStream.use {
|
||||||
|
it.write(jsonString.toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
val responseMessage = conn.responseMessage
|
||||||
|
isSuccess = responseCode == 200
|
||||||
|
|
||||||
|
context.runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
if (isSuccess) {
|
||||||
|
"Absensi diterima server"
|
||||||
|
} else {
|
||||||
|
"Absensi ditolak server (Kode: $responseCode - $responseMessage)"
|
||||||
|
},
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
context.runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Gagal kirim ke server: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
context.runOnUiThread {
|
||||||
|
onResult(isSuccess)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= ACTIVITY ================= */
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Box(
|
||||||
Greeting(
|
modifier = Modifier
|
||||||
name = "Android",
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
) { innerPadding ->
|
||||||
|
AbsensiScreen(
|
||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -29,19 +162,315 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= UI ================= */
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
fun AbsensiBerhasilScreen(onReset: () -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = "Absensi Berhasil",
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(120.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Hello $name!",
|
text = "Absensi Berhasil",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Data kehadiran Anda telah berhasil direkam oleh sistem.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
Button(
|
||||||
|
onClick = onReset,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Lakukan Absensi Lagi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AbsensiScreen(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = context as ComponentActivity
|
||||||
|
|
||||||
|
var npm by remember { mutableStateOf("") }
|
||||||
|
var nama by remember { mutableStateOf("") }
|
||||||
|
var mataKuliah by remember { mutableStateOf("") }
|
||||||
|
var isMenuMataKuliahExpanded by remember { mutableStateOf(false) }
|
||||||
|
val daftarMataKuliah = listOf("Pemrograman Perangkat Bergerak", "Kecerdasan Buatan", "Pembelajaran Mesin", "Interaksi Manusia dan Komputer", "Keamanan Siber", "Manajemen Proyek Perangkat Lunak", "Manajemen Sekuriti")
|
||||||
|
|
||||||
|
var lokasi by remember { mutableStateOf("Meminta izin lokasi...") }
|
||||||
|
var latitude by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var longitude by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var absensiBerhasil by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val fusedLocationClient =
|
||||||
|
LocationServices.getFusedLocationProviderClient(context)
|
||||||
|
|
||||||
|
val locationPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
lokasi = "Mencari lokasi..."
|
||||||
|
fusedLocationClient.lastLocation
|
||||||
|
.addOnSuccessListener { location ->
|
||||||
|
if (location != null) {
|
||||||
|
latitude = location.latitude
|
||||||
|
longitude = location.longitude
|
||||||
|
lokasi = String.format("Lat: %.6f\nLon: %.6f", location.latitude, location.longitude)
|
||||||
|
} else {
|
||||||
|
lokasi = "Gagal mendapatkan lokasi. Coba lagi."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
lokasi = "Gagal mendapatkan lokasi: ${it.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lokasi = "Izin lokasi ditolak."
|
||||||
|
Toast.makeText(context, "Izin lokasi diperlukan untuk absensi.", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
result.data?.extras?.getParcelable("data", Bitmap::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
result.data?.extras?.getParcelable("data") as? Bitmap
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
foto = bitmap
|
||||||
|
Toast.makeText(context, "Foto berhasil diambil", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
cameraLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetForm() {
|
||||||
|
absensiBerhasil = false
|
||||||
|
npm = ""
|
||||||
|
nama = ""
|
||||||
|
mataKuliah = ""
|
||||||
|
foto = null
|
||||||
|
lokasi = "Meminta izin lokasi..."
|
||||||
|
latitude = null
|
||||||
|
longitude = null
|
||||||
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedContent(targetState = absensiBerhasil, label = "Layout Switcher") { isSuccess ->
|
||||||
|
if (isSuccess) {
|
||||||
|
AbsensiBerhasilScreen(onReset = ::resetForm)
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// --- Header ---
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Text(
|
||||||
|
text = "Absensi Akademik",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Isi data berikut untuk melakukan absensi",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
// --- User Info Card ---
|
||||||
fun GreetingPreview() {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
SistemAkademikTheme {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Greeting("Android")
|
OutlinedTextField(
|
||||||
|
value = npm,
|
||||||
|
onValueChange = { npm = it },
|
||||||
|
label = { Text("NPM (Nomor Pokok Mahasiswa)") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = nama,
|
||||||
|
onValueChange = { nama = it },
|
||||||
|
label = { Text("Nama Lengkap") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = isMenuMataKuliahExpanded,
|
||||||
|
onExpandedChange = { isMenuMataKuliahExpanded = !isMenuMataKuliahExpanded }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = mataKuliah,
|
||||||
|
onValueChange = {}, // readOnly
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Mata Kuliah") },
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = isMenuMataKuliahExpanded)
|
||||||
|
},
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = isMenuMataKuliahExpanded,
|
||||||
|
onDismissRequest = { isMenuMataKuliahExpanded = false }
|
||||||
|
) {
|
||||||
|
daftarMataKuliah.forEach { item ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(item) },
|
||||||
|
onClick = {
|
||||||
|
mataKuliah = item
|
||||||
|
isMenuMataKuliahExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Attendance Data Card ---
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(vertical = 24.dp)) {
|
||||||
|
// -- Photo --
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(140.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (foto != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = foto!!.asImageBitmap(),
|
||||||
|
contentDescription = "Foto Absensi",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CameraAlt,
|
||||||
|
contentDescription = "Placeholder Kamera",
|
||||||
|
modifier = Modifier.size(50.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.CameraAlt, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text(if (foto == null) "Ambil Foto" else "Ambil Ulang Foto")
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp))
|
||||||
|
|
||||||
|
// -- Location --
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
|
Icon(Icons.Default.LocationOn, contentDescription = "Ikon Lokasi", tint = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text("Lokasi Anda Saat Ini", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(lokasi, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Submit Button ---
|
||||||
|
val isDataLengkap = npm.isNotBlank() && nama.isNotBlank() && mataKuliah.isNotBlank() && latitude != null && longitude != null && foto != null
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (isDataLengkap) {
|
||||||
|
isLoading = true
|
||||||
|
kirimKeN8n(activity, npm, nama, mataKuliah, latitude!!, longitude!!, foto!!) { success ->
|
||||||
|
isLoading = false
|
||||||
|
if (success) {
|
||||||
|
absensiBerhasil = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Harap lengkapi semua data terlebih dahulu", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
enabled = isDataLengkap && !isLoading
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(ButtonDefaults.IconSize))
|
||||||
|
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||||
|
Text("Kirim Absensi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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