Penyesuaian Fungsi Submit Absensi

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2026-01-14 21:32:31 +07:00
parent 080b265607
commit e35fdd36d1
2 changed files with 122 additions and 151 deletions

View File

@ -81,20 +81,23 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
/* ================= CONSTANTS ================= */
object AppConstants {
// Backend API URL - GANTI SESUAI SERVER ANDA
// const val BASE_URL = "http://10.0.2.2:5000" // Untuk emulator Android
const val BASE_URL = "http://192.168.1.70:5000" // Untuk device fisik
const val BASE_URL = "http://192.168.100.99:5000" // Untuk device fisik
// Koordinat Kampus (UBHARA Jaya)
const val KAMPUS_LATITUDE = -6.223325
const val KAMPUS_LONGITUDE = 107.009406
// const val KAMPUS_LATITUDE = -6.239513
// const val KAMPUS_LONGITUDE = 107.089676
const val RADIUS_METER = 2000.0
// const val KAMPUS_LATITUDE = -6.223325
// const val KAMPUS_LONGITUDE = 107.009406
// Koordinat Saat ini
const val KAMPUS_LATITUDE = -6.239513
const val KAMPUS_LONGITUDE = 107.089676
const val RADIUS_METER = 500.0
// Offset untuk privasi
const val LATITUDE_OFFSET = 0.0001
@ -205,9 +208,29 @@ class UserPreferences(private val context: Context) {
/* ================= UTIL FUNCTIONS ================= */
fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
// 1. Tentukan ukuran baru (Misal Max Lebar 600px)
val maxDimension = 600
var newWidth = maxDimension
var newHeight = (bitmap.height.toFloat() / bitmap.width.toFloat() * newWidth).toInt()
// Jika gambar aslinya sudah kecil, jangan dibesarkan
if (bitmap.width <= maxDimension) {
newWidth = bitmap.width
newHeight = bitmap.height
}
// 2. Lakukan Resize
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
// 3. Kompres ke ByteArray
val outputStream = java.io.ByteArrayOutputStream()
// Kualitas 50 sudah cukup jika resolusinya kecil
resizedBitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 50, outputStream)
val byteArray = outputStream.toByteArray()
// 4. Return Base64
return android.util.Base64.encodeToString(byteArray, android.util.Base64.NO_WRAP)
}
fun base64ToBitmap(base64: String): Bitmap? {
@ -1643,33 +1666,10 @@ fun RiwayatScreen(
}
}
// Tombol Filter Bulat
IconButton(
onClick = { showFilterDialog = true },
modifier = Modifier
.background(if (filterActive) GoldPrimary else androidx.compose.ui.graphics.Color.White, CircleShape)
.border(1.dp, androidx.compose.ui.graphics.Color.LightGray, CircleShape)
) {
Icon(
imageVector = Icons.Default.FilterList,
contentDescription = "Filter",
tint = if (filterActive) androidx.compose.ui.graphics.Color.White else androidx.compose.ui.graphics.Color.Gray
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Stats Row (Dashboard Style)
if (stats != null) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
StatsCard("Total", stats!!.totalAbsensi.toString(), Icons.Default.CheckCircle, GoldPrimary, Modifier.weight(1f))
StatsCard("Minggu", stats!!.absensiMingguIni.toString(), Icons.Default.DateRange, androidx.compose.ui.graphics.Color(0xFF1976D2), Modifier.weight(1f)) // Biru
StatsCard("Bulan", stats!!.absensiBulanIni.toString(), Icons.Default.CalendarToday, MaroonSecondary, Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(24.dp))
}
// List Content
if (isLoading) {
Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator(color = GoldPrimary) }
@ -1701,30 +1701,6 @@ fun RiwayatScreen(
}
}
// Komponen Card Statistik Baru
@Composable
fun StatsCard(
title: String, value: String, icon: androidx.compose.ui.graphics.vector.ImageVector,
color: androidx.compose.ui.graphics.Color, modifier: Modifier
) {
Card(
modifier = modifier,
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(imageVector = icon, contentDescription = null, tint = color, modifier = Modifier.size(24.dp))
Spacer(modifier = Modifier.height(8.dp))
Text(text = value, style = MaterialTheme.typography.titleLarge.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), color = androidx.compose.ui.graphics.Color.Black)
Text(text = title, style = MaterialTheme.typography.labelSmall, color = androidx.compose.ui.graphics.Color.Gray)
}
}
}
// Komponen Card Riwayat Baru
@Composable
fun RiwayatCard(
@ -1740,133 +1716,110 @@ fun RiwayatCard(
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// Header Card: Tanggal & Jam
// HEADER: Teks Matkul (Kiri) & Badge Status (Kanan)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
// Alignment Top agar jika teks 2 baris, badge tetap di pojok kanan atas
verticalAlignment = Alignment.Top
) {
Column {
// 1. KOLOM TEKS (Gunakan weight 1f agar tidak menabrak badge)
Column(
modifier = Modifier
.weight(1f) // KUNCI UTAMA: Ambil sisa ruang
.padding(end = 12.dp) // Beri jarak dengan badge
) {
Text(
text = formatTanggalCard(riwayat.timestamp),
style = MaterialTheme.typography.labelMedium,
color = androidx.compose.ui.graphics.Color.Gray
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = riwayat.mataKuliah ?: "-",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold),
color = androidx.compose.ui.graphics.Color.Black
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
// Opsional: Atur tinggi baris agar lebih lega
lineHeight = 20.sp
),
color = androidx.compose.ui.graphics.Color.Black,
// Batasi maksimal 2 baris agar kartu tidak terlalu tinggi
maxLines = 2,
// Jika lebih dari 2 baris, potong dengan "..."
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
)
}
// Badge Hadir
// 2. BADGE STATUS (Ukuran statis sesuai konten)
Surface(
shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp),
color = if (riwayat.status == "HADIR") androidx.compose.ui.graphics.Color(0xFFE8F5E9) else androidx.compose.ui.graphics.Color(0xFFFFEBEE)
) {
Text(
text = riwayat.status,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelSmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold),
color = if (riwayat.status == "HADIR") androidx.compose.ui.graphics.Color(0xFF2E7D32) else androidx.compose.ui.graphics.Color(0xFFC62828)
)
}
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(16.dp))
Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.2f))
Spacer(modifier = Modifier.height(12.dp))
// Detail: Waktu & Lokasi
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
// FOOTER: Jam & Tombol Lihat Foto
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// Info Jam
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.AccessTime, null, modifier = Modifier.size(14.dp), tint = GoldPrimary)
Spacer(modifier = Modifier.width(4.dp))
Icon(
Icons.Default.AccessTime,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = GoldPrimary
)
Spacer(modifier = Modifier.width(6.dp))
// Logic jam (Copy dari kode sebelumnya)
val waktuText = if (riwayat.jamMulai != null && riwayat.jamSelesai != null) {
"${riwayat.jamMulai.take(5)} - ${riwayat.jamSelesai.take(5)}"
} else {
formatJam(riwayat.timestamp)
}
Text(waktuText, style = MaterialTheme.typography.bodySmall, color = androidx.compose.ui.graphics.Color.Gray)
Text(
text = waktuText,
style = MaterialTheme.typography.bodyMedium,
color = androidx.compose.ui.graphics.Color.Gray
)
}
// Tombol Lihat Foto Kecil
TextButton(
onClick = { onLihatFoto(riwayat.idAbsensi) },
contentPadding = PaddingValues(0.dp),
modifier = Modifier.height(24.dp)
// Tombol Lihat Foto
Row(
modifier = Modifier.clickable { onLihatFoto(riwayat.idAbsensi) },
verticalAlignment = Alignment.CenterVertically
) {
Text("Lihat Foto", style = MaterialTheme.typography.labelSmall, color = GoldPrimary)
Text(
text = "Lihat Foto",
style = MaterialTheme.typography.labelSmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold),
color = GoldPrimary
)
Spacer(modifier = Modifier.width(4.dp))
Icon(Icons.Default.ArrowForwardIos, null, modifier = Modifier.size(10.dp), tint = GoldPrimary)
Icon(
Icons.Default.ArrowForwardIos,
contentDescription = null,
modifier = Modifier.size(10.dp),
tint = GoldPrimary
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterDateDialog(
startDate: String?,
endDate: String?,
onDismiss: () -> Unit,
onApply: (String?, String?) -> Unit,
onReset: () -> Unit
) {
var tempStartDate by remember { mutableStateOf(startDate) }
var tempEndDate by remember { mutableStateOf(endDate) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Filter Tanggal") },
text = {
Column {
OutlinedTextField(
value = tempStartDate ?: "",
onValueChange = { tempStartDate = it },
label = { Text("Tanggal Mulai") },
placeholder = { Text("YYYY-MM-DD") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = tempEndDate ?: "",
onValueChange = { tempEndDate = it },
label = { Text("Tanggal Akhir") },
placeholder = { Text("YYYY-MM-DD") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Format: YYYY-MM-DD (contoh: 2026-01-13)",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
confirmButton = {
TextButton(onClick = { onApply(tempStartDate, tempEndDate) }) {
Text("Terapkan")
}
},
dismissButton = {
Row {
TextButton(onClick = onReset) {
Text("Reset")
}
TextButton(onClick = onDismiss) {
Text("Batal")
}
}
}
)
}
// ========== HELPER FUNCTIONS ==========

View File

@ -207,6 +207,10 @@ def submit_absensi():
try:
data = request.get_json()
status = data.get('status', 'HADIR')
# Ambil data mentah dari Android
foto_input = data.get('foto_base64') or data.get('photo')
conn = get_db_connection()
cur = conn.cursor(dictionary=True)
@ -223,28 +227,42 @@ def submit_absensi():
cur.execute("SELECT nama_matkul FROM mata_kuliah m JOIN jadwal_kelas j ON m.id_matkul=j.id_matkul WHERE j.id_jadwal=%s", (data['id_jadwal'],))
nama_matkul = cur.fetchone()['nama_matkul']
# 3. Waktu Server
# 3. Insert ke Database
waktu_skrg = datetime.now()
timestamp_str = waktu_skrg.strftime('%Y-%m-%d %H:%M:%S')
# 4. Insert ke Database
cur.execute("""
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)
""", (request.user_data['id_mahasiswa'], request.user_data['npm'], nama_mhs, data['id_jadwal'], nama_matkul,
data['latitude'], data['longitude'], timestamp_str, data.get('foto_base64'), data.get('foto_base64'), status))
conn.commit()
data['latitude'], data['longitude'], timestamp_str, foto_input, foto_input, status))
# Ambil ID yang baru dibuat
# Simpan perubahan & Ambil ID Baru
conn.commit()
new_id = cur.lastrowid
# ==========================================================
# 4. AMBIL ULANG DARI DATABASE (SOLUSI PASTI)
# ==========================================================
# Sesuai permintaan Anda: Kita ambil data yang BARU SAJA masuk
# untuk memastikan variabelnya tidak kosong.
cur.execute("SELECT foto_base64 FROM absensi WHERE id_absensi=%s", (new_id,))
row = cur.fetchone()
# Pastikan kita punya datanya
foto_final = row['foto_base64'] if row else None
cur.close(); conn.close()
# ==========================================================
# 🔗 5. KIRIM KE WEBHOOK N8N (DIKEMBALIKAN)
# 5. KIRIM KE WEBHOOK N8N
# ==========================================================
try:
webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
# Payload dengan Foto ASLI dari Database
webhook_payload = {
"id_absensi": new_id,
"npm": request.user_data['npm'],
"nama": nama_mhs,
"mata_kuliah": nama_matkul,
@ -252,17 +270,17 @@ def submit_absensi():
"longitude": data['longitude'],
"timestamp": timestamp_str,
"status": status,
"keterangan": "Absensi via Android"
"foto_base64": foto_final, # Kirim String Base64 Panjang
}
# Timeout 3 detik agar aplikasi tidak loading lama jika N8N lambat
requests.post(webhook_url, json=webhook_payload, timeout=3)
print("✅ Data terkirim ke N8N")
# Kirim (Timeout agak lama karena Base64 besar)
requests.post(webhook_url, json=webhook_payload, timeout=10)
print(f"✅ Data ID {new_id} terkirim ke N8N (Size foto: {len(str(foto_final))} chars)")
except Exception as e:
print(f"⚠️ Gagal kirim ke N8N: {e}")
# ==========================================================
# ✅ 6. RESPON JSON (FORMAT SESUAI ANDROID)
# ==========================================================
# 6. Respon ke Android
return jsonify({
'message': 'Absensi berhasil disimpan',
'data': {