EAS_202310715200_IndrisAlpasela

This commit is contained in:
202310715200 INDRIS ALPASELA 2026-01-14 21:20:01 +07:00
parent 7cd8b241d0
commit ca6d6e2d33
5 changed files with 451 additions and 169 deletions

View File

@ -5,9 +5,6 @@ import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayOutputStream
/**
* Data class untuk menyimpan informasi absensi mahasiswa
*/
data class Absensi(
val npm: String,
val nama: String,
@ -16,30 +13,21 @@ data class Absensi(
val waktu: String,
val foto: Bitmap
) {
/**
* Convert objek Absensi ini menjadi JSONObject
* Siap untuk dikirim ke server
*/
fun toJson(): JSONObject {
val json = JSONObject()
json.put("npm", npm)
json.put("nama", nama)
json.put("latitude", latitude)
json.put("longitude", longitude)
json.put("timestamp", System.currentTimeMillis())
json.put("waktu", waktu)
json.put("foto_base64", bitmapToBase64(foto))
// Convert Bitmap to Base64
val byteArrayOutputStream = ByteArrayOutputStream()
foto.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)
val byteArray = byteArrayOutputStream.toByteArray()
val encodedImage = Base64.encodeToString(byteArray, Base64.DEFAULT)
json.put("foto", encodedImage)
return json
}
companion object {
/**
* Helper function untuk convert Bitmap ke Base64
*/
fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
}
}
}
}

View File

