Penyesuaian Fungsi Submit Absensi
This commit is contained in:
parent
080b265607
commit
e35fdd36d1
@ -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,134 +1716,111 @@ 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
|
||||
) {
|
||||
// 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
|
||||
) {
|
||||
Column {
|
||||
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)
|
||||
}
|
||||
|
||||
// Tombol Lihat Foto Kecil
|
||||
TextButton(
|
||||
onClick = { onLihatFoto(riwayat.idAbsensi) },
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("Lihat Foto", style = MaterialTheme.typography.labelSmall, color = GoldPrimary)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(Icons.Default.ArrowForwardIos, 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
|
||||
text = waktuText,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = androidx.compose.ui.graphics.Color.Gray
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onApply(tempStartDate, tempEndDate) }) {
|
||||
Text("Terapkan")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Row {
|
||||
TextButton(onClick = onReset) {
|
||||
Text("Reset")
|
||||
}
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Batal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tombol Lihat Foto
|
||||
Row(
|
||||
modifier = Modifier.clickable { onLihatFoto(riwayat.idAbsensi) },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
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,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(10.dp),
|
||||
tint = GoldPrimary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ========== HELPER FUNCTIONS ==========
|
||||
|
||||
// Helper untuk memparsing tanggal dari String MySQL (YYYY-MM-DD HH:mm:ss)
|
||||
|
||||
@ -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': {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user