""" Backend API untuk Aplikasi Absensi Akademik Python Flask + MySQL + JWT Authentication Requirements: pip install flask flask-cors mysql-connector-python PyJWT bcrypt python-dotenv File Structure: - app.py (main file) - .env (konfigurasi) - requirements.txt """ from flask import Flask, request, jsonify from flask_cors import CORS import mysql.connector from mysql.connector import Error import jwt import bcrypt from datetime import datetime, timedelta import os from functools import wraps import base64 import requests app = Flask(__name__) CORS(app) # ==================== KONFIGURASI ==================== # Ganti dengan konfigurasi MySQL Anda DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': '@Rique03', # Ganti dengan password MySQL Anda 'database': 'db_absensi_akademik' } # Secret key untuk JWT (GANTI dengan random string yang aman!) SECRET_KEY = 'ubhara-jaya-absensi-secret-2026-change-this' # ==================== DATABASE CONNECTION ==================== def get_db_connection(): """Membuat koneksi ke database MySQL""" try: connection = mysql.connector.connect(**DB_CONFIG) return connection except Error as e: print(f"Error connecting to MySQL: {e}") return None def init_database(): """Inisialisasi database dan tabel""" connection = get_db_connection() if connection is None: return cursor = connection.cursor() try: cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']}") cursor.execute(f"USE {DB_CONFIG['database']}") # Tabel Mahasiswa (sudah ada) cursor.execute(""" CREATE TABLE IF NOT EXISTS mahasiswa ( id_mahasiswa INT AUTO_INCREMENT PRIMARY KEY, npm VARCHAR(20) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, nama VARCHAR(100) NOT NULL, jenkel ENUM('L', 'P') NOT NULL, fakultas VARCHAR(100) NOT NULL, jurusan VARCHAR(100) NOT NULL, semester INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_npm (npm) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) # TABEL BARU: Mata Kuliah cursor.execute(""" CREATE TABLE IF NOT EXISTS mata_kuliah ( id_matkul INT AUTO_INCREMENT PRIMARY KEY, kode_matkul VARCHAR(20) UNIQUE NOT NULL, nama_matkul VARCHAR(100) NOT NULL, sks INT NOT NULL, semester INT NOT NULL, dosen VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_kode (kode_matkul) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) # TABEL BARU: Jadwal Kelas cursor.execute(""" CREATE TABLE IF NOT EXISTS jadwal_kelas ( id_jadwal INT AUTO_INCREMENT PRIMARY KEY, id_matkul INT NOT NULL, hari ENUM('Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu') NOT NULL, jam_mulai TIME NOT NULL, jam_selesai TIME NOT NULL, ruangan VARCHAR(50) NOT NULL, semester INT NOT NULL, jurusan VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (id_matkul) REFERENCES mata_kuliah(id_matkul) ON DELETE CASCADE, INDEX idx_hari (hari), INDEX idx_semester_jurusan (semester, jurusan) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) # Tabel Absensi (UPDATE: tambah kolom mata_kuliah) cursor.execute(""" CREATE TABLE IF NOT EXISTS absensi ( id_absensi INT AUTO_INCREMENT PRIMARY KEY, id_mahasiswa INT NOT NULL, npm VARCHAR(20) NOT NULL, nama VARCHAR(100) NOT NULL, id_jadwal INT NOT NULL, mata_kuliah VARCHAR(100) NOT NULL, latitude DECIMAL(10, 8) NOT NULL, longitude DECIMAL(11, 8) NOT NULL, timestamp DATETIME NOT NULL, photo LONGTEXT, foto_base64 LONGTEXT, status VARCHAR(20) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (id_mahasiswa) REFERENCES mahasiswa(id_mahasiswa) ON DELETE CASCADE, FOREIGN KEY (id_jadwal) REFERENCES jadwal_kelas(id_jadwal) ON DELETE CASCADE, INDEX idx_mahasiswa (id_mahasiswa), INDEX idx_npm (npm), INDEX idx_timestamp (timestamp), INDEX idx_status (status), INDEX idx_jadwal (id_jadwal) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) connection.commit() print("✅ Database dan tabel berhasil dibuat!") # INSERT DATA DUMMY MATA KULIAH (untuk testing) cursor.execute("SELECT COUNT(*) FROM mata_kuliah") if cursor.fetchone()[0] == 0: dummy_matkul = [ ('IF101', 'Pemrograman Mobile', 3, 5, 'Dr. Budi Santoso'), ('IF102', 'Basis Data Lanjut', 3, 5, 'Dr. Siti Aminah'), ('IF103', 'Jaringan Komputer', 3, 5, 'Dr. Ahmad Fauzi'), ('IF104', 'Kecerdasan Buatan', 3, 5, 'Dr. Rina Wati'), ] cursor.executemany(""" INSERT INTO mata_kuliah (kode_matkul, nama_matkul, sks, semester, dosen) VALUES (%s, %s, %s, %s, %s) """, dummy_matkul) connection.commit() print("✅ Data dummy mata kuliah berhasil ditambahkan!") # INSERT DATA DUMMY JADWAL KELAS (untuk testing) cursor.execute("SELECT COUNT(*) FROM jadwal_kelas") if cursor.fetchone()[0] == 0: dummy_jadwal = [ (1, 'Senin', '08:00:00', '10:30:00', 'Lab Komputer 1', 5, 'Informatika'), (2, 'Senin', '13:00:00', '15:30:00', 'Ruang 301', 5, 'Informatika'), (3, 'Selasa', '08:00:00', '10:30:00', 'Lab Jaringan', 5, 'Informatika'), (4, 'Rabu', '10:30:00', '13:00:00', 'Ruang 302', 5, 'Informatika'), (1, 'Kamis', '13:30:00', '16:00:00', 'Lab Komputer 2', 5, 'Informatika'), ] cursor.executemany(""" INSERT INTO jadwal_kelas (id_matkul, hari, jam_mulai, jam_selesai, ruangan, semester, jurusan) VALUES (%s, %s, %s, %s, %s, %s, %s) """, dummy_jadwal) connection.commit() print("✅ Data dummy jadwal kelas berhasil ditambahkan!") except Error as e: print(f"❌ Error creating tables: {e}") finally: cursor.close() connection.close() # ==================== JWT HELPER ==================== def generate_token(id_mahasiswa, npm): """Generate JWT token""" payload = { 'id_mahasiswa': id_mahasiswa, 'npm': npm, 'exp': datetime.utcnow() + timedelta(days=30) # Token berlaku 30 hari } return jwt.encode(payload, SECRET_KEY, algorithm='HS256') def token_required(f): """Decorator untuk endpoint yang memerlukan authentication""" @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization') if not token: return jsonify({'error': 'Token tidak ditemukan'}), 401 try: # Format: "Bearer " if token.startswith('Bearer '): token = token.split(' ')[1] data = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) request.user_data = data except jwt.ExpiredSignatureError: return jsonify({'error': 'Token sudah kadaluarsa'}), 401 except jwt.InvalidTokenError: return jsonify({'error': 'Token tidak valid'}), 401 return f(*args, **kwargs) return decorated # ==================== API ENDPOINTS ==================== @app.route('/api/health', methods=['GET']) def health_check(): """Health check endpoint""" return jsonify({ 'status': 'OK', 'message': 'Backend API Absensi Akademik Running', 'timestamp': datetime.now().isoformat() }) # ==================== REGISTRASI ==================== @app.route('/api/auth/register', methods=['POST']) def register(): """ Endpoint registrasi mahasiswa baru Request Body: { "npm": "2023010001", "password": "password123", "nama": "John Doe", "jenkel": "L", "fakultas": "Teknik", "jurusan": "Informatika", "semester": 5 } """ try: data = request.get_json() # Validasi input required_fields = ['npm', 'password', 'nama', 'jenkel', 'fakultas', 'jurusan', 'semester'] for field in required_fields: if field not in data or not data[field]: return jsonify({'error': f'Field {field} wajib diisi'}), 400 # Validasi jenis kelamin if data['jenkel'] not in ['L', 'P']: return jsonify({'error': 'Jenis kelamin harus L atau P'}), 400 # Validasi semester if not isinstance(data['semester'], int) or data['semester'] < 1 or data['semester'] > 14: return jsonify({'error': 'Semester harus antara 1-14'}), 400 # Hash password hashed_password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()) connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor() # Cek apakah NPM sudah terdaftar cursor.execute("SELECT npm FROM mahasiswa WHERE npm = %s", (data['npm'],)) if cursor.fetchone(): cursor.close() connection.close() return jsonify({'error': 'NPM sudah terdaftar'}), 409 # Insert mahasiswa baru insert_query = """ INSERT INTO mahasiswa (npm, password, nama, jenkel, fakultas, jurusan, semester) VALUES (%s, %s, %s, %s, %s, %s, %s) """ cursor.execute(insert_query, ( data['npm'], hashed_password.decode('utf-8'), data['nama'], data['jenkel'], data['fakultas'], data['jurusan'], data['semester'] )) connection.commit() id_mahasiswa = cursor.lastrowid cursor.close() connection.close() # Generate token token = generate_token(id_mahasiswa, data['npm']) return jsonify({ 'message': 'Registrasi berhasil', 'data': { 'id_mahasiswa': id_mahasiswa, 'npm': data['npm'], 'nama': data['nama'], 'token': token } }), 201 except Exception as e: return jsonify({'error': str(e)}), 500 # ==================== LOGIN ==================== @app.route('/api/auth/login', methods=['POST']) def login(): """ Endpoint login mahasiswa Request Body: { "npm": "2023010001", "password": "password123" } """ try: data = request.get_json() if not data.get('npm') or not data.get('password'): return jsonify({'error': 'NPM dan password wajib diisi'}), 400 connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) # Cari mahasiswa berdasarkan NPM cursor.execute(""" SELECT id_mahasiswa, npm, password, nama, jenkel, fakultas, jurusan, semester FROM mahasiswa WHERE npm = %s """, (data['npm'],)) mahasiswa = cursor.fetchone() cursor.close() connection.close() if not mahasiswa: return jsonify({'error': 'NPM tidak ditemukan'}), 404 # Verifikasi password if not bcrypt.checkpw(data['password'].encode('utf-8'), mahasiswa['password'].encode('utf-8')): return jsonify({'error': 'Password salah'}), 401 # Generate token token = generate_token(mahasiswa['id_mahasiswa'], mahasiswa['npm']) return jsonify({ 'message': 'Login berhasil', 'data': { 'id_mahasiswa': mahasiswa['id_mahasiswa'], 'npm': mahasiswa['npm'], 'nama': mahasiswa['nama'], 'jenkel': mahasiswa['jenkel'], 'fakultas': mahasiswa['fakultas'], 'jurusan': mahasiswa['jurusan'], 'semester': mahasiswa['semester'], 'token': token } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # ==================== PROFIL ==================== @app.route('/api/mahasiswa/profile', methods=['GET']) @token_required def get_profile(): """ Endpoint untuk mendapatkan profil mahasiswa Memerlukan Authorization header dengan JWT token """ try: id_mahasiswa = request.user_data['id_mahasiswa'] connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) cursor.execute(""" SELECT id_mahasiswa, npm, nama, jenkel, fakultas, jurusan, semester, created_at FROM mahasiswa WHERE id_mahasiswa = %s """, (id_mahasiswa,)) mahasiswa = cursor.fetchone() cursor.close() connection.close() if not mahasiswa: return jsonify({'error': 'Profil tidak ditemukan'}), 404 return jsonify({ 'message': 'Profil berhasil diambil', 'data': mahasiswa }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # ==================== ABSENSI ==================== @app.route('/api/absensi/submit', methods=['POST']) @token_required def submit_absensi(): """ Endpoint untuk submit absensi UPDATE KEAMANAN: Menggunakan Waktu Server untuk validasi dan penyimpanan """ try: data = request.get_json() id_mahasiswa = request.user_data['id_mahasiswa'] npm = request.user_data['npm'] # Validasi input (timestamp dari client kita abaikan untuk logic, tapi tetap dicek keberadaannya gapapa) required_fields = ['id_jadwal', 'latitude', 'longitude', 'foto_base64', 'status'] for field in required_fields: if field not in data: return jsonify({'error': f'Field {field} wajib diisi'}), 400 connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) # 1. Ambil Data Mahasiswa cursor.execute("SELECT nama FROM mahasiswa WHERE id_mahasiswa = %s", (id_mahasiswa,)) mahasiswa = cursor.fetchone() if not mahasiswa: cursor.close() connection.close() return jsonify({'error': 'Mahasiswa tidak ditemukan'}), 404 # 2. Ambil Jadwal cursor.execute(""" SELECT j.id_jadwal, j.jam_mulai, j.jam_selesai, m.nama_matkul FROM jadwal_kelas j JOIN mata_kuliah m ON j.id_matkul = m.id_matkul WHERE j.id_jadwal = %s """, (data['id_jadwal'],)) jadwal = cursor.fetchone() if not jadwal: cursor.close() connection.close() return jsonify({'error': 'Jadwal tidak ditemukan'}), 404 # ========================================================================= # 🛡️ SECURITY FIX: TIME MANIPULATION # Menggunakan Waktu Server saat ini, BUKAN waktu dari client Android # ========================================================================= waktu_server_sekarang = datetime.now() # Opsi: Jika server Anda UTC, konversi ke WIB (UTC+7) # waktu_server_sekarang = datetime.utcnow() + timedelta(hours=7) jam_sekarang = waktu_server_sekarang.time() tanggal_sekarang_str = waktu_server_sekarang.strftime('%Y-%m-%d %H:%M:%S') # Normalisasi jam mulai & selesai dari database jam_mulai = jadwal['jam_mulai'] jam_selesai = jadwal['jam_selesai'] # Helper convert timedelta ke time (jika perlu) if isinstance(jam_mulai, timedelta): total_seconds = int(jam_mulai.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 jam_mulai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time() elif isinstance(jam_mulai, str): jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time() if isinstance(jam_selesai, timedelta): total_seconds = int(jam_selesai.total_seconds()) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 jam_selesai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time() elif isinstance(jam_selesai, str): jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time() # 3. Validasi Waktu (Pakai Jam Server) if not (jam_mulai <= jam_sekarang <= jam_selesai): cursor.close() connection.close() return jsonify({ 'error': 'Absensi gagal! Diluar jam kelas (Server Time)', 'detail': { 'jam_mulai': str(jam_mulai), 'jam_selesai': str(jam_selesai), 'waktu_server': str(jam_sekarang) } }), 400 # 4. Cek Double Absen Hari Ini cursor.execute(""" SELECT COUNT(*) as count FROM absensi WHERE id_mahasiswa = %s AND id_jadwal = %s AND DATE(timestamp) = DATE(%s) """, (id_mahasiswa, data['id_jadwal'], tanggal_sekarang_str)) if cursor.fetchone()['count'] > 0: cursor.close() connection.close() return jsonify({'error': 'Anda sudah absen untuk kelas ini hari ini'}), 400 # 5. Insert ke Database (Pakai Waktu Server) insert_query = """ INSERT INTO absensi ( id_mahasiswa, npm, nama, id_jadwal, mata_kuliah, latitude, longitude, timestamp, photo, foto_base64, status ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ # Ambil foto (prioritaskan field foto_base64) foto = data.get('foto_base64') or data.get('photo') cursor.execute(insert_query, ( id_mahasiswa, npm, mahasiswa['nama'], data['id_jadwal'], jadwal['nama_matkul'], data['latitude'], data['longitude'], tanggal_sekarang_str, # <--- PENTING: Simpan waktu server foto, foto, data['status'] )) connection.commit() id_absensi = cursor.lastrowid cursor.close() connection.close() # 6. Kirim ke Webhook N8N (Opsional) try: webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254" webhook_payload = { "npm": npm, "nama": mahasiswa['nama'], "mata_kuliah": jadwal['nama_matkul'], "latitude": data['latitude'], "longitude": data['longitude'], "timestamp": tanggal_sekarang_str, # Kirim waktu server "status": data['status'] } # Gunakan try-except timeout agar tidak memblokir response requests.post(webhook_url, json=webhook_payload, timeout=3) except Exception as e: print(f"⚠️ Webhook error: {e}") return jsonify({ 'message': 'Absensi berhasil disimpan', 'data': { 'id_absensi': id_absensi, 'mata_kuliah': jadwal['nama_matkul'], 'timestamp': tanggal_sekarang_str, 'status': data['status'] } }), 201 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/absensi/history', methods=['GET']) @token_required def get_history(): """ Endpoint untuk mendapatkan riwayat absensi UPDATE: Join dengan jadwal_kelas untuk ambil jam_mulai & jam_selesai """ try: id_mahasiswa = request.user_data['id_mahasiswa'] start_date = request.args.get('start_date') end_date = request.args.get('end_date') connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) # QUERY UPDATE: Join ke tabel jadwal_kelas (alias j) query = """ SELECT a.id_absensi, a.npm, a.nama, a.mata_kuliah, a.latitude, a.longitude, a.timestamp, a.status, a.created_at, 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 """ params = [id_mahasiswa] if start_date and end_date: query += " AND DATE(a.timestamp) BETWEEN %s AND %s" params.extend([start_date, end_date]) elif start_date: query += " AND DATE(a.timestamp) >= %s" params.append(start_date) elif end_date: query += " AND DATE(a.timestamp) <= %s" params.append(end_date) query += " ORDER BY a.timestamp DESC" cursor.execute(query, params) history = cursor.fetchall() # Konversi objek timedelta/time ke string 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']) cursor.close() connection.close() return jsonify({ 'message': 'Riwayat berhasil diambil', 'count': len(history), 'data': history }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/absensi/photo/', methods=['GET']) @token_required def get_photo(id_absensi): """ Endpoint untuk mendapatkan foto absensi """ try: id_mahasiswa = request.user_data['id_mahasiswa'] connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) cursor.execute(""" SELECT foto_base64 FROM absensi WHERE id_absensi = %s AND id_mahasiswa = %s """, (id_absensi, id_mahasiswa)) result = cursor.fetchone() cursor.close() connection.close() if not result: return jsonify({'error': 'Foto tidak ditemukan'}), 404 return jsonify({ 'message': 'Foto berhasil diambil', 'data': { 'id_absensi': id_absensi, 'foto_base64': result['foto_base64'] } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # ==================== STATISTIK ==================== @app.route('/api/absensi/stats', methods=['GET']) @token_required def get_stats(): """ Endpoint untuk mendapatkan statistik absensi mahasiswa """ try: id_mahasiswa = request.user_data['id_mahasiswa'] connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) # Total absensi cursor.execute(""" SELECT COUNT(*) as total FROM absensi WHERE id_mahasiswa = %s """, (id_mahasiswa,)) total = cursor.fetchone()['total'] # Absensi bulan ini cursor.execute(""" SELECT COUNT(*) as bulan_ini FROM absensi WHERE id_mahasiswa = %s AND MONTH(timestamp) = MONTH(CURRENT_DATE()) AND YEAR(timestamp) = YEAR(CURRENT_DATE()) """, (id_mahasiswa,)) bulan_ini = cursor.fetchone()['bulan_ini'] # Absensi minggu ini cursor.execute(""" SELECT COUNT(*) as minggu_ini FROM absensi WHERE id_mahasiswa = %s AND YEARWEEK(timestamp, 1) = YEARWEEK(CURRENT_DATE(), 1) """, (id_mahasiswa,)) minggu_ini = cursor.fetchone()['minggu_ini'] cursor.close() connection.close() return jsonify({ 'message': 'Statistik berhasil diambil', 'data': { 'total_absensi': total, 'absensi_bulan_ini': bulan_ini, 'absensi_minggu_ini': minggu_ini } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # ==================== JADWAL KELAS ==================== @app.route('/api/jadwal/today', methods=['GET']) @token_required def get_jadwal_today(): """ Endpoint untuk mendapatkan jadwal kelas hari ini berdasarkan semester dan jurusan mahasiswa """ try: id_mahasiswa = request.user_data['id_mahasiswa'] connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) # Ambil data mahasiswa cursor.execute(""" SELECT semester, jurusan FROM mahasiswa WHERE id_mahasiswa = %s """, (id_mahasiswa,)) mahasiswa = cursor.fetchone() if not mahasiswa: cursor.close() connection.close() return jsonify({'error': 'Mahasiswa tidak ditemukan'}), 404 # Ambil hari ini dalam bahasa Indonesia import locale from datetime import datetime hari_mapping = { 'Monday': 'Senin', 'Tuesday': 'Selasa', 'Wednesday': 'Rabu', 'Thursday': 'Kamis', 'Friday': 'Jumat', 'Saturday': 'Sabtu', 'Sunday': 'Minggu' } hari_ini = hari_mapping.get(datetime.now().strftime('%A'), 'Senin') # Query jadwal hari ini cursor.execute(""" SELECT j.id_jadwal, j.hari, j.jam_mulai, j.jam_selesai, j.ruangan, m.kode_matkul, m.nama_matkul, m.sks, m.dosen FROM jadwal_kelas j JOIN mata_kuliah m ON j.id_matkul = m.id_matkul WHERE j.hari = %s AND j.semester = %s AND j.jurusan = %s ORDER BY j.jam_mulai """, (hari_ini, mahasiswa['semester'], mahasiswa['jurusan'])) jadwal = cursor.fetchall() # Cek apakah mahasiswa sudah absen untuk jadwal tertentu for item in jadwal: cursor.execute(""" SELECT COUNT(*) as sudah_absen FROM absensi WHERE id_mahasiswa = %s AND id_jadwal = %s AND DATE(timestamp) = CURDATE() """, (id_mahasiswa, item['id_jadwal'])) result = cursor.fetchone() item['sudah_absen'] = result['sudah_absen'] > 0 # Format waktu item['jam_mulai'] = str(item['jam_mulai']) item['jam_selesai'] = str(item['jam_selesai']) cursor.close() connection.close() return jsonify({ 'message': 'Jadwal berhasil diambil', 'hari': hari_ini, 'count': len(jadwal), 'data': jadwal }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/jadwal/check/', methods=['GET']) @token_required def check_jadwal_aktif(id_jadwal): """ Endpoint untuk cek apakah jadwal sedang aktif (dalam rentang waktu) """ try: connection = get_db_connection() if connection is None: return jsonify({'error': 'Gagal koneksi ke database'}), 500 cursor = connection.cursor(dictionary=True) # Ambil jadwal cursor.execute(""" SELECT j.id_jadwal, j.jam_mulai, j.jam_selesai, m.nama_matkul FROM jadwal_kelas j JOIN mata_kuliah m ON j.id_matkul = m.id_matkul WHERE j.id_jadwal = %s """, (id_jadwal,)) jadwal = cursor.fetchone() cursor.close() connection.close() if not jadwal: return jsonify({'error': 'Jadwal tidak ditemukan'}), 404 # Cek waktu sekarang from datetime import datetime, time waktu_sekarang = datetime.now().time() jam_mulai = jadwal['jam_mulai'] jam_selesai = jadwal['jam_selesai'] # Convert to time if needed if isinstance(jam_mulai, str): jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time() if isinstance(jam_selesai, str): jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time() is_aktif = jam_mulai <= waktu_sekarang <= jam_selesai return jsonify({ 'message': 'Pengecekan jadwal berhasil', 'data': { 'id_jadwal': jadwal['id_jadwal'], 'mata_kuliah': jadwal['nama_matkul'], 'jam_mulai': str(jam_mulai), 'jam_selesai': str(jam_selesai), 'waktu_sekarang': str(waktu_sekarang), 'is_aktif': is_aktif } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 # ==================== RUN SERVER ==================== if __name__ == '__main__': print("🚀 Menginisialisasi database...") init_database() print("🌐 Starting Flask server...") print("📍 Backend API: http://localhost:5000") print("📍 Health Check: http://localhost:5000/api/health") app.run(debug=True, host='0.0.0.0', port=5000)