Compare commits

..

No commits in common. "f91fb981babf22a28924990dce0eac90499573e1" and "cbe7e50b9621330a16f900cded7f674a939644a5" have entirely different histories.

20 changed files with 82 additions and 627 deletions

View File

@ -4,14 +4,6 @@
<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
View File

@ -1,6 +1,5 @@
<?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
View File

@ -1,3 +1,4 @@
<?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
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

121
README.md
View File

@ -1,83 +1,82 @@
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile) # 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
## 👤 Identitas Mahasiswa
- **Nama:** Fadlan Rivaldi
- **NPM:** 202310715280
---
## 📌 Deskripsi Proyek ## 📌 Deskripsi Proyek
Proyek ini merupakan **Tugas Akhir / Project Akhir Mata Kuliah Pemrograman Mobile**. Aplikasi ini dikembangkan untuk mengelola absensi mahasiswa secara cerdas dengan validasi ganda: **Lokasi GPS (Geofencing)** dan **Verifikasi Wajah (Selfie)**. 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 memastikan kehadiran mahasiswa valid apabila: Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
1. Mahasiswa berada dalam radius lokasi kampus yang telah ditentukan. 1. Berada pada **lokasi yang telah ditentukan**, dan
2. Mahasiswa mengambil foto selfie sebagai bukti kehadiran fisik pada saat jam mata kuliah berlangsung. 2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
--- ---
## 📝 Change Log (Modifikasi & Pengembangan) ## 🎯 Tujuan Proyek
Berdasarkan *starter project* yang disediakan, berikut adalah perubahan dan fitur yang telah saya implementasikan: - Mengimplementasikan **Location-Based Service (LBS)** pada aplikasi mobile
- Mengintegrasikan **kamera perangkat** untuk dokumentasi absensi
1. **Otomatisasi Data Profil**: Melakukan *hardcode* data profil pada `AbsensiViewModel` agar saat login, data Nama (**Fadlan Rivaldi**) dan NPM (**202310715280**) langsung terisi secara otomatis. - Mencegah kecurangan absensi (titip absen)
2. **Manajemen State dengan StateFlow**: Mengimplementasikan `MutableStateFlow` pada `AbsensiViewModel` untuk menangani data reaktif seperti list Mata Kuliah, Lokasi terkini, dan status Foto yang diambil. - Mengembangkan aplikasi mobile akademik berbasis Android
3. **Integrasi Kamera Modern (CameraX)**: Mengimplementasikan fungsionalitas pengambilan gambar menggunakan library CameraX dan menyimpannya ke dalam *State* aplikasi (`capturedPhoto`). - Melatih kemampuan perancangan dan implementasi aplikasi mobile
4. **Sistem Lokasi Presisi**: Menggunakan `play-services-location` untuk mendapatkan titik koordinat Latitude dan Longitude secara *real-time*.
5. **Manajemen History Dinamis**: Menambahkan logika untuk memasukkan data absensi yang baru dikirim ke dalam daftar riwayat (`_absensiHistory`) secara instan di dalam aplikasi.
6. **Penyelesaian Bug XML & Gradle**:
- Memperbaiki error `Unresolved reference 'R'` melalui sinkronisasi namespace.
- Memperbaiki error `XMLStreamException` pada file layout `fragment_preview.xml`.
- Merapikan struktur plugin pada `build.gradle.kts`.
--- ---
## 🚀 Fitur Utama ## 🚀 Fitur Utama
- 🔐 **Login Mahasiswa**: Autentikasi dummy yang langsung mengarahkan ke profil mahasiswa. - 🔐 **Login Pengguna (Mahasiswa)**
- 🎓 **Pemilihan Mata Kuliah**: Daftar mata kuliah dinamis (Pemrograman Mobile, Basis Data, dll). - 📍 **Pengambilan Koordinat Lokasi (Latitude & Longitude)**
- 📍 **LBS (Location Based Service)**: Deteksi otomatis lokasi mahasiswa. - 🏫 **Validasi Lokasi Absensi (Radius Area)**
- 📸 **Selfie Verification**: Integrasi kamera depan untuk pengambilan foto bukti kehadiran. - 📸 **Pengambilan Foto Mahasiswa Saat Absensi**
- 📄 **History Absensi**: Menampilkan riwayat absensi (Mata kuliah, Tanggal, Waktu, dan Status). - 🕒 **Pencatatan Waktu Absensi**
- 🕒 **Auto Timestamp**: Pencatatan waktu otomatis saat tombol "Submit" ditekan. - 📄 **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 ## 🛠️ Teknologi yang Digunakan
- **Bahasa**: Kotlin - **Platform** : Android
- **UI Framework**: Jetpack Compose (Modern UI) & XML Layout (Interoperability). - **Bahasa Pemrograman** : Kotlin / Java
- **Architecture**: MVVM (Model-View-ViewModel). - **Location Service** :
- **State Management**: StateFlow & LiveData. - Google Maps API
- **Kamera**: CameraX API. - Fused Location Provider
- **Lokasi**: Fused Location Provider. - **Camera API** : CameraX / Camera2
- **Dependency Management**: Gradle Version Catalog (libs.versions.toml). - **Database** : Firebase / SQLite / MySQL
- **Storage** : Firebase Storage / Local Storage
--- - **IDE** : Android Studio
## 🗺️ Mekanisme Absensi
1. Login dengan NIM dan Password.
2. Aplikasi mendeteksi lokasi dan meminta izin kamera.
3. Pilih Mata Kuliah yang sedang berlangsung.
4. Ambil foto selfie.
5. Tekan tombol **Submit**. Sistem akan mengirimkan data (Nama, NPM, Koordinat, Foto, dan Matkul) ke server melalui Webhook.
--- ---
## 🔐 Izin Aplikasi (Permissions) ## 🔐 Izin Aplikasi (Permissions)
Aplikasi memerlukan akses: Aplikasi memerlukan izin berikut:
- `CAMERA`: Untuk mengambil foto selfie. - `ACCESS_FINE_LOCATION`
- `ACCESS_FINE_LOCATION`: Untuk validasi lokasi kampus. - `ACCESS_COARSE_LOCATION`
- `INTERNET`: Untuk mengirim data absensi ke server. - `CAMERA`
- `INTERNET`
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
--- ---
## ⚠️ Disclaimer & Catatan ## 📂 Struktur Proyek (Contoh)
- **PENGGUNAAN AI**: Proyek ini dikembangkan dengan bantuan **Kecerdasan Buatan (AI)** (ChatGPT/Claude) dalam proses debugging Gradle, perbaikan error XML, dan penyusunan struktur dokumentasi ini.
- **PENGEMBANGAN**: Aplikasi dikembangkan dari *starter project* dengan modifikasi pada bagian logika ViewModel dan UI Composable untuk menyesuaikan kebutuhan tugas.
---
## 🔗 Link Monitoring & Data
- **Monitoring (ntfy)**: [ntfy.ubharajaya.ac.id/EAS](https://ntfy.ubharajaya.ac.id/EAS)
- **Data Spreadsheet**: [Cek Data Absensi](https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0)
- **Webhook Production**: `https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254`
---
*Dibuat untuk memenuhi Tugas Project Akhir EAS 2025/2026.*

View File

@ -45,15 +45,11 @@ dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation("androidx.activity:activity-compose:1.9.0")
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
// Location (GPS)
implementation("com.google.android.gms:play-services-location:21.0.1")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -2,14 +2,6 @@
<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"

View File

@ -1,112 +1,28 @@
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.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.padding
import androidx.compose.material3.* import androidx.compose.material3.Scaffold
import androidx.compose.runtime.* import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview
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,
latitude: Double,
longitude: Double,
foto: Bitmap
) {
thread {
try {
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
val conn = url.openConnection() as HttpURLConnection
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
val json = JSONObject().apply {
put("npm", "12345")
put("nama","Arif R D")
put("latitude", latitude)
put("longitude", longitude)
put("timestamp", System.currentTimeMillis())
put("foto_base64", bitmapToBase64(foto))
}
conn.outputStream.use {
it.write(json.toString().toByteArray())
}
val responseCode = conn.responseCode
context.runOnUiThread {
Toast.makeText(
context,
if (responseCode == 200)
"Absensi diterima server"
else
"Absensi ditolak server",
Toast.LENGTH_SHORT
).show()
}
conn.disconnect()
} catch (_: Exception) {
context.runOnUiThread {
Toast.makeText(
context,
"Gagal kirim ke server",
Toast.LENGTH_SHORT
).show()
}
}
}
}
/* ================= ACTIVITY ================= */
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
SistemAkademikTheme { SistemAkademikTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
AbsensiScreen( Greeting(
modifier = Modifier.padding(innerPadding), name = "Android",
activity = this modifier = Modifier.padding(innerPadding)
) )
} }
} }
@ -114,161 +30,18 @@ class MainActivity : ComponentActivity() {
} }
} }
/* ================= UI ================= */
@Composable @Composable
fun AbsensiScreen( fun Greeting(name: String, modifier: Modifier = Modifier) {
modifier: Modifier = Modifier,
activity: ComponentActivity
) {
val context = LocalContext.current
var lokasi by remember { mutableStateOf("Koordinat: -") }
var latitude by remember { mutableStateOf<Double?>(null) }
var longitude by remember { mutableStateOf<Double?>(null) }
var foto by remember { mutableStateOf<Bitmap?>(null) }
val fusedLocationClient =
LocationServices.getFusedLocationProviderClient(context)
/* ===== Permission Lokasi ===== */
val locationPermissionLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
if (
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
latitude = location.latitude
longitude = location.longitude
lokasi =
"Lat: ${location.latitude}\nLon: ${location.longitude}"
} else {
lokasi = "Lokasi tidak tersedia"
}
}
.addOnFailureListener {
lokasi = "Gagal mengambil lokasi"
}
}
} else {
Toast.makeText(
context,
"Izin lokasi ditolak",
Toast.LENGTH_SHORT
).show()
}
}
/* ===== Kamera ===== */
val cameraLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val bitmap =
result.data?.extras?.getParcelable("data", Bitmap::class.java)
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 ===== */
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center
) {
Text( Text(
text = "Absensi Akademik", text = "Hello $name!",
style = MaterialTheme.typography.titleLarge modifier = modifier
) )
Spacer(modifier = Modifier.height(16.dp))
Text(text = lokasi)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
cameraPermissionLauncher.launch(
Manifest.permission.CAMERA
)
},
modifier = Modifier.fillMaxWidth()
) {
Text("Ambil Foto")
} }
Spacer(modifier = Modifier.height(12.dp)) @Preview(showBackground = true)
@Composable
Button( fun GreetingPreview() {
onClick = { SistemAkademikTheme {
if (latitude != null && longitude != null && foto != null) { Greeting("Android")
kirimKeN8n(
activity,
latitude!!,
longitude!!,
foto!!
)
} else {
Toast.makeText(
context,
"Lokasi atau foto belum lengkap",
Toast.LENGTH_SHORT
).show()
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Kirim Absensi")
}
} }
} }

