Compare commits
10 Commits
d4d1b27209
...
5b39cd9673
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b39cd9673 | |||
| e88ada64b2 | |||
| 900cf99681 | |||
| 8aad590097 | |||
| ed435ffbc1 | |||
| 926d3e0a14 | |||
| cddaf87d88 | |||
| c9cc99baa2 | |||
| 2a00b834c7 | |||
| 4d7fc844e2 |
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>
|
||||||
|
|||||||
21
.idea/deviceManager.xml
generated
Normal file
21
.idea/deviceManager.xml
generated
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="API" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Type" />
|
||||||
|
<option name="order" value="DESCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
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>
|
||||||
115
README.md
115
README.md
@ -1,83 +1,74 @@
|
|||||||
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
||||||
|
|
||||||
## 📌 Deskripsi Proyek
|
## 👤 Identitas Mahasiswa
|
||||||
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**.
|
- **Nama:** Dirson Ali Wardana
|
||||||
|
- **NPM:** 202310715246---
|
||||||
|
|
||||||
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa:
|
## 📌 Deskripsi Proyek
|
||||||
1. Berada pada **lokasi yang telah ditentukan**, dan
|
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)**.
|
||||||
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
|
|
||||||
|
Aplikasi memastikan kehadiran mahasiswa valid apabila mahasiswa berada dalam radius lokasi kampus dan melakukan pengambilan foto selfie sebagai bukti kehadiran fisik yang kemudian dikirimkan ke server pemantau (n8n & Google Sheets).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Tujuan Proyek
|
## 📝 Change Log (Modifikasi & Pengembangan)
|
||||||
- Mengimplementasikan **Location-Based Service (LBS)** pada aplikasi mobile
|
Berdasarkan *starter project* yang disediakan, berikut adalah catatan perubahan signifikan yang telah saya implementasikan:
|
||||||
- Mengintegrasikan **kamera perangkat** untuk dokumentasi absensi
|
|
||||||
- Mencegah kecurangan absensi (titip absen)
|
I. **Dropdown Mata Kuliah & Integrasi Data**: Menambahkan fitur pilihan mata kuliah dan memastikan data `namaMatkul` terkirim dengan benar ke server melalui payload JSON.
|
||||||
- Mengembangkan aplikasi mobile akademik berbasis Android
|
II. **Fragment Preview (History Logic)**: Mengimplementasikan `PreviewFragment` yang merangkum seluruh data (Foto, Koordinat, Timestamp) sebelum dikirim, serta menambahkan logika penyimpanan otomatis ke `DataHolder.historyList` setelah pengiriman berhasil.
|
||||||
- Melatih kemampuan perancangan dan implementasi aplikasi mobile
|
III. **Konversi Base64**: Mengimplementasikan fungsi `base64ToBitmap` dan pengiriman `foto_base64` agar foto selfie mahasiswa dapat tersimpan dan dilihat di server.
|
||||||
|
IV. **Modernisasi UI (Material Card & Progress Bar)**: Menggunakan `MaterialCardView` untuk tampilan foto preview dan menambahkan `ProgressBar` (Loading) untuk memberikan feedback visual saat proses pengiriman data ke server.
|
||||||
|
V. **Network Threading**: Menggunakan `kotlin.concurrent.thread` untuk menangani proses pengiriman data ke Webhook agar aplikasi tidak *freeze* (ANR) saat melakukan request jaringan.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Fitur Utama
|
## 🚀 Fitur Utama
|
||||||
- 🔐 **Login Pengguna (Mahasiswa)**
|
- 🔐 **Login Mahasiswa**: Autentikasi otomatis dengan data profil tetap (Hardcoded).
|
||||||
- 📍 **Pengambilan Koordinat Lokasi (Latitude & Longitude)**
|
- 🎓 **Mata Kuliah Selector**: Memilih mata kuliah aktif sebelum melakukan absensi.
|
||||||
- 🏫 **Validasi Lokasi Absensi (Radius Area)**
|
- 📍 **GPS Tracking**: Pengambilan koordinat Latitude dan Longitude secara presisi.
|
||||||
- 📸 **Pengambilan Foto Mahasiswa Saat Absensi**
|
- 📸 **Selfie Verification**: Pratinjau foto selfie sebelum data dikirim ke server.
|
||||||
- 🕒 **Pencatatan Waktu Absensi**
|
- 📄 **Real-time History**: Daftar riwayat absensi yang langsung terupdate menggunakan `RecyclerView` / `LazyColumn`.
|
||||||
- 📄 **Riwayat Kehadiran Mahasiswa**
|
- 📡 **Webhook Integration**: Pengiriman data langsung ke sistem n8n Ubhara Jaya.
|
||||||
- ⚠️ **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
|
||||||
- **Platform** : Android
|
- **Bahasa**: Kotlin
|
||||||
- **Bahasa Pemrograman** : Kotlin / Java
|
- **UI Framework**: Android XML (View System) & Jetpack Compose (Interoperability).
|
||||||
- **Location Service** :
|
- **Network**: `HttpURLConnection` dengan format data **JSON**.
|
||||||
- Google Maps API
|
- **Kamera**: CameraX API.
|
||||||
- Fused Location Provider
|
- **Lokasi**: Fused Location Provider Client.
|
||||||
- **Camera API** : CameraX / Camera2
|
- **Data Manager**: `DataHolder` (Singleton) untuk manajemen data antar layar.
|
||||||
- **Database** : Firebase / SQLite / MySQL
|
|
||||||
- **Storage** : Firebase Storage / Local Storage
|
---
|
||||||
- **IDE** : Android Studio
|
|
||||||
|
## 🗺️ Mekanisme Absensi
|
||||||
|
1. Mahasiswa memilih mata kuliah.
|
||||||
|
2. Aplikasi mengambil lokasi dan mahasiswa melakukan foto selfie.
|
||||||
|
3. Di halaman **Preview**, mahasiswa memvalidasi data (Nama, NPM, Lokasi, dan Foto).
|
||||||
|
4. Klik **Submit**: Data dikonversi ke JSON dan dikirim ke Webhook.
|
||||||
|
5. Jika berhasil (Response 200), data otomatis masuk ke menu **Riwayat**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔐 Izin Aplikasi (Permissions)
|
## 🔐 Izin Aplikasi (Permissions)
|
||||||
Aplikasi memerlukan izin berikut:
|
Aplikasi memerlukan akses berikut:
|
||||||
- `ACCESS_FINE_LOCATION`
|
- `android.permission.CAMERA`: Untuk mengambil foto selfie.
|
||||||
- `ACCESS_COARSE_LOCATION`
|
- `android.permission.ACCESS_FINE_LOCATION`: Untuk validasi lokasi presisi.
|
||||||
- `CAMERA`
|
- `android.permission.INTERNET`: Untuk mengirim data absensi ke n8n.
|
||||||
- `INTERNET`
|
|
||||||
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📂 Mockup
|
## ⚠️ Disclaimer & Catatan
|
||||||

