Register, Fitur Kelas, Absensi sesuai waktu hari ini dan waktu kelas

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2026-01-13 22:44:33 +07:00
parent 9a1e127859
commit 89355bfbb7
6 changed files with 2548 additions and 181 deletions

View File

@ -53,6 +53,8 @@ dependencies {
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
// Location (GPS) // Location (GPS)
implementation("com.google.android.gms:play-services-location:21.0.1") implementation("com.google.android.gms:play-services-location:21.0.1")
implementation("androidx.compose.material:material-icons-extended:1.6.0")
implementation(libs.androidx.compose.animation.core.lint)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -2,13 +2,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/> <!-- ==================== PERMISSIONS ==================== -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
<uses-feature
android:name="android.hardware.camera.front"
android:required="false" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@ -18,15 +24,16 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SistemAkademik"> android:theme="@style/Theme.SistemAkademik"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SistemAkademik"> android:theme="@style/Theme.SistemAkademik">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>

921
backend/app.py Normal file
View File

@ -0,0 +1,921 @@
"""
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)

8
backend/requirements.txt Normal file
View File

@ -0,0 +1,8 @@
# ==================== requirements.txt ====================
flask==3.0.0
flask-cors==4.0.0
mysql-connector-python==8.2.0
PyJWT==2.8.0
bcrypt==4.1.2
python-dotenv==1.0.0
requests==2.31.0

View File

@ -8,6 +8,7 @@ espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4" lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0" activityCompose = "1.11.0"
composeBom = "2024.09.00" composeBom = "2024.09.00"
animationCoreLint = "1.10.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -24,6 +25,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-animation-core-lint = { group = "androidx.compose.animation", name = "animation-core-lint", version.ref = "animationCoreLint" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }