Register, Fitur Kelas, Absensi sesuai waktu hari ini dan waktu kelas
This commit is contained in:
parent
9a1e127859
commit
89355bfbb7
@ -53,6 +53,8 @@ dependencies {
|
||||
implementation(libs.androidx.compose.material3)
|
||||
// Location (GPS)
|
||||
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)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
||||
@ -2,13 +2,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<!-- ==================== PERMISSIONS ==================== -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<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
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.front"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@ -18,15 +24,16 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SistemAkademik">
|
||||
android:theme="@style/Theme.SistemAkademik"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.SistemAkademik">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
921
backend/app.py
Normal file
921
backend/app.py
Normal 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
8
backend/requirements.txt
Normal 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
|
||||
@ -8,6 +8,7 @@ espressoCore = "3.7.0"
|
||||
lifecycleRuntimeKtx = "2.9.4"
|
||||
activityCompose = "1.11.0"
|
||||
composeBom = "2024.09.00"
|
||||
animationCoreLint = "1.10.0"
|
||||
|
||||
[libraries]
|
||||
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-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user