Compare commits

..

No commits in common. "main" and "redesign" have entirely different histories.

26 changed files with 99 additions and 196 deletions

View File

@ -1,71 +0,0 @@
# 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: 89 KiB

After

Width:  |  Height:  |  Size: 715 KiB

157
README.md
View File

@ -1,106 +1,95 @@
<div align="center">
# 📱 Aplikasi Absensi Akademik UBHARA Jaya
### Mobile (Kotlin) & Backend (Python Flask)
![Kotlin](https://img.shields.io/badge/Kotlin-20232A?style=for-the-badge&logo=kotlin&logoColor=7F52FF)
![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-4285F4?style=for-the-badge&logo=android&logoColor=white)
![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>
---
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
## 📌 Deskripsi Proyek
Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang mengimplementasikan sistem absensi akademik berbasis **Client-Server**.
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**.
Sistem ini dirancang untuk validitas tinggi dengan mengintegrasikan:
* ✅ **Geofencing (GPS)** untuk validasi lokasi kampus.
* ✅ **Face Detection (CameraX)** untuk validasi biometrik kehadiran.
* ✅ **Anti-Fraud** untuk mendeteksi penggunaan *Fake GPS*.
* ✅ **Otomatisasi** pelaporan real-time ke **N8N Webhook**.
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
2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi**
---
## 📸 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.
## 🎯 Tujuan Proyek
- Mengimplementasikan **Location-Based Service (LBS)** pada aplikasi mobile
- Mengintegrasikan **kamera perangkat** untuk dokumentasi absensi
- Mencegah kecurangan absensi (titip absen)
- Mengembangkan aplikasi mobile akademik berbasis Android
- Melatih kemampuan perancangan dan implementasi aplikasi mobile
---
## 🚀 Fitur Utama
| Fitur | Deskripsi |
| :--- | :--- |
| **📱 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. |
| **💻 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). |
- 🔐 **Login Pengguna (Mahasiswa)**
- 📍 **Pengambilan Koordinat Lokasi (Latitude & Longitude)**
- 🏫 **Validasi Lokasi Absensi (Radius Area)**
- 📸 **Pengambilan Foto Mahasiswa Saat Absensi**
- 🕒 **Pencatatan Waktu Absensi**
- 📄 **Riwayat Kehadiran Mahasiswa**
- ⚠️ **Notifikasi Absensi Ditolak jika Tidak Valid**
---
## 🗺️ Mekanisme Alur Absensi
### 🟢 1. Status "HADIR"
1. Mahasiswa memilih Jadwal Kelas aktif.
2. **Validasi Sistem:**
* 📍 Lokasi dalam radius **500m**?
* 🚫 Tidak ada aplikasi **Fake GPS** aktif?
3. **Validasi Wajah:** Kamera terbuka, shutter hanya aktif jika wajah terdeteksi.
4. Data (Lokasi + Foto Wajah) dikirim ke server.
### 🟡 2. Status "SAKIT / IZIN"
1. Mahasiswa memilih status Sakit/Izin.
2. **Bypass Lokasi:** Validasi radius dilewati.
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"**.
## 🗺️ 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**
---
## 🛠️ Tech Stack
| Komponen | Teknologi |
| :--- | :--- |
| **Mobile Platform** | Android (Kotlin) |
| **UI Framework** | Jetpack Compose (Material3) |
| **Camera & AI** | CameraX + ML Kit (Face Analysis) |
| **Backend** | Python Flask |
| **Database** | MySQL |
| **Integrasi** | N8N Webhook |
| **AI Assistant** | Gemini & Claude (Development Tools) |
## 📸 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
---
## ⚙️ Konfigurasi & Instalasi
## 🛠️ Teknologi yang Digunakan
- **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)
---
### 1. Setup Backend
Pastikan server berjalan pada jaringan lokal:
```bash
# Install dependencies
pip install -r requirements.txt
## 🔐 Izin Aplikasi (Permissions)
Aplikasi memerlukan izin berikut:
- `ACCESS_FINE_LOCATION`
- `ACCESS_COARSE_LOCATION`
- `CAMERA`
- `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.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@ -88,14 +88,14 @@ import androidx.compose.ui.unit.sp
object AppConstants {
// 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://192.168.xxx.xxx:5000" // Untuk device fisik
const val BASE_URL = "http://192.168.100.99:5000" // Untuk device fisik
// Koordinat Kampus (UBHARA Jaya)
const val KAMPUS_LATITUDE = -6.223325
const val KAMPUS_LONGITUDE = 107.009406
// Koordinat Device Saat ini (Untuk Testing)
// const val KAMPUS_LATITUDE = -6.239513
// const val KAMPUS_LONGITUDE = 107.089676
// const val KAMPUS_LATITUDE = -6.223325
// const val KAMPUS_LONGITUDE = 107.009406
// Koordinat Saat ini
const val KAMPUS_LATITUDE = -6.239513
const val KAMPUS_LONGITUDE = 107.089676
const val RADIUS_METER = 500.0

View File

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

@ -0,0 +1,6 @@
<?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: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

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

View File

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