@ -92,7 +92,7 @@ fun LoginScreen(modifier: Modifier = Modifier) {
// Tombol Login
Button(
onClick = {
if (npm == "202310715123" && password == "123") {
if (npm == "202310715200" && password == "123") {
val intent = Intent(context, MainActivity::class.java)
context.startActivity(intent)
} else {

View File

@ -14,21 +14,22 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.google.android.gms.location.LocationServices
@ -38,7 +39,11 @@ import java.util.*
import kotlin.concurrent.thread
import java.net.HttpURLConnection
import java.net.URL
import org.json.JSONObject
// Warna Akademik Biru Konsisten
val AkademikBlue = Color(0xFF1565C0)
val AkademikLightBlue = Color(0xFFE3F2FD)
val AkademikDarkBlue = Color(0xFF0D47A1)
// ================== UTILS ==================
fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
@ -55,14 +60,14 @@ fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
context.runOnUiThread {
Toast.makeText(
context,
if (responseCode == 200) "Absensi diterima server" else "Absensi ditolak server",
if (responseCode == 200) "Absensi berhasil dikirim" else "Absensi gagal dikirim",
Toast.LENGTH_SHORT
).show()
}
conn.disconnect()
} catch (_: Exception) {
context.runOnUiThread {
Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Gagal terhubung ke server", Toast.LENGTH_SHORT).show()
}
}
}
@ -77,8 +82,11 @@ class MainActivity : ComponentActivity() {
setContent {
SistemAkademikTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this)
Surface(
modifier = Modifier.fillMaxSize(),
color = AkademikLightBlue
) {
AbsensiScreen(activity = this)
}
}
}
@ -86,30 +94,23 @@ class MainActivity : ComponentActivity() {
}
// ================== COMPOSABLE UI ==================
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
fun AbsensiScreen(activity: ComponentActivity) {
val context = LocalContext.current
val session = remember { SessionManager(context) }
var lokasi by remember { mutableStateOf("Koordinat: -") }
var lokasi by remember { mutableStateOf("Menunggu lokasi...") }
var latitude by remember { mutableStateOf<Double?>(null) }
var longitude by remember { mutableStateOf<Double?>(null) }
var foto by remember { mutableStateOf<Bitmap?>(null) }
var waktuAbsensi by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
var showLogoutDialog by remember { mutableStateOf(false) }
val absensiList = remember { mutableStateListOf<Absensi>() }
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
// ================== STATE WARNA ==================
var primaryColor by remember { mutableStateOf(Color(0xFF6200EE)) }
var backgroundColor by remember { mutableStateOf(Color(0xFFF2F2F2)) }
val colors = listOf(
Color.Red, Color.Green, Color.Blue, Color.Magenta, Color.Cyan,
Color.Yellow, Color.Gray, Color.DarkGray, Color.Black, Color(0xFFFF9800),
Color(0xFF9C27B0), Color(0xFF4CAF50), Color(0xFF03A9F4), Color(0xFFE91E63)
)
// ===== Permission & Kamera =====
val locationPermissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
@ -122,7 +123,7 @@ fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
if (location != null) {
latitude = location.latitude
longitude = location.longitude
lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}"
lokasi = "${String.format("%.6f", location.latitude)}, ${String.format("%.6f", location.longitude)}"
} else {
lokasi = "Lokasi tidak tersedia"
}
@ -130,7 +131,7 @@ fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
.addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
}
} else {
Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Izin lokasi diperlukan", Toast.LENGTH_SHORT).show()
}
}
@ -156,7 +157,7 @@ fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraLauncher.launch(intent)
} else {
Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Izin kamera diperlukan", Toast.LENGTH_SHORT).show()
}
}
@ -164,121 +165,406 @@ fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
// ================== UI ==================
Column(
modifier = modifier
.fillMaxSize()
.background(backgroundColor)
.padding(24.dp)
) {
Text(
text = "Absensi Akademik",
style = MaterialTheme.typography.titleLarge,
color = primaryColor
)
Spacer(modifier = Modifier.height(16.dp))
// PALETTE WARNA BULETAN
Text("Pilih Warna Tema:", style = MaterialTheme.typography.bodyMedium, color = primaryColor)
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
colors.forEach { color ->
Box(
modifier = Modifier
.size(40.dp)
.padding(4.dp)
.background(color, shape = CircleShape)
.clickable {
primaryColor = color
backgroundColor = color.copy(alpha = 0.1f)
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = lokasi, color = primaryColor)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
) { Text("Ambil Foto") }
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
if (latitude != null && longitude != null && foto != null) {
val waktu = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()).format(System.currentTimeMillis())
waktuAbsensi = waktu
val absensi = Absensi(
npm = session.getUserNpm() ?: "Faris Naufal Priatna",
nama = session.getUserName() ?: "202310715123",
latitude = latitude!!,
longitude = longitude!!,
waktu = waktu,
foto = foto!!
)
kirimKeN8n(activity, absensi)
absensiList.add(absensi)
} else {
Toast.makeText(context, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show()
// Logout Dialog
if (showLogoutDialog) {
AlertDialog(
onDismissRequest = { showLogoutDialog = false },
title = { Text("Konfirmasi Keluar") },
text = { Text("Apakah Anda yakin ingin keluar dari sistem?") },
confirmButton = {
TextButton(
onClick = {
session.logout()
context.startActivity(Intent(context, LoginActivity::class.java))
(context as ComponentActivity).finish()
}
) {
Text("Ya", color = Color.Red, fontWeight = FontWeight.Bold)
}
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
) { Text("Kirim Absensi") }
dismissButton = {
TextButton(onClick = { showLogoutDialog = false }) {
Text("Batal", color = AkademikBlue)
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
waktuAbsensi?.let { Text("Waktu Absensi: $it", color = primaryColor) }
Spacer(modifier = Modifier.height(16.dp))
// ================== LOGOUT BUTTON ==================
Button(
onClick = {
session.logout()
context.startActivity(Intent(context, LoginActivity::class.java))
(context as ComponentActivity).finish()
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
Text("Logout")
// ================== UI ==================
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text(
"Sistem Absensi Mahasiswa",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
"Universitas Bhayangkara Jakarta Raya",
style = MaterialTheme.typography.bodySmall
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = AkademikBlue,
titleContentColor = Color.White
),
actions = {
TextButton(onClick = { showLogoutDialog = true }) {
Text("Keluar", color = Color.White, fontWeight = FontWeight.Bold)
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
Text("Riwayat Kehadiran", color = primaryColor)
Spacer(modifier = Modifier.height(12.dp))
if (absensiList.isEmpty()) Text("Belum ada absensi", color = primaryColor)
else LazyColumn {
items(absensiList) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
// CARD PROFIL MAHASISWA
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
) {
Row(
modifier = Modifier.padding(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.padding(12.dp)) {
Text("Nama: ${item.nama}", color = primaryColor)
Text("NPM: ${item.npm}", color = primaryColor)
Text("Waktu: ${item.waktu}", color = primaryColor)
Text("Lat: ${item.latitude}, Lon: ${item.longitude}", color = primaryColor)
Spacer(modifier = Modifier.height(8.dp))
Image(
bitmap = item.foto.asImageBitmap(),
contentDescription = "Foto Absensi",
Surface(
modifier = Modifier.size(70.dp),
shape = RoundedCornerShape(12.dp),
color = AkademikLightBlue
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
"IA",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = AkademikBlue
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = session.getUserName() ?: "Indris Alpasela",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = AkademikDarkBlue
)
Text(
text = "NPM: ${session.getUserNpm() ?: "202310715200"}",
style = MaterialTheme.typography.bodyLarge,
color = Color.Gray
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// INFO CARD: LOKASI & WAKTU
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// Lokasi
Row(verticalAlignment = Alignment.Top) {
Text(
"📍",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
"Lokasi Saat Ini",
style = MaterialTheme.typography.labelMedium,
color = Color.Gray
)
Text(
lokasi,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
}
}
waktuAbsensi?.let {
Spacer(modifier = Modifier.height(16.dp))
Divider()
Spacer(modifier = Modifier.height(16.dp))
// Waktu Absensi
Row(verticalAlignment = Alignment.Top) {
Text(
"🕐",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
"Waktu Absensi Terakhir",
style = MaterialTheme.typography.labelMedium,
color = Color.Gray
)
Text(
it,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// PREVIEW FOTO
AnimatedVisibility(visible = foto != null) {
Column {
Card(
modifier = Modifier
.fillMaxWidth()
.height(240.dp),
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
) {
Box(modifier = Modifier.fillMaxSize()) {
foto?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "Preview Foto",
modifier = Modifier.fillMaxSize()
)
}
Surface(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp),
shape = RoundedCornerShape(8.dp),
color = Color.White.copy(alpha = 0.9f)
) {
Text(
"Preview",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = AkademikBlue
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
// TOMBOL AMBIL FOTO
Button(
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
colors = ButtonDefaults.buttonColors(containerColor = AkademikBlue),
shape = RoundedCornerShape(12.dp),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp)
) {
Text(
"📷",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.width(12.dp))
Text(
"Ambil Foto",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(12.dp))
// TOMBOL KIRIM ABSENSI
Button(
onClick = {
if (latitude != null && longitude != null && foto != null) {
isLoading = true
val waktu = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale("id", "ID")).format(System.currentTimeMillis())
waktuAbsensi = waktu
val absensi = Absensi(
npm = session.getUserNpm() ?: "202310715200",
nama = session.getUserName() ?: "Indris Alpasela",
latitude = latitude!!,
longitude = longitude!!,
waktu = waktu,
foto = foto!!
)
kirimKeN8n(activity, absensi)
absensiList.add(0, absensi)
isLoading = false
} else {
Toast.makeText(
context,
"Mohon ambil foto dan pastikan lokasi aktif",
Toast.LENGTH_LONG
).show()
}
},
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32)),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading,
elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp)
) {
if (isLoading) {
CircularProgressIndicator(
color = Color.White,
modifier = Modifier.size(28.dp)
)
} else {
Text(
"✉️",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.width(12.dp))
Text(
"Kirim Absensi",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.height(20.dp))
// RIWAYAT ABSENSI
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Riwayat Kehadiran",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = AkademikDarkBlue
)
if (absensiList.isNotEmpty()) {
Surface(
shape = RoundedCornerShape(20.dp),
color = AkademikLightBlue
) {
Text(
"${absensiList.size}",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = AkademikBlue
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
if (absensiList.isEmpty()) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.White),
shape = RoundedCornerShape(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(40.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
"📋",
style = MaterialTheme.typography.displayMedium
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"Belum ada riwayat kehadiran",
color = Color.Gray,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
} else {
LazyColumn {
items(absensiList) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
)
.padding(vertical = 6.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Foto Kecil
Image(
bitmap = item.foto.asImageBitmap(),
contentDescription = "Foto Absensi",
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(8.dp))
)
Spacer(modifier = Modifier.width(16.dp))
// Info
Column(modifier = Modifier.weight(1f)) {
Text(
item.nama,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = AkademikDarkBlue
)
Text(
item.npm,
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(6.dp))
Text(
item.waktu,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
// Status
Text(
"",
style = MaterialTheme.typography.headlineMedium
)
}
}
}
}
}
}
}
}
}

View File

@ -5,29 +5,37 @@ import android.content.SharedPreferences
class SessionManager(context: Context) {
private val prefs: SharedPreferences =
context.getSharedPreferences("user_session", Context.MODE_PRIVATE)
context.getSharedPreferences("AbsensiSession", Context.MODE_PRIVATE)
companion object {
private const val KEY_IS_LOGGED_IN = "is_logged_in"
private const val KEY_NPM = "npm"
private const val KEY_NAME = "name"
private const val KEY_IS_LOGGED_IN = "isLoggedIn"
private const val KEY_USER_NAME = "userName"
private const val KEY_USER_NPM = "userNpm"
}
fun saveLogin(npm: String, name: String) {
prefs.edit().apply {
putBoolean(KEY_IS_LOGGED_IN, true)
putString(KEY_NPM, npm)
putString(KEY_NAME, name)
apply()
}
fun saveLoginSession(nama: String, npm: String) {
val editor = prefs.edit()
editor.putBoolean(KEY_IS_LOGGED_IN, true)
editor.putString(KEY_USER_NAME, nama)
editor.putString(KEY_USER_NPM, npm)
editor.apply()
}
fun isLoggedIn(): Boolean = prefs.getBoolean(KEY_IS_LOGGED_IN, false)
fun isLoggedIn(): Boolean {
return prefs.getBoolean(KEY_IS_LOGGED_IN, false)
}
fun getUserName(): String? {
return prefs.getString(KEY_USER_NAME, null)
}
fun getUserNpm(): String? {
return prefs.getString(KEY_USER_NPM, null)
}
fun logout() {
prefs.edit().clear().apply()
val editor = prefs.edit()
editor.clear()
editor.apply()
}
fun getUserNpm(): String? = prefs.getString(KEY_NPM, null)
fun getUserName(): String? = prefs.getString(KEY_NAME, null)
}
}