Compare commits
10 Commits
cbe7e50b96
...
7cd8b241d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cd8b241d0 | |||
| ed435ffbc1 | |||
| 926d3e0a14 | |||
| cddaf87d88 | |||
| c9cc99baa2 | |||
| 2a00b834c7 | |||
| 4d7fc844e2 | |||
| d4d1b27209 | |||
| 3e66ebcf9e | |||
| 46b74d7099 |
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>
|
||||||
|
|||||||
13
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
.idea/gradle.xml
generated
@ -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>
|
||||||
|
|||||||
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
1
.idea/misc.xml
generated
@ -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
@ -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
|
After Width: | Height: | Size: 715 KiB |
44
README.md
@ -1,12 +1,20 @@
|
|||||||
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
||||||
|
|
||||||
## 📌 Deskripsi Proyek
|
## 📌 Deskripsi Proyek
|
||||||
Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang bertujuan untuk membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**.
|
Proyek ini merupakan **Tugas Akhir EAS (Evaluasi Akhir Semester)** yang dikembangkan oleh:
|
||||||
|
|
||||||
|
- **Nama** : Faris Naufal Priatna
|
||||||
|
- **NPM** : 202310715123
|
||||||
|
|
||||||
|
Tujuan proyek ini adalah membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**.
|
||||||
|
|
||||||
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
|
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
|
||||||
|
|
||||||
1. Berada pada **lokasi yang telah ditentukan**, dan
|
1. Berada pada **lokasi yang telah ditentukan**, dan
|
||||||
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
|
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
|
||||||
|
|
||||||
|
Aplikasi ini **dibantu dikembangkan oleh AI ChatGPT** untuk memberikan saran teknis, struktur kode, dan pengembangan fitur tambahan.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Tujuan Proyek
|
## 🎯 Tujuan Proyek
|
||||||
@ -39,7 +47,7 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den
|
|||||||
- 📍 **Koordinat lokasi mahasiswa**
|
- 📍 **Koordinat lokasi mahasiswa**
|
||||||
- 📸 **Foto mahasiswa secara real-time**
|
- 📸 **Foto mahasiswa secara real-time**
|
||||||
5. Sistem melakukan validasi:
|
5. Sistem melakukan validasi:
|
||||||
- Lokasi berada dalam **radius absensi**
|
- Absensi harus menggunakan foto
|
||||||
- Foto berhasil diambil
|
- Foto berhasil diambil
|
||||||
6. Jika valid → **Absensi berhasil**
|
6. Jika valid → **Absensi berhasil**
|
||||||
7. Jika tidak valid → **Absensi ditolak**
|
7. Jika tidak valid → **Absensi ditolak**
|
||||||
@ -75,8 +83,36 @@ Aplikasi memerlukan izin berikut:
|
|||||||
- `ACCESS_COARSE_LOCATION`
|
- `ACCESS_COARSE_LOCATION`
|
||||||
- `CAMERA`
|
- `CAMERA`
|
||||||
- `INTERNET`
|
- `INTERNET`
|
||||||
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
|
- `WRITE_EXTERNAL_STORAGE` (opsional, jika diperlukan)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📂 Struktur Proyek (Contoh)
|
## 📂 Mockup
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Catatan
|
||||||
|
- Kembangkan project dari starter yang sudah disediakan, **tidak membuat dari awal**.
|
||||||
|
- Koordinat bisa ditambah/kurangi sedikit agar tidak memunculkan lokasi rumah masing-masing, data awal tetap diambil dari GPS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pengecekan
|
||||||
|
- https://ntfy.ubharajaya.ac.id/EAS
|
||||||
|
- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Webhook
|
||||||
|
- Test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254
|
||||||
|
- Production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Identitas Pengembang
|
||||||
|
- **Nama** : Faris Naufal Priatna
|
||||||
|
- **NPM** : 202310715123
|
||||||
|
- **Jenis Proyek** : Tugas Akhir EAS
|
||||||
|
- **Bantuan Pengembangan** : AI ChatGPT
|
||||||
|
Output Aplikasi yang sudah jadi
|
||||||
@ -45,11 +45,14 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation("androidx.activity:activity-compose:1.9.0")
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
// Location (GPS)
|
||||||
|
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@ -1,27 +1,46 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
package="id.ac.ubharajaya.sistemakademik">
|
||||||
|
|
||||||
|
<!-- ===== PERMISSIONS ===== -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<!-- ===== FEATURES (OPTIONAL TAPI AMAN) ===== -->
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="true" />
|
||||||
|
|
||||||
|
<!-- ===== APPLICATION ===== -->
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.SistemAkademik">
|
android:theme="@style/Theme.SistemAkademik">
|
||||||
|
|
||||||
|
<!-- ===== LOGIN ACTIVITY (LAUNCHER) ===== -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".LoginActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:screenOrientation="portrait">
|
||||||
android:theme="@style/Theme.SistemAkademik">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- ===== MAIN ACTIVITY ===== -->
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="portrait" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
45
app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.Base64
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class untuk menyimpan informasi absensi mahasiswa
|
||||||
|
*/
|
||||||
|
data class Absensi(
|
||||||
|
val npm: String,
|
||||||
|
val nama: String,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val waktu: String,
|
||||||
|
val foto: Bitmap
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Convert objek Absensi ini menjadi JSONObject
|
||||||
|
* Siap untuk dikirim ke server
|
||||||
|
*/
|
||||||
|
fun toJson(): JSONObject {
|
||||||
|
val json = JSONObject()
|
||||||
|
json.put("npm", npm)
|
||||||
|
json.put("nama", nama)
|
||||||
|
json.put("latitude", latitude)
|
||||||
|
json.put("longitude", longitude)
|
||||||
|
json.put("timestamp", System.currentTimeMillis())
|
||||||
|
json.put("waktu", waktu)
|
||||||
|
json.put("foto_base64", bitmapToBase64(foto))
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Helper function untuk convert Bitmap ke Base64
|
||||||
|
*/
|
||||||
|
fun bitmapToBase64(bitmap: Bitmap): String {
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||||
|
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
|
|
||||||
|
class LoginActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
SistemAkademikTheme {
|
||||||
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
|
LoginScreen(modifier = Modifier.padding(innerPadding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var npm by remember { mutableStateOf("") }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
// LOGO KAMPUS
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.logo_absensi), // logo kamu
|
||||||
|
contentDescription = "Logo Kampus",
|
||||||
|
modifier = Modifier.size(120.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Judul Login
|
||||||
|
Text(
|
||||||
|
text = "Login Mahasiswa",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Input NPM
|
||||||
|
OutlinedTextField(
|
||||||
|
value = npm,
|
||||||
|
onValueChange = { npm = it },
|
||||||
|
label = { Text("NPM") },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Input Password
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
label = { Text("Password") },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
visualTransformation = PasswordVisualTransformation()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Tombol Login
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (npm == "202310715123" && password == "123") {
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "NPM atau password salah", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,47 +1,284 @@
|
|||||||
package id.ac.ubharajaya.sistemakademik
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.android.gms.location.LocationServices
|
||||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
// ================== UTILS ==================
|
||||||
|
fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
|
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.requestMethod = "POST"
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
|
conn.doOutput = true
|
||||||
|
val json = absensi.toJson()
|
||||||
|
conn.outputStream.use { it.write(json.toString().toByteArray()) }
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
context.runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
if (responseCode == 200) "Absensi diterima server" else "Absensi ditolak server",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
conn.disconnect()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
context.runOnUiThread {
|
||||||
|
Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== MAIN ACTIVITY ==================
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
Greeting(
|
AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this)
|
||||||
name = "Android",
|
|
||||||
modifier = Modifier.padding(innerPadding)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================== COMPOSABLE UI ==================
|
||||||
@Composable
|
@Composable
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
|
||||||
Text(
|
val context = LocalContext.current
|
||||||
text = "Hello $name!",
|
val session = remember { SessionManager(context) }
|
||||||
|
|
||||||
|
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) }
|
||||||
|
var waktuAbsensi by remember { mutableStateOf<String?>(null) }
|
||||||
|
val absensiList = remember { mutableStateListOf<Absensi>() }
|
||||||
|
|
||||||
|
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||||
|
|
||||||
|
// ================== STATE WARNA ==================
|
||||||
|
var primaryColor by remember { mutableStateOf(Color(0xFF6200EE)) }
|
||||||
|
var backgroundColor by remember { mutableStateOf(Color(0xFFF2F2F2)) }
|
||||||
|
|
||||||
|
val colors = listOf(
|
||||||
|
Color.Red, Color.Green, Color.Blue, Color.Magenta, Color.Cyan,
|
||||||
|
Color.Yellow, Color.Gray, Color.DarkGray, Color.Black, Color(0xFFFF9800),
|
||||||
|
Color(0xFF9C27B0), Color(0xFF4CAF50), Color(0xFF03A9F4), Color(0xFFE91E63)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ===== Permission & Kamera =====
|
||||||
|
val locationPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
fusedLocationClient.lastLocation
|
||||||
|
.addOnSuccessListener { location ->
|
||||||
|
if (location != null) {
|
||||||
|
latitude = location.latitude
|
||||||
|
longitude = location.longitude
|
||||||
|
lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}"
|
||||||
|
} else {
|
||||||
|
lokasi = "Lokasi tidak tersedia"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val bitmap: Bitmap? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
result.data?.extras?.getParcelable("data", Bitmap::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
result.data?.extras?.getParcelable("data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap != null) {
|
||||||
|
foto = bitmap
|
||||||
|
Toast.makeText(context, "Foto berhasil diambil", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
|
cameraLauncher.launch(intent)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== UI ==================
|
||||||
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(backgroundColor)
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Absensi Akademik",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = primaryColor
|
||||||
)
|
)
|
||||||
}
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
// PALETTE WARNA BULETAN
|
||||||
@Composable
|
Text("Pilih Warna Tema:", style = MaterialTheme.typography.bodyMedium, color = primaryColor)
|
||||||
fun GreetingPreview() {
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
SistemAkademikTheme {
|
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
|
||||||
Greeting("Android")
|
colors.forEach { color ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.padding(4.dp)
|
||||||
|
.background(color, shape = CircleShape)
|
||||||
|
.clickable {
|
||||||
|
primaryColor = color
|
||||||
|
backgroundColor = color.copy(alpha = 0.1f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(text = lokasi, color = primaryColor)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
|
||||||
|
) { Text("Ambil Foto") }
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (latitude != null && longitude != null && foto != null) {
|
||||||
|
val waktu = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()).format(System.currentTimeMillis())
|
||||||
|
waktuAbsensi = waktu
|
||||||
|
|
||||||
|
val absensi = Absensi(
|
||||||
|
npm = session.getUserNpm() ?: "Faris Naufal Priatna",
|
||||||
|
nama = session.getUserName() ?: "202310715123",
|
||||||
|
latitude = latitude!!,
|
||||||
|
longitude = longitude!!,
|
||||||
|
waktu = waktu,
|
||||||
|
foto = foto!!
|
||||||
|
)
|
||||||
|
kirimKeN8n(activity, absensi)
|
||||||
|
absensiList.add(absensi)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
|
||||||
|
) { Text("Kirim Absensi") }
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
waktuAbsensi?.let { Text("Waktu Absensi: $it", color = primaryColor) }
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// ================== LOGOUT BUTTON ==================
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
session.logout()
|
||||||
|
context.startActivity(Intent(context, LoginActivity::class.java))
|
||||||
|
(context as ComponentActivity).finish()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
|
||||||
|
) {
|
||||||
|
Text("Logout")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text("Riwayat Kehadiran", color = primaryColor)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
if (absensiList.isEmpty()) Text("Belum ada absensi", color = primaryColor)
|
||||||
|
else LazyColumn {
|
||||||
|
items(absensiList) { item ->
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text("Nama: ${item.nama}", color = primaryColor)
|
||||||
|
Text("NPM: ${item.npm}", color = primaryColor)
|
||||||
|
Text("Waktu: ${item.waktu}", color = primaryColor)
|
||||||
|
Text("Lat: ${item.latitude}, Lon: ${item.longitude}", color = primaryColor)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Image(
|
||||||
|
bitmap = item.foto.asImageBitmap(),
|
||||||
|
contentDescription = "Foto Absensi",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(150.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
class SessionManager(context: Context) {
|
||||||
|
private val prefs: SharedPreferences =
|
||||||
|
context.getSharedPreferences("user_session", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_IS_LOGGED_IN = "is_logged_in"
|
||||||
|
private const val KEY_NPM = "npm"
|
||||||
|
private const val KEY_NAME = "name"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLogin(npm: String, name: String) {
|
||||||
|
prefs.edit().apply {
|
||||||
|
putBoolean(KEY_IS_LOGGED_IN, true)
|
||||||
|
putString(KEY_NPM, npm)
|
||||||
|
putString(KEY_NAME, name)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLoggedIn(): Boolean = prefs.getBoolean(KEY_IS_LOGGED_IN, false)
|
||||||
|
|
||||||
|
fun logout() {
|
||||||
|
prefs.edit().clear().apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUserNpm(): String? = prefs.getString(KEY_NPM, null)
|
||||||
|
fun getUserName(): String? = prefs.getString(KEY_NAME, null)
|
||||||
|
}
|
||||||
BIN
app/src/main/res/drawable/logo_absensi.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 9.6 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 16 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#DE0505</color>
|
||||||
|
</resources>
|
||||||
@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Sistem Akademik</string>
|
<string name="app_name">Absensi Wajah</string>
|
||||||
</resources>
|
</resources>
|
||||||
28
er.name Faris Naufal Priatna
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
diff.astextplain.textconv=astextplain
|
||||||
|
filter.lfs.clean=git-lfs clean -- %f
|
||||||
|
filter.lfs.smudge=git-lfs smudge -- %f
|
||||||
|
filter.lfs.process=git-lfs filter-process
|
||||||
|
filter.lfs.required=true
|
||||||
|
http.sslbackend=openssl
|
||||||
|
http.sslcainfo=C:/Program Files/Git/mingw64/etc/ssl/certs/ca-bundle.crt
|
||||||
|
core.autocrlf=true
|
||||||
|
core.fscache=true
|
||||||
|
core.symlinks=false
|
||||||
|
pull.rebase=false
|
||||||
|
credential.helper=manager
|
||||||
|
credential.https://dev.azure.com.usehttppath=true
|
||||||
|
init.defaultbranch=master
|
||||||
|
credential.https://git.lab.ubharajaya.ac.id.provider=generic
|
||||||
|
user.name=Faris Naufal Priatna
|
||||||
|
user.email=202310715123@mhs.ubharajaya.ac.id
|
||||||
|
core.repositoryformatversion=0
|
||||||
|
core.filemode=false
|
||||||
|
core.bare=false
|
||||||
|
core.logallrefupdates=true
|
||||||
|
core.symlinks=false
|
||||||
|
core.ignorecase=true
|
||||||
|
submodule.active=.
|
||||||
|
remote.origin.url=https://git.lab.ubharajaya.ac.id/administrator/Starter-EAS-2025-2026.git
|
||||||
|
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
|
||||||
|
branch.main.remote=origin
|
||||||
|
branch.main.merge=refs/heads/main
|
||||||
225
n8n-workflow-EAS.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||