Compare commits

..

8 Commits

26 changed files with 197 additions and 100 deletions

71
CHANGELOG.md Normal file
View File

@ -0,0 +1,71 @@
# Changelog
Semua perubahan penting pada proyek Aplikasi Absensi Akademik didokumentasikan dalam file ini.
## [Unreleased] - Versi Terbaru (Redesign & Backend Integration)
Perubahan besar-besaran dilakukan pada arsitektur aplikasi, beralih dari aplikasi *single-screen* sederhana menjadi aplikasi manajemen akademik yang komprehensif dengan autentikasi dan validasi keamanan.
### ✨ Fitur Baru (Added)
- **Sistem Autentikasi**:
- Menambahkan layar **Login** dan **Register**.
- Integrasi **JWT Authentication** (penyimpanan token via `SharedPreferences`).
- Manajemen sesi pengguna (Auto-login jika token tersimpan).
- **Manajemen Jadwal & Mata Kuliah**:
- Menambahkan fitur pengambilan data **Jadwal Kuliah** hari ini dari database.
- Dropdown pemilihan mata kuliah saat melakukan absensi.
- **Validasi Keamanan Tingkat Lanjut**:
- **Geofencing**: Validasi lokasi dalam radius 500m dari koordinat kampus.
- **Anti-Fake GPS**: Logika deteksi aplikasi *Mock Location* untuk mencegah kecurangan.
- **Face Detection**: Integrasi **CameraX + ML Kit** (tersirat dalam logika kamera baru) untuk mewajibkan deteksi wajah saat status "HADIR".
- **Fitur Kamera Kustom**:
- Mengganti intent kamera bawaan (`MediaStore`) dengan **CameraX** tertanam dalam UI.
- Fitur overlay kamera untuk panduan posisi wajah/dokumen.
- Dukungan kamera depan (selfie) dan belakang (dokumen).
- **Riwayat & Profil**:
- Menambahkan tab **Riwayat Absensi** untuk melihat log kehadiran dan bukti foto.
- Menambahkan tab **Profil** yang menampilkan data mahasiswa dan fitur Logout.
- **Multi-Status Absensi**:
- Dukungan untuk status **HADIR**, **SAKIT**, dan **IZIN**.
- Logika validasi berbeda untuk setiap status (Sakit/Izin tidak butuh radius lokasi).
### 🎨 Antarmuka & UX (Changed)
- **UI Overhaul (Material3)**:
- Redesign total menggunakan **Jetpack Compose Material3**.
- Penerapan tema identitas kampus (**Gold & Maroon**).
- Penggunaan komponen UI modern: `Card`, `NavigationBar` (Bottom Nav), `Gradient Button`.
- **Navigasi**:
- Implementasi **Bottom Navigation Bar** dengan 4 menu utama (Absensi, Kelas, Riwayat, Profil).
- Transisi antar layar (Login <-> Main <-> Register).
- **Feedback User**:
- Loading indicator saat proses API berjalan.
- Dialog error dan sukses yang lebih informatif dibandingkan `Toast` sederhana.
### ⚙️ Teknis & Backend (Changed)
- **Migrasi API**:
- **Sebelumnya**: Mengirim data hardcoded langsung ke Webhook N8N via `HttpURLConnection`.
- **Sekarang**: Berkomunikasi dengan **Backend Python Flask** (`/api/auth`, `/api/absensi`, `/api/jadwal`). Backend yang kemudian meneruskan data ke N8N.
- **Struktur Kode**:
- Refactoring dari *Single Activity Monolith* menjadi struktur modular.
- Pemisahan logic ke dalam:
- `AppConstants` (Konfigurasi URL & Koordinat).
- `UserPreferences` (Manajemen sesi lokal).
- `Data Classes` (Mahasiswa, Jadwal, Riwayat).
- Helper functions (Bitmap Converter, Distance Calculation).
### 🔥 Dihapus (Removed)
- Menghapus pengiriman data hardcoded (NPM "12345") pada fungsi `kirimKeN8n`.
- Menghapus penggunaan `MediaStore.ACTION_IMAGE_CAPTURE` (Intent kamera eksternal).
- Menghapus tampilan *single-screen* sederhana tanpa navigasi.
---
## [Legacy] - Versi Awal (Sebelum Redesign)
Versi purwarupa (prototype) untuk pengujian fungsionalitas dasar.
### Fitur
- Pengambilan titik koordinat GPS sederhana (`FusedLocationProvider`).
- Pengambilan foto menggunakan aplikasi kamera bawaan HP (`Intent`).
- Pengiriman data dummy langsung ke Webhook N8N.
- Tampilan UI dasar menggunakan `Column` dan `Button` standar.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 89 KiB

