Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3bb31488c | |||
| 59b67b428a | |||
| aedaea310d | |||
| 29e8577d0a | |||
| 4e4160f9f1 | |||
| 8ea8dc66a6 | |||
| 17af72cbab | |||
| 14c4c19a4f |
71
CHANGELOG.md
Normal 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.
|
||||
BIN
Mockup.png
|
Before Width: | Height: | Size: 715 KiB After Width: | Height: | Size: 89 KiB |
159
README.md
@ -1,95 +1,106 @@
|
||||
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)
|
||||
<div align="center">
|
||||
|
||||
## 📌 Deskripsi Proyek
|
||||
Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang bertujuan untuk membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**.
|
||||
# 📱 Aplikasi Absensi Akademik UBHARA Jaya
|
||||
### Mobile (Kotlin) & Backend (Python Flask)
|
||||
|
||||
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**
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[📋 Lihat Changelog](./CHANGELOG.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 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
|
||||
## 📌 Deskripsi Proyek
|
||||
Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang mengimplementasikan sistem absensi akademik berbasis **Client-Server**.
|
||||
|
||||
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**.
|
||||
|
||||
---
|
||||
|
||||
## 📸 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
|
||||
- 🔐 **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**
|
||||
|
||||
| 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). |
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ 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**
|
||||
## 🗺️ 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"**.
|
||||
|
||||
---
|
||||
|
||||
## 📸 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
|
||||
## 🛠️ 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) |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 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
|
||||
## ⚙️ Konfigurasi & Instalasi
|
||||
|
||||
---
|
||||
### Prasyarat
|
||||
* Python 3.x
|
||||
* Android Studio (Ladybug/Latest)
|
||||
* Koneksi jaringan yang sama (Laptop & HP)
|
||||
|
||||
## 🔐 Izin Aplikasi (Permissions)
|
||||
Aplikasi memerlukan izin berikut:
|
||||
- `ACCESS_FINE_LOCATION`
|
||||
- `ACCESS_COARSE_LOCATION`
|
||||
- `CAMERA`
|
||||
- `INTERNET`
|
||||
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
|
||||
### 1. Setup Backend
|
||||
Pastikan server berjalan pada jaringan lokal:
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
---
|
||||
|
||||
## 📂 Mockup
|
||||

|
||||
|
||||
## 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
|
||||
# Jalankan server
|
||||
python app.py
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
@ -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.100.99:5000" // Untuk device fisik
|
||||
const val BASE_URL = "http://192.168.xxx.xxx:5000" // Untuk device fisik
|
||||
|
||||
// Koordinat Kampus (UBHARA Jaya)
|
||||
// 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 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 RADIUS_METER = 500.0
|
||||
|
||||
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 24 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@ -299,7 +299,6 @@ 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
|
||||
@ -319,11 +318,17 @@ def get_jadwal_today():
|
||||
""", (hari_ini, mhs['jurusan'], mhs['semester']))
|
||||
jadwal = cur.fetchall()
|
||||
|
||||
# === FIX ERROR TIMEDELTA DISINI ===
|
||||
for j in jadwal:
|
||||
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_mulai (timedelta) ke string "HH:MM:SS"
|
||||
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("""
|
||||
SELECT status FROM absensi
|
||||
WHERE id_mahasiswa=%s AND id_jadwal=%s AND DATE(timestamp)=CURDATE()
|
||||
@ -341,24 +346,32 @@ def get_jadwal_today():
|
||||
cur.close(); conn.close()
|
||||
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'])
|
||||
@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 item['jam_mulai']: item['jam_mulai'] = str(item['jam_mulai'])
|
||||
if item['jam_selesai']: item['jam_selesai'] = str(item['jam_selesai'])
|
||||
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'])
|
||||
|
||||
cursor.close(); connection.close()
|
||||
return jsonify({'data': history}), 200
|
||||
|
||||
|
||||