|
- **PENGGUNAAN AI**: Proyek ini dikembangkan dengan bantuan **Kecerdasan Buatan (AI)** (ChatGPT/Claude) dalam proses debugging Gradle, perbaikan error `XMLStreamException`, dan penyusunan struktur pengiriman data JSON pada `PreviewFragment.kt`.
|
||||||
|
- **PENGEMBANGAN**: Aplikasi ini merupakan hasil modifikasi intensif dari *starter project* yang disediakan untuk memenuhi standar capaian project akhir EAS 2025/2026.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 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 Mata Kuliah Pemrograman Mobile.*
|
||||||
@ -51,8 +51,25 @@ dependencies {
|
|||||||
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 ("com.google.android.material:material:1.11.0")
|
||||||
|
|
||||||
|
|
||||||
// Location (GPS)
|
// Location (GPS)
|
||||||
implementation("com.google.android.gms:play-services-location:21.0.1")
|
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||||
|
// Navigation Component
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
||||||
|
|
||||||
|
// ViewModel & LiveData
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
|
||||||
|
|
||||||
|
// Google Play Services Location
|
||||||
|
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||||
|
|
||||||
|
// Gson untuk parsing JSON
|
||||||
|
implementation("com.google.code.gson:gson:2.10.1")
|
||||||
|
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
@ -1,271 +1,24 @@
|
|||||||
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 androidx.appcompat.app.AppCompatActivity
|
||||||
import android.util.Base64
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import android.widget.Toast
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.activity.ComponentActivity
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
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 org.json.JSONObject
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
/* ================= UTIL ================= */
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
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-s.pii.or.id/webhook/dcef8a16-d333-4259-a201-bd12ba780761")
|
|
||||||
val conn = url.openConnection() as HttpURLConnection
|
|
||||||
|
|
||||||
conn.requestMethod = "POST"
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json")
|
|
||||||
conn.doOutput = true
|
|
||||||
|
|
||||||
val json = JSONObject().apply {
|
|
||||||
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 (e: Exception) {
|
|
||||||
context.runOnUiThread {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Gagal kirim ke server",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= ACTIVITY ================= */
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
setContent {
|
// Setup Navigation
|
||||||
SistemAkademikTheme {
|
val navHostFragment = supportFragmentManager
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
AbsensiScreen(
|
val navController = navHostFragment.navController
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
activity = this
|
// Setup Bottom Navigation
|
||||||
)
|
val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
|
||||||
}
|
bottomNav.setupWithNavController(navController)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= UI ================= */
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AbsensiScreen(
|
|
||||||
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?.get("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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== Request Awal ===== */
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
locationPermissionLauncher.launch(
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== UI ===== */
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp),
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "Absensi Akademik",
|
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (latitude != null && longitude != null && foto != null) {
|
|
||||||
kirimKeN8n(
|
|
||||||
activity,
|
|
||||||
latitude!!,
|
|
||||||
longitude!!,
|
|
||||||
foto!!
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Lokasi atau foto belum lengkap",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Kirim Absensi")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.adapter
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import id.ac.ubharajaya.sistemakademik.R
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.AbsensiData
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class HistoryAdapter(
|
||||||
|
private val dataList: List<AbsensiData>
|
||||||
|
) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
val ivThumbnail: ImageView = view.findViewById(R.id.ivThumbnail)
|
||||||
|
val tvNama: TextView = view.findViewById(R.id.tvHistoryNama)
|
||||||
|
val tvTanggal: TextView = view.findViewById(R.id.tvHistoryTanggal)
|
||||||
|
val tvLokasi: TextView = view.findViewById(R.id.tvHistoryLokasi)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_history, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val data = dataList[position]
|
||||||
|
|
||||||
|
// Set foto thumbnail
|
||||||
|
val bitmap = base64ToBitmap(data.fotoBase64)
|
||||||
|
holder.ivThumbnail.setImageBitmap(bitmap)
|
||||||
|
|
||||||
|
// Set nama
|
||||||
|
holder.tvNama.text = data.nama
|
||||||
|
|
||||||
|
// Set tanggal
|
||||||
|
val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale("id", "ID"))
|
||||||
|
holder.tvTanggal.text = "📅 ${dateFormat.format(Date(data.timestamp))}"
|
||||||
|
|
||||||
|
// Set lokasi
|
||||||
|
holder.tvLokasi.text = "📍 Lat: ${data.latitude}, Lon: ${data.longitude}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount() = dataList.size
|
||||||
|
|
||||||
|
private fun base64ToBitmap(base64: String): Bitmap {
|
||||||
|
val decodedBytes = Base64.decode(base64, Base64.NO_WRAP)
|
||||||
|
return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.data
|
||||||
|
|
||||||
|
data class MataKuliah(
|
||||||
|
val kode: String,
|
||||||
|
val nama: String
|
||||||
|
)
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.data
|
||||||
|
data class UserProfile(
|
||||||
|
var nama: String = "",
|
||||||
|
var npm: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AbsensiData(
|
||||||
|
val nama: String,
|
||||||
|
val npm: String,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val timestamp: Long,
|
||||||
|
val fotoBase64: String,
|
||||||
|
|
||||||
|
// 🔽 FIELD BARU (WAJIB DITARUH SEBELUM DEFAULT VALUE)
|
||||||
|
val kodeMatkul: String,
|
||||||
|
val namaMatkul: String,
|
||||||
|
|
||||||
|
// 🔽 FIELD DENGAN DEFAULT VALUE HARUS PALING BAWAH
|
||||||
|
val alamat: String = "Alamat tidak tersedia"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Singleton untuk menyimpan data sementara
|
||||||
|
object DataHolder {
|
||||||
|
var userProfile: UserProfile = UserProfile()
|
||||||
|
var currentAbsensi: AbsensiData? = null
|
||||||
|
val historyList = mutableListOf<AbsensiData>()
|
||||||
|
}
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.ui.fragments
|
||||||
|
|
||||||
|
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.provider.MediaStore
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.gms.location.LocationServices
|
||||||
|
import id.ac.ubharajaya.sistemakademik.R
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.AbsensiData
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.DataHolder
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.MataKuliah
|
||||||
|
import id.ac.ubharajaya.sistemakademik.utils.DummyData
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
|
class AbsensiFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var ivPreview: ImageView
|
||||||
|
private lateinit var tvLokasi: TextView
|
||||||
|
private lateinit var btnAmbilFoto: Button
|
||||||
|
private lateinit var btnLanjut: Button
|
||||||
|
private lateinit var spinnerMatkul: Spinner
|
||||||
|
|
||||||
|
private var foto: Bitmap? = null
|
||||||
|
private var latitude: Double? = null
|
||||||
|
private var longitude: Double? = null
|
||||||
|
private var selectedMatkul: MataKuliah? = null
|
||||||
|
|
||||||
|
private val fusedLocationClient by lazy {
|
||||||
|
LocationServices.getFusedLocationProviderClient(requireActivity())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Permission Launcher =====
|
||||||
|
private val locationPermissionLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) ambilLokasiGPS()
|
||||||
|
else Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraPermissionLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) bukaKamera()
|
||||||
|
else Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cameraLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val bitmap = result.data?.extras?.get("data") as? Bitmap
|
||||||
|
bitmap?.let {
|
||||||
|
foto = it
|
||||||
|
ivPreview.setImageBitmap(it)
|
||||||
|
cekKelengkapanData()
|
||||||
|
Toast.makeText(context, "Foto berhasil diambil!", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View = inflater.inflate(R.layout.fragment_absensi, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
ivPreview = view.findViewById(R.id.ivPreviewFoto)
|
||||||
|
tvLokasi = view.findViewById(R.id.tvLokasi)
|
||||||
|
btnAmbilFoto = view.findViewById(R.id.btnAmbilFoto)
|
||||||
|
btnLanjut = view.findViewById(R.id.btnLanjutPreview)
|
||||||
|
spinnerMatkul = view.findViewById(R.id.spinnerMatkul)
|
||||||
|
|
||||||
|
setupSpinnerMatkul()
|
||||||
|
requestLocationPermission()
|
||||||
|
|
||||||
|
btnAmbilFoto.setOnClickListener { requestCameraPermission() }
|
||||||
|
|
||||||
|
btnLanjut.setOnClickListener {
|
||||||
|
simpanDataSementara()
|
||||||
|
findNavController().navigate(R.id.action_absensi_to_preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Mata Kuliah =====
|
||||||
|
private fun setupSpinnerMatkul() {
|
||||||
|
val matkulList = DummyData.mataKuliahList
|
||||||
|
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
requireContext(),
|
||||||
|
android.R.layout.simple_spinner_dropdown_item,
|
||||||
|
matkulList.map { "${it.kode} - ${it.nama}" }
|
||||||
|
)
|
||||||
|
|
||||||
|
spinnerMatkul.adapter = adapter
|
||||||
|
|
||||||
|
spinnerMatkul.onItemSelectedListener =
|
||||||
|
object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(
|
||||||
|
parent: AdapterView<*>, view: View?, position: Int, id: Long
|
||||||
|
) {
|
||||||
|
selectedMatkul = matkulList[position]
|
||||||
|
cekKelengkapanData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||||
|
selectedMatkul = null
|
||||||
|
cekKelengkapanData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Location =====
|
||||||
|
private fun requestLocationPermission() {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ambilLokasiGPS()
|
||||||
|
} else {
|
||||||
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ambilLokasiGPS() {
|
||||||
|
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
|
||||||
|
if (location != null) {
|
||||||
|
latitude = location.latitude
|
||||||
|
longitude = location.longitude
|
||||||
|
tvLokasi.text =
|
||||||
|
"📍 GPS Ready\nLat: ${location.latitude}\nLon: ${location.longitude}"
|
||||||
|
cekKelengkapanData()
|
||||||
|
} else {
|
||||||
|
tvLokasi.text = "❌ Lokasi tidak tersedia"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Camera =====
|
||||||
|
private fun requestCameraPermission() {
|
||||||
|
if (ContextCompat.checkSelfPermission(
|
||||||
|
requireContext(),
|
||||||
|
Manifest.permission.CAMERA
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
bukaKamera()
|
||||||
|
} else {
|
||||||
|
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bukaKamera() {
|
||||||
|
cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Validation =====
|
||||||
|
private fun cekKelengkapanData() {
|
||||||
|
btnLanjut.isEnabled =
|
||||||
|
foto != null &&
|
||||||
|
latitude != null &&
|
||||||
|
longitude != null &&
|
||||||
|
selectedMatkul != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Save =====
|
||||||
|
private fun simpanDataSementara() {
|
||||||
|
val bitmap = foto ?: return
|
||||||
|
val lat = latitude ?: return
|
||||||
|
val lon = longitude ?: return
|
||||||
|
val matkul = selectedMatkul ?: return
|
||||||
|
|
||||||
|
DataHolder.currentAbsensi = AbsensiData(
|
||||||
|
nama = DataHolder.userProfile.nama,
|
||||||
|
npm = DataHolder.userProfile.npm,
|
||||||
|
latitude = lat,
|
||||||
|
longitude = lon,
|
||||||
|
timestamp = System.currentTimeMillis(),
|
||||||
|
fotoBase64 = bitmapToBase64(bitmap),
|
||||||
|
alamat = "Alamat akan diambil saat preview",
|
||||||
|
kodeMatkul = matkul.kode,
|
||||||
|
namaMatkul = matkul.nama
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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,57 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.ui.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import id.ac.ubharajaya.sistemakademik.R
|
||||||
|
import id.ac.ubharajaya.sistemakademik.adapter.HistoryAdapter
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.DataHolder
|
||||||
|
|
||||||
|
class HistoryFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var rvHistory: RecyclerView
|
||||||
|
private lateinit var tvEmpty: TextView
|
||||||
|
private lateinit var adapter: HistoryAdapter
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_history, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
rvHistory = view.findViewById(R.id.rvHistory)
|
||||||
|
tvEmpty = view.findViewById(R.id.tvEmptyState)
|
||||||
|
|
||||||
|
adapter = HistoryAdapter(DataHolder.historyList)
|
||||||
|
rvHistory.layoutManager = LinearLayoutManager(context)
|
||||||
|
rvHistory.adapter = adapter
|
||||||
|
|
||||||
|
cekDataKosong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
cekDataKosong()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cekDataKosong() {
|
||||||
|
if (DataHolder.historyList.isEmpty()) {
|
||||||
|
tvEmpty.visibility = View.VISIBLE
|
||||||
|
rvHistory.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
tvEmpty.visibility = View.GONE
|
||||||
|
rvHistory.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.ui.fragments
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import id.ac.ubharajaya.sistemakademik.R
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.DataHolder
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class PreviewFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var ivFoto: ImageView
|
||||||
|
private lateinit var tvNama: TextView
|
||||||
|
private lateinit var tvNpm: TextView
|
||||||
|
private lateinit var tvTimestamp: TextView
|
||||||
|
private lateinit var tvKoordinat: TextView
|
||||||
|
private lateinit var tvAlamat: TextView
|
||||||
|
private lateinit var tvStatus: TextView
|
||||||
|
private lateinit var btnSubmit: Button
|
||||||
|
private lateinit var btnKembali: Button
|
||||||
|
private lateinit var progressBar: ProgressBar
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_preview, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
ivFoto = view.findViewById(R.id.ivFotoPreview)
|
||||||
|
tvNama = view.findViewById(R.id.tvNama)
|
||||||
|
tvNpm = view.findViewById(R.id.tvNpm)
|
||||||
|
tvTimestamp = view.findViewById(R.id.tvTimestamp)
|
||||||
|
tvKoordinat = view.findViewById(R.id.tvKoordinat)
|
||||||
|
tvAlamat = view.findViewById(R.id.tvAlamat)
|
||||||
|
tvStatus = view.findViewById(R.id.tvStatusValidasi)
|
||||||
|
btnSubmit = view.findViewById(R.id.btnSubmitAbsensi)
|
||||||
|
btnKembali = view.findViewById(R.id.btnKembali)
|
||||||
|
progressBar = view.findViewById(R.id.progressBar)
|
||||||
|
|
||||||
|
tampilkanDataPreview()
|
||||||
|
|
||||||
|
btnSubmit.setOnClickListener {
|
||||||
|
kirimAbsensi()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnKembali.setOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tampilkanDataPreview() {
|
||||||
|
val absensi = DataHolder.currentAbsensi ?: run {
|
||||||
|
Toast.makeText(context, "Data tidak ditemukan", Toast.LENGTH_SHORT).show()
|
||||||
|
findNavController().popBackStack()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tampilkan foto
|
||||||
|
val bitmap = base64ToBitmap(absensi.fotoBase64)
|
||||||
|
ivFoto.setImageBitmap(bitmap)
|
||||||
|
|
||||||
|
// Tampilkan data mahasiswa
|
||||||
|
tvNama.text = "👤 Nama: ${absensi.nama}"
|
||||||
|
tvNpm.text = "🎓 NPM: ${absensi.npm}"
|
||||||
|
|
||||||
|
// Tampilkan timestamp
|
||||||
|
val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm:ss", Locale("id", "ID"))
|
||||||
|
val tanggal = dateFormat.format(Date(absensi.timestamp))
|
||||||
|
tvTimestamp.text = "⏰ $tanggal WIB"
|
||||||
|
|
||||||
|
// Tampilkan koordinat
|
||||||
|
tvKoordinat.text = "🌍 Lat: ${absensi.latitude}, Lon: ${absensi.longitude}"
|
||||||
|
|
||||||
|
// Tampilkan alamat (dummy dulu)
|
||||||
|
tvAlamat.text = "📮 ${absensi.alamat}"
|
||||||
|
|
||||||
|
// Status validasi (dummy - selalu valid)
|
||||||
|
tvStatus.text = "✅ Dalam Radius Kampus (0m dari lokasi)"
|
||||||
|
tvStatus.setTextColor(resources.getColor(android.R.color.holo_green_dark, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun base64ToBitmap(base64: String): Bitmap {
|
||||||
|
val decodedBytes = Base64.decode(base64, Base64.NO_WRAP)
|
||||||
|
return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun kirimAbsensi() {
|
||||||
|
val absensi = DataHolder.currentAbsensi ?: return
|
||||||
|
|
||||||
|
// Tampilkan loading
|
||||||
|
progressBar.visibility = View.VISIBLE
|
||||||
|
btnSubmit.isEnabled = false
|
||||||
|
btnKembali.isEnabled = false
|
||||||
|
|
||||||
|
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 = JSONObject().apply {
|
||||||
|
|
||||||
|
// 1️⃣ HARUS ADA
|
||||||
|
put("timestamp", absensi.timestamp)
|
||||||
|
|
||||||
|
// 2️⃣ IP address (kalau belum ada, kirim dummy)
|
||||||
|
put("ip_addr", "android")
|
||||||
|
|
||||||
|
// 3️⃣ Identitas
|
||||||
|
put("npm", absensi.npm)
|
||||||
|
put("nama", absensi.nama)
|
||||||
|
|
||||||
|
// 4️⃣ Lokasi
|
||||||
|
put("latitude", absensi.latitude)
|
||||||
|
put("longitude", absensi.longitude)
|
||||||
|
|
||||||
|
// 5️⃣ MATA KULIAH (INI YANG KOSONG KEMARIN)
|
||||||
|
put("mata_kuliah", absensi.namaMatkul)
|
||||||
|
// atau kalau mau sekalian kode:
|
||||||
|
// put("mata_kuliah", "${absensi.kodeMatkul} - ${absensi.namaMatkul}")
|
||||||
|
|
||||||
|
// 6️⃣ Photo (boleh string apa aja / URL / label)
|
||||||
|
put("photo", "camera")
|
||||||
|
|
||||||
|
// 7️⃣ Status
|
||||||
|
put("status", "hadir")
|
||||||
|
|
||||||
|
// 8️⃣ FOTO BASE64
|
||||||
|
put("foto_base64", absensi.fotoBase64)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
conn.outputStream.use {
|
||||||
|
it.write(json.toString().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
btnSubmit.isEnabled = true
|
||||||
|
btnKembali.isEnabled = true
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
|
Toast.makeText(context, "✅ Absensi berhasil dikirim!", Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
|
// Simpan ke history
|
||||||
|
DataHolder.historyList.add(0, absensi)
|
||||||
|
|
||||||
|
// Kembali ke halaman absensi
|
||||||
|
findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "❌ Gagal kirim (Code: $responseCode)", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
progressBar.visibility = View.GONE
|
||||||
|
btnSubmit.isEnabled = true
|
||||||
|
btnKembali.isEnabled = true
|
||||||
|
Toast.makeText(context, "❌ Error: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.ui.fragments
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import id.ac.ubharajaya.sistemakademik.R
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.DataHolder
|
||||||
|
|
||||||
|
class ProfileFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var etNama: TextInputEditText
|
||||||
|
private lateinit var etNpm: TextInputEditText
|
||||||
|
private lateinit var btnSimpan: Button
|
||||||
|
private lateinit var tvInfo: TextView
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_profile, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
etNama = view.findViewById(R.id.etNama)
|
||||||
|
etNpm = view.findViewById(R.id.etNpm)
|
||||||
|
btnSimpan = view.findViewById(R.id.btnSimpanProfile)
|
||||||
|
tvInfo = view.findViewById(R.id.tvProfileInfo)
|
||||||
|
|
||||||
|
// Load data yang sudah disimpan
|
||||||
|
etNama.setText(DataHolder.userProfile.nama)
|
||||||
|
etNpm.setText(DataHolder.userProfile.npm)
|
||||||
|
|
||||||
|
if (DataHolder.userProfile.nama.isNotEmpty()) {
|
||||||
|
tampilkanInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSimpan.setOnClickListener {
|
||||||
|
val nama = etNama.text.toString().trim()
|
||||||
|
val npm = etNpm.text.toString().trim()
|
||||||
|
|
||||||
|
if (nama.isEmpty() || npm.isEmpty()) {
|
||||||
|
Toast.makeText(context, "Nama dan NPM harus diisi!", Toast.LENGTH_SHORT).show()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan ke DataHolder
|
||||||
|
DataHolder.userProfile.nama = nama
|
||||||
|
DataHolder.userProfile.npm = npm
|
||||||
|
|
||||||
|
Toast.makeText(context, "Profile berhasil disimpan!", Toast.LENGTH_SHORT).show()
|
||||||
|
tampilkanInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tampilkanInfo() {
|
||||||
|
tvInfo.visibility = View.VISIBLE
|
||||||
|
tvInfo.text = "✅ Profile tersimpan\n${DataHolder.userProfile.nama}\n${DataHolder.userProfile.npm}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package id.ac.ubharajaya.sistemakademik.utils
|
||||||
|
|
||||||
|
import id.ac.ubharajaya.sistemakademik.data.MataKuliah
|
||||||
|
|
||||||
|
object DummyData {
|
||||||
|
val mataKuliahList = listOf(
|
||||||
|
MataKuliah("IF301", "Pemrograman Mobile"),
|
||||||
|
MataKuliah("IF302", "Kecerdasan Buatan"),
|
||||||
|
MataKuliah("IF303", "Basis Data")
|
||||||
|
)
|
||||||
|
}
|
||||||
29
app/src/main/res/layout/activity_main.xml
Normal file
29
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/nav_host_fragment"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:defaultNavHost="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:navGraph="@navigation/nav_graph" />
|
||||||
|
|
||||||
|
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
android:id="@+id/bottom_navigation"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:menu="@menu/bottom_nav_menu" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
58
app/src/main/res/layout/fragment_absensi.xml
Normal file
58
app/src/main/res/layout/fragment_absensi.xml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Ambil Absensi"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="32dp"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivPreviewFoto"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:background="@android:color/darker_gray"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:layout_marginBottom="24dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLokasi"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Menunggu GPS..."
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:layout_marginBottom="32dp"/>
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerMatkul"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"/>
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnAmbilFoto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:text="📷 Ambil Foto"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnLanjutPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:text="Lanjut ke Preview"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:enabled="false"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
32
app/src/main/res/layout/fragment_history.xml
Normal file
32
app/src/main/res/layout/fragment_history.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Riwayat Absensi"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvHistory"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvEmptyState"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Belum ada riwayat absensi"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="100dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
176
app/src/main/res/layout/fragment_preview.xml
Normal file
176
app/src/main/res/layout/fragment_preview.xml
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Preview Absensi"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="24dp"/>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="📸 Foto Absensi"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="12dp"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivFotoPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="300dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@android:color/darker_gray"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="👤 Data Mahasiswa"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvNama"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Nama: -"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvNpm"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="NPM: -"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="📍 Lokasi & Waktu"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTimestamp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⏰ -"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvKoordinat"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="🌍 -"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAlamat"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="📮 -"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvStatusValidasi"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="✅ Dalam Radius Kampus"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#4CAF50"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSubmitAbsensi"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:text="✅ Submit Absensi"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginTop="8dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnKembali"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:text="🔄 Ambil Ulang Foto"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
78
app/src/main/res/layout/fragment_profile.xml
Normal file
78
app/src/main/res/layout/fragment_profile.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
app:cardCornerRadius="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Profil Mahasiswa"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginBottom="24dp"/>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Nama Lengkap"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etNama"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPersonName"/>
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="NPM"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etNpm"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"/>
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSimpanProfile"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:text="Simpan Profile"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:layout_marginTop="24dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProfileInfo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
59
app/src/main/res/layout/item_history.xml
Normal file
59
app/src/main/res/layout/item_history.xml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:cardElevation="4dp"
|
||||||
|
app:cardCornerRadius="12dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivThumbnail"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:background="@android:color/darker_gray"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHistoryNama"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Nama"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHistoryTanggal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Tanggal"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:layout_marginTop="4dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHistoryLokasi"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lokasi"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:layout_marginTop="4dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
15
app/src/main/res/menu/bottom_nav_menu.xml
Normal file
15
app/src/main/res/menu/bottom_nav_menu.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/profileFragment"
|
||||||
|
android:title="Profil"
|
||||||
|
android:icon="@android:drawable/ic_menu_myplaces" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/absensiFragment"
|
||||||
|
android:title="Absensi"
|
||||||
|
android:icon="@android:drawable/ic_menu_camera" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/historyFragment"
|
||||||
|
android:title="Riwayat"
|
||||||
|
android:icon="@android:drawable/ic_menu_recent_history" />
|
||||||
|
</menu>
|
||||||
31
app/src/main/res/navigation/nav_graph.xml
Normal file
31
app/src/main/res/navigation/nav_graph.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/nav_graph"
|
||||||
|
app:startDestination="@id/profileFragment">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/profileFragment"
|
||||||
|
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.ProfileFragment"
|
||||||
|
android:label="Profil" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/absensiFragment"
|
||||||
|
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.AbsensiFragment"
|
||||||
|
android:label="Absensi">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_absensi_to_preview"
|
||||||
|
app:destination="@id/previewFragment" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/previewFragment"
|
||||||
|
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.PreviewFragment"
|
||||||
|
android:label="Preview" />
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/historyFragment"
|
||||||
|
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.HistoryFragment"
|
||||||
|
android:label="Riwayat" />
|
||||||
|
|
||||||
|
</navigation>
|
||||||
19
app/src/main/res/values-night/themes.xml
Normal file
19
app/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.SistemAkademik"
|
||||||
|
parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
|
||||||
|
<item name="textAppearanceBody1">
|
||||||
|
@style/TextAppearance.MaterialComponents.Body1
|
||||||
|
</item>
|
||||||
|
<item name="textAppearanceBody2">
|
||||||
|
@style/TextAppearance.MaterialComponents.Body2
|
||||||
|
</item>
|
||||||
|
<item name="textAppearanceButton">
|
||||||
|
@style/TextAppearance.MaterialComponents.Button
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@ -1,5 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.SistemAkademik" parent="android:Theme.Material.Light.NoActionBar" />
|
<style name="Theme.SistemAkademik"
|
||||||
|
parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
|
||||||
|
<!-- WAJIB buat TextInputLayout -->
|
||||||
|
<item name="textAppearanceBody1">
|
||||||
|
@style/TextAppearance.MaterialComponents.Body1
|
||||||
|
</item>
|
||||||
|
<item name="textAppearanceBody2">
|
||||||
|
@style/TextAppearance.MaterialComponents.Body2
|
||||||
|
</item>
|
||||||
|
<item name="textAppearanceButton">
|
||||||
|
@style/TextAppearance.MaterialComponents.Button
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
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