159
README.md
View File

@ -1,95 +1,106 @@
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile) <div align="center">
## 📌 Deskripsi Proyek # 📱 Aplikasi Absensi Akademik UBHARA Jaya
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**. ### Mobile (Kotlin) & Backend (Python Flask)
Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa: ![Kotlin](https://img.shields.io/badge/Kotlin-20232A?style=for-the-badge&logo=kotlin&logoColor=7F52FF)
1. Berada pada **lokasi yang telah ditentukan**, dan ![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-4285F4?style=for-the-badge&logo=android&logoColor=white)
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi** ![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white)
![Flask](https://img.shields.io/badge/Flask-000000?style=for-the-badge&logo=flask&logoColor=white)
![MySQL](https://img.shields.io/badge/MySQL-00000F?style=for-the-badge&logo=mysql&logoColor=white)
![n8n](https://img.shields.io/badge/n8n-EA4B71?style=for-the-badge&logo=n8n&logoColor=white)
[📋 Lihat Changelog](./CHANGELOG.md)
</div>
--- ---
## 🎯 Tujuan Proyek ## 📌 Deskripsi Proyek
- Mengimplementasikan **Location-Based Service (LBS)** pada aplikasi mobile Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang mengimplementasikan sistem absensi akademik berbasis **Client-Server**.
- Mengintegrasikan **kamera perangkat** untuk dokumentasi absensi
- Mencegah kecurangan absensi (titip absen) Sistem ini dirancang untuk validitas tinggi dengan mengintegrasikan:
- Mengembangkan aplikasi mobile akademik berbasis Android * ✅ **Geofencing (GPS)** untuk validasi lokasi kampus.
- Melatih kemampuan perancangan dan implementasi aplikasi mobile * ✅ **Face Detection (CameraX)** untuk validasi biometrik kehadiran.
* ✅ **Anti-Fraud** untuk mendeteksi penggunaan *Fake GPS*.
* ✅ **Otomatisasi** pelaporan real-time ke **N8N Webhook**.
---
## 📸 Tampilan Aplikasi (Mockup)
<div align="center">
<img src="Mockup.png" alt="Mockup Aplikasi Absensi" width="800">
</div>
---
## 🎯 Tujuan & Capaian
Proyek ini bertujuan untuk melatih kemampuan **Fullstack Mobile Development** dengan fokus pada:
1. **REST API Architecture**: Komunikasi aman antara Android dan Python Flask.
2. **Biometric Validation**: Implementasi ML Kit untuk deteksi wajah.
3. **Security Awareness**: Pencegahan kecurangan lokasi (*Mock Location*).
4. **Database Logic**: Penanganan status otomatis (*Auto-Alfa*) di sisi server.
--- ---
## 🚀 Fitur Utama ## 🚀 Fitur Utama
- 🔐 **Login Pengguna (Mahasiswa)**
- 📍 **Pengambilan Koordinat Lokasi (Latitude & Longitude)** | Fitur | Deskripsi |
- 🏫 **Validasi Lokasi Absensi (Radius Area)** | :--- | :--- |
- 📸 **Pengambilan Foto Mahasiswa Saat Absensi** | **📱 Sisi Mobile** | • **Smart Attendance:** Validasi radius & wajah real-time.<br>**Anti-Fraud:** Deteksi & blokir Fake GPS.<br>**Mode Izin/Sakit:** Upload bukti dokumen tanpa validasi radius.<br>**Riwayat:** Log kehadiran lengkap dengan foto bukti. |
- 🕒 **Pencatatan Waktu Absensi** | **💻 Sisi Backend** | • **JWT Auth:** Keamanan token pada setiap request.<br>**Auto-Alfa:** Menandai "TIDAK HADIR" otomatis jika telat.<br>**N8N Integration:** Kirim data valid ke webhook untuk notifikasi.<br>**Image Compression:** Optimasi penyimpanan foto (Base64). |
- 📄 **Riwayat Kehadiran Mahasiswa**
- ⚠️ **Notifikasi Absensi Ditolak jika Tidak Valid**
--- ---
## 🗺️ Mekanisme Absensi Berbasis Lokasi dan Foto ## 🗺️ Mekanisme Alur Absensi
1. Mahasiswa melakukan **login**
2. Memilih menu **Absensi** ### 🟢 1. Status "HADIR"
3. Sistem meminta: 1. Mahasiswa memilih Jadwal Kelas aktif.
- Izin **akses lokasi** 2. **Validasi Sistem:**
- Izin **akses kamera** * 📍 Lokasi dalam radius **500m**?
4. Aplikasi mengambil: * 🚫 Tidak ada aplikasi **Fake GPS** aktif?
- 📍 **Koordinat lokasi mahasiswa** 3. **Validasi Wajah:** Kamera terbuka, shutter hanya aktif jika wajah terdeteksi.
- 📸 **Foto mahasiswa secara real-time** 4. Data (Lokasi + Foto Wajah) dikirim ke server.
5. Sistem melakukan validasi:
- Lokasi berada dalam **radius absensi** ### 🟡 2. Status "SAKIT / IZIN"
- Foto berhasil diambil 1. Mahasiswa memilih status Sakit/Izin.
6. Jika valid → **Absensi berhasil** 2. **Bypass Lokasi:** Validasi radius dilewati.
7. Jika tidak valid → **Absensi ditolak** 3. **Dokumentasi:** Kamera dokumen terbuka (Tanpa deteksi wajah).
4. Foto surat/bukti dikirim ke server.
### 🔴 3. Logika Auto-Alfa (Server Side)
> *Background Process*
* Server memantau jadwal kuliah.
* Jika `jam_selesai < jam_sekarang` dan tidak ada data masuk ➔ Input otomatis **"TIDAK HADIR"**.
--- ---
## 📸 Pengambilan Foto Saat Absensi ## 🛠️ Tech Stack
- Foto diambil menggunakan **kamera depan (selfie)**
- Foto hanya dapat diambil **saat proses absensi** | Komponen | Teknologi |
- Foto disimpan sebagai **bukti kehadiran** | :--- | :--- |
- Foto dapat digunakan untuk: | **Mobile Platform** | Android (Kotlin) |
- Verifikasi manual oleh dosen | **UI Framework** | Jetpack Compose (Material3) |
- Dokumentasi akademik | **Camera & AI** | CameraX + ML Kit (Face Analysis) |
| **Backend** | Python Flask |
| **Database** | MySQL |
| **Integrasi** | N8N Webhook |
| **AI Assistant** | Gemini & Claude (Development Tools) |
--- ---
## 🛠️ Teknologi yang Digunakan ## ⚙️ Konfigurasi & Instalasi
- **Platform** : Android
- **Bahasa Pemrograman** : Kotlin / Java
- **Location Service** :
- Google Maps API
- Fused Location Provider
- **Camera API** : CameraX / Camera2
- **Database** : Firebase / SQLite / MySQL
- **Storage** : Firebase Storage / Local Storage
- **IDE** : Android Studio
--- ### Prasyarat
* Python 3.x
* Android Studio (Ladybug/Latest)
* Koneksi jaringan yang sama (Laptop & HP)
## 🔐 Izin Aplikasi (Permissions) ### 1. Setup Backend
Aplikasi memerlukan izin berikut: Pastikan server berjalan pada jaringan lokal:
- `ACCESS_FINE_LOCATION` ```bash
- `ACCESS_COARSE_LOCATION` # Install dependencies
- `CAMERA` pip install -r requirements.txt
- `INTERNET`
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
--- # Jalankan server
python app.py
## 📂 Mockup
![mockup](Mockup.png)
## Catatan:
- Kembangkan project dari starter yang sudah disediakan, tidak membuat dari awal.
- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS.
## Pengecekan:
- https://ntfy.ubharajaya.ac.id/EAS
- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0
## Webhook:
- test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254
- production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -88,14 +88,14 @@ import androidx.compose.ui.unit.sp
object AppConstants { object AppConstants {
// Backend API URL - GANTI SESUAI SERVER ANDA // Backend API URL - GANTI SESUAI SERVER ANDA
// const val BASE_URL = "http://10.0.2.2:5000" // Untuk emulator Android // const val BASE_URL = "http://10.0.2.2:5000" // Untuk emulator Android
const val BASE_URL = "http://192.168.100.99:5000" // Untuk device fisik const val BASE_URL = "http://192.168.xxx.xxx:5000" // Untuk device fisik
// Koordinat Kampus (UBHARA Jaya) // Koordinat Kampus (UBHARA Jaya)
// const val KAMPUS_LATITUDE = -6.223325 const val KAMPUS_LATITUDE = -6.223325
// const val KAMPUS_LONGITUDE = 107.009406 const val KAMPUS_LONGITUDE = 107.009406
// Koordinat Saat ini // Koordinat Device Saat ini (Untuk Testing)
const val KAMPUS_LATITUDE = -6.239513 // const val KAMPUS_LATITUDE = -6.239513
const val KAMPUS_LONGITUDE = 107.089676 // const val KAMPUS_LONGITUDE = 107.089676
const val RADIUS_METER = 500.0 const val RADIUS_METER = 500.0

View 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>

View 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>

View File

@ -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>

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -299,7 +299,6 @@ def submit_absensi():
def get_jadwal_today(): def get_jadwal_today():
try: try:
# 1. TRIGGER AUTO ALFA # 1. TRIGGER AUTO ALFA
# Jalankan pengecekan otomatis SEBELUM mengambil data jadwal
jalankan_auto_alfa() jalankan_auto_alfa()
# 2. Ambil Data Jadwal # 2. Ambil Data Jadwal
@ -319,11 +318,17 @@ def get_jadwal_today():
""", (hari_ini, mhs['jurusan'], mhs['semester'])) """, (hari_ini, mhs['jurusan'], mhs['semester']))
jadwal = cur.fetchall() jadwal = cur.fetchall()
# === FIX ERROR TIMEDELTA DISINI ===
for j in jadwal: for j in jadwal:
if isinstance(j['jam_mulai'], timedelta): j['jam_mulai'] = str(j['jam_mulai']) # Ubah jam_mulai (timedelta) ke string "HH:MM:SS"
if isinstance(j['jam_selesai'], timedelta): j['jam_selesai'] = str(j['jam_selesai']) if isinstance(j.get('jam_mulai'), timedelta):
j['jam_mulai'] = str(j['jam_mulai'])
# Ambil Status Absensi (HADIR / TIDAK HADIR / SAKIT / IZIN) # Ubah jam_selesai (timedelta) ke string "HH:MM:SS"
if isinstance(j.get('jam_selesai'), timedelta):
j['jam_selesai'] = str(j['jam_selesai'])
# Cek Status Absensi
cur.execute(""" cur.execute("""
SELECT status FROM absensi SELECT status FROM absensi
WHERE id_mahasiswa=%s AND id_jadwal=%s AND DATE(timestamp)=CURDATE() WHERE id_mahasiswa=%s AND id_jadwal=%s AND DATE(timestamp)=CURDATE()
@ -341,24 +346,32 @@ def get_jadwal_today():
cur.close(); conn.close() cur.close(); conn.close()
return jsonify({'data': jadwal, 'hari': hari_ini}) return jsonify({'data': jadwal, 'hari': hari_ini})
except Exception as e: return jsonify({'error': str(e)}), 500 except Exception as e:
print(f"Error Jadwal: {e}") # Print error di terminal agar jelas
return jsonify({'error': str(e)}), 500
@app.route('/api/absensi/history', methods=['GET']) @app.route('/api/absensi/history', methods=['GET'])
@token_required @token_required
def get_history(): def get_history():
connection = get_db_connection() connection = get_db_connection()
cursor = connection.cursor(dictionary=True) cursor = connection.cursor(dictionary=True)
# Join jadwal untuk ambil jam
cursor.execute(""" cursor.execute("""
SELECT a.*, j.jam_mulai, j.jam_selesai SELECT a.*, j.jam_mulai, j.jam_selesai
FROM absensi a FROM absensi a
LEFT JOIN jadwal_kelas j ON a.id_jadwal = j.id_jadwal LEFT JOIN jadwal_kelas j ON a.id_jadwal = j.id_jadwal
WHERE a.id_mahasiswa = %s ORDER BY a.timestamp DESC WHERE a.id_mahasiswa = %s ORDER BY a.timestamp DESC
""", (request.user_data['id_mahasiswa'],)) """, (request.user_data['id_mahasiswa'],))
history = cursor.fetchall() history = cursor.fetchall()
# === FIX ERROR TIMEDELTA DISINI ===
for item in history: for item in history:
if item['jam_mulai']: item['jam_mulai'] = str(item['jam_mulai']) if isinstance(item.get('jam_mulai'), timedelta):
if item['jam_selesai']: item['jam_selesai'] = str(item['jam_selesai']) item['jam_mulai'] = str(item['jam_mulai'])
if isinstance(item.get('jam_selesai'), timedelta):
item['jam_selesai'] = str(item['jam_selesai'])
cursor.close(); connection.close() cursor.close(); connection.close()
return jsonify({'data': history}), 200 return jsonify({'data': history}), 200