View File

@ -1,2 +0,0 @@
package id.ac.ubharajaya.sistemakademik.Repository

View File

@ -1,38 +0,0 @@
package id.ac.ubharajaya.sistemakademik
data class MataKuliah(
val id: String,
val nama: String,
val kode: String,
val dosen: String,
val hari: String,
val jam: String,
val ruang: String
)
data class Student(
val nim: String,
val nama: String,
val email: String,
val prodi: String,
val foto: String? = null
)
data class AbsensiData(
val mataKuliahId: String,
val mataKuliahNama: String,
val nim: String,
val nama: String,
val latitude: Double,
val longitude: Double,
val fotoPath: String,
val timestamp: Long
)
data class AbsensiHistory(
val mataKuliah: String,
val tanggal: String,
val waktu: String,
val status: String, // "Hadir", "Terlambat"
val foto: String
)

View File

@ -1,8 +0,0 @@
package id.ac.ubharajaya.sistemakademik
sealed class Screen(val route: String) {
object Login : Screen("login")
object MataKuliah : Screen("mata_kuliah")
object Preview : Screen("preview/{mataKuliahId}") {
fun createRoute(mataKuliahId: String) = "preview/$mataKuliahId"
}

View File

@ -1,2 +0,0 @@
package id.ac.ubharajaya.sistemakademik.network

View File

@ -1,2 +0,0 @@
package id.ac.ubharajaya.sistemakademik.ui.theme.screen

View File

@ -1,4 +0,0 @@
package id.ac.ubharajaya.sistemakademik.ui.theme.screen
class MataKuliahScreen {
}

View File

@ -1,2 +0,0 @@
package id.ac.ubharajaya.sistemakademik.ui.theme.screen

View File

@ -1,2 +0,0 @@
package id.ac.ubharajaya.sistemakademik.ui.theme.screen

View File

@ -1,2 +0,0 @@
package id.ac.ubharajaya.sistemakademik.viewmodel

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
</PreferenceScreen>

View File

@ -1,225 +0,0 @@
{
"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": []
}