From e35fdd36d1d90707d1032e72035fe42f5d80c35c Mon Sep 17 00:00:00 2001 From: Raihan Ariq <202310715297@mhs.ubharajaya.ac.id> Date: Wed, 14 Jan 2026 21:32:31 +0700 Subject: [PATCH] Penyesuaian Fungsi Submit Absensi --- .../ubharajaya/sistemakademik/MainActivity.kt | 229 +++++++----------- backend/app.py | 44 +++- 2 files changed, 122 insertions(+), 151 deletions(-) diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt index 758de57..17113b7 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -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 ========== diff --git a/backend/app.py b/backend/app.py index c8d5448..c58f1bb 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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': {