921 lines
29 KiB
Python
921 lines
29 KiB
Python
"""
|
|
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 <token>"
|
|
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: dengan validasi jadwal)
|
|
|
|
Request Body:
|
|
{
|
|
"id_jadwal": 1,
|
|
"latitude": -6.223276,
|
|
"longitude": 107.009273,
|
|
"timestamp": "2026-01-13 14:30:00",
|
|
"foto_base64": "base64_string",
|
|
"status": "HADIR"
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json()
|
|
id_mahasiswa = request.user_data['id_mahasiswa']
|
|
npm = request.user_data['npm']
|
|
|
|
# Validasi input
|
|
required_fields = ['id_jadwal', 'latitude', 'longitude', 'timestamp', '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)
|
|
|
|
# Ambil nama 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
|
|
|
|
# Ambil info jadwal & mata kuliah
|
|
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
|
|
|
|
# Validasi waktu absensi
|
|
from datetime import datetime, timedelta
|
|
|
|
timestamp_absensi = datetime.strptime(data['timestamp'], '%Y-%m-%d %H:%M:%S')
|
|
waktu_absensi = timestamp_absensi.time()
|
|
|
|
jam_mulai = jadwal['jam_mulai']
|
|
jam_selesai = jadwal['jam_selesai']
|
|
|
|
# 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
|
|
seconds = total_seconds % 60
|
|
jam_mulai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%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
|
|
seconds = total_seconds % 60
|
|
jam_selesai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
|
|
elif isinstance(jam_selesai, str):
|
|
jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time()
|
|
|
|
if not (jam_mulai <= waktu_absensi <= jam_selesai):
|
|
cursor.close()
|
|
connection.close()
|
|
return jsonify({
|
|
'error': 'Absensi di luar jam kelas',
|
|
'detail': {
|
|
'jam_mulai': str(jam_mulai),
|
|
'jam_selesai': str(jam_selesai),
|
|
'waktu_absensi': str(waktu_absensi)
|
|
}
|
|
}), 400
|
|
|
|
# Cek apakah sudah absen hari ini untuk jadwal 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'], data['timestamp']))
|
|
|
|
if cursor.fetchone()['count'] > 0:
|
|
cursor.close()
|
|
connection.close()
|
|
return jsonify({'error': 'Anda sudah absen untuk kelas ini hari ini'}), 400
|
|
|
|
# Insert absensi ke MySQL
|
|
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)
|
|
"""
|
|
cursor.execute(insert_query, (
|
|
id_mahasiswa,
|
|
npm,
|
|
mahasiswa['nama'],
|
|
data['id_jadwal'],
|
|
jadwal['nama_matkul'],
|
|
data['latitude'],
|
|
data['longitude'],
|
|
data['timestamp'],
|
|
data.get('photo', data['foto_base64']),
|
|
data['foto_base64'],
|
|
data['status']
|
|
))
|
|
|
|
connection.commit()
|
|
id_absensi = cursor.lastrowid
|
|
|
|
cursor.close()
|
|
connection.close()
|
|
|
|
# KIRIM KE WEBHOOK N8N
|
|
try:
|
|
import requests
|
|
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": data['timestamp'],
|
|
"photo": data['foto_base64'],
|
|
"foto_base64": data['foto_base64'],
|
|
"status": data['status']
|
|
}
|
|
webhook_response = requests.post(webhook_url, json=webhook_payload, timeout=10)
|
|
print(f"✅ Webhook n8n: {webhook_response.status_code}")
|
|
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'],
|
|
'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
|
|
|
|
Query Parameters:
|
|
- start_date (optional): YYYY-MM-DD
|
|
- end_date (optional): YYYY-MM-DD
|
|
"""
|
|
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 dasar
|
|
query = """
|
|
SELECT id_absensi, npm, nama, latitude, longitude, timestamp, status, created_at
|
|
FROM absensi
|
|
WHERE id_mahasiswa = %s
|
|
"""
|
|
params = [id_mahasiswa]
|
|
|
|
# Filter berdasarkan tanggal
|
|
if start_date and end_date:
|
|
query += " AND DATE(timestamp) BETWEEN %s AND %s"
|
|
params.extend([start_date, end_date])
|
|
elif start_date:
|
|
query += " AND DATE(timestamp) >= %s"
|
|
params.append(start_date)
|
|
elif end_date:
|
|
query += " AND DATE(timestamp) <= %s"
|
|
params.append(end_date)
|
|
|
|
query += " ORDER BY timestamp DESC"
|
|
|
|
cursor.execute(query, params)
|
|
history = cursor.fetchall()
|
|
|
|
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/<int:id_absensi>', 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/<int:id_jadwal>', 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) |