EAS_202310715200_IndrisAlpasela
This commit is contained in:
parent
7cd8b241d0
commit
ca6d6e2d33
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user