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 org.json.JSONObject
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
/**
|
|
||||||
* Data class untuk menyimpan informasi absensi mahasiswa
|
|
||||||
*/
|
|
||||||
data class Absensi(
|
data class Absensi(
|
||||||
val npm: String,
|
val npm: String,
|
||||||
val nama: String,
|
val nama: String,
|
||||||
@ -16,30 +13,21 @@ data class Absensi(
|
|||||||
val waktu: String,
|
val waktu: String,
|
||||||
val foto: Bitmap
|
val foto: Bitmap
|
||||||
) {
|
) {
|
||||||
/**
|
|
||||||
* Convert objek Absensi ini menjadi JSONObject
|
|
||||||
* Siap untuk dikirim ke server
|
|
||||||
*/
|
|
||||||
fun toJson(): JSONObject {
|
fun toJson(): JSONObject {
|
||||||
val json = JSONObject()
|
val json = JSONObject()
|
||||||
json.put("npm", npm)
|
json.put("npm", npm)
|
||||||
json.put("nama", nama)
|
json.put("nama", nama)
|
||||||
json.put("latitude", latitude)
|
json.put("latitude", latitude)
|
||||||
json.put("longitude", longitude)
|
json.put("longitude", longitude)
|
||||||
json.put("timestamp", System.currentTimeMillis())
|
|
||||||
json.put("waktu", waktu)
|
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
|
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
|
// Tombol Login
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (npm == "202310715123" && password == "123") {
|
if (npm == "202310715200" && password == "123") {
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -14,21 +14,22 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
@ -38,7 +39,11 @@ import java.util.*
|
|||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
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 ==================
|
// ================== UTILS ==================
|
||||||
fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
|
fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
|
||||||
@ -55,14 +60,14 @@ fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) {
|
|||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
if (responseCode == 200) "Absensi diterima server" else "Absensi ditolak server",
|
if (responseCode == 200) "Absensi berhasil dikirim" else "Absensi gagal dikirim",
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
context.runOnUiThread {
|
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 {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Surface(
|
||||||
AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this)
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = AkademikLightBlue
|
||||||
|
) {
|
||||||
|
AbsensiScreen(activity = this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,30 +94,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================== COMPOSABLE UI ==================
|
// ================== COMPOSABLE UI ==================
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
|
fun AbsensiScreen(activity: ComponentActivity) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val session = remember { SessionManager(context) }
|
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 latitude by remember { mutableStateOf<Double?>(null) }
|
||||||
var longitude by remember { mutableStateOf<Double?>(null) }
|
var longitude by remember { mutableStateOf<Double?>(null) }
|
||||||
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
var foto by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
var waktuAbsensi by remember { mutableStateOf<String?>(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 absensiList = remember { mutableStateListOf<Absensi>() }
|
||||||
|
|
||||||
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
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 =====
|
// ===== Permission & Kamera =====
|
||||||
val locationPermissionLauncher =
|
val locationPermissionLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
@ -122,7 +123,7 @@ fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
|
|||||||
if (location != null) {
|
if (location != null) {
|
||||||
latitude = location.latitude
|
latitude = location.latitude
|
||||||
longitude = location.longitude
|
longitude = location.longitude
|
||||||
lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}"
|
lokasi = "${String.format("%.6f", location.latitude)}, ${String.format("%.6f", location.longitude)}"
|
||||||
} else {
|
} else {
|
||||||
lokasi = "Lokasi tidak tersedia"
|
lokasi = "Lokasi tidak tersedia"
|
||||||
}
|
}
|
||||||
@ -130,7 +131,7 @@ fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) {
|
|||||||
.addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
|
.addOnFailureListener { lokasi = "Gagal mengambil lokasi" }
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||||
cameraLauncher.launch(intent)
|
cameraLauncher.launch(intent)
|
||||||
} else {
|
} 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)
|
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showLogoutDialog = false }) {
|
||||||
|
Text("Batal", color = AkademikBlue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ================== UI ==================
|
// ================== 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(backgroundColor)
|
.padding(paddingValues)
|
||||||
.padding(24.dp)
|
.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
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(70.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = AkademikLightBlue
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Absensi Akademik",
|
"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,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = primaryColor
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AkademikDarkBlue
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
text = "NPM: ${session.getUserNpm() ?: "202310715200"}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// PALETTE WARNA BULETAN
|
// INFO CARD: LOKASI & WAKTU
|
||||||
Text("Pilih Warna Tema:", style = MaterialTheme.typography.bodyMedium, color = primaryColor)
|
Card(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
modifier = Modifier.fillMaxWidth(),
|
||||||
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors.forEach { color ->
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
Box(
|
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
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.fillMaxWidth()
|
||||||
.padding(4.dp)
|
.height(240.dp),
|
||||||
.background(color, shape = CircleShape)
|
shape = RoundedCornerShape(12.dp),
|
||||||
.clickable {
|
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
|
||||||
primaryColor = color
|
) {
|
||||||
backgroundColor = color.copy(alpha = 0.1f)
|
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))
|
|
||||||
Text(text = lokasi, color = primaryColor)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOMBOL AMBIL FOTO
|
||||||
Button(
|
Button(
|
||||||
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
|
.fillMaxWidth()
|
||||||
) { Text("Ambil Foto") }
|
.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))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// TOMBOL KIRIM ABSENSI
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (latitude != null && longitude != null && foto != null) {
|
if (latitude != null && longitude != null && foto != null) {
|
||||||
val waktu = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()).format(System.currentTimeMillis())
|
isLoading = true
|
||||||
|
val waktu = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale("id", "ID")).format(System.currentTimeMillis())
|
||||||
waktuAbsensi = waktu
|
waktuAbsensi = waktu
|
||||||
|
|
||||||
val absensi = Absensi(
|
val absensi = Absensi(
|
||||||
npm = session.getUserNpm() ?: "Faris Naufal Priatna",
|
npm = session.getUserNpm() ?: "202310715200",
|
||||||
nama = session.getUserName() ?: "202310715123",
|
nama = session.getUserName() ?: "Indris Alpasela",
|
||||||
latitude = latitude!!,
|
latitude = latitude!!,
|
||||||
longitude = longitude!!,
|
longitude = longitude!!,
|
||||||
waktu = waktu,
|
waktu = waktu,
|
||||||
foto = foto!!
|
foto = foto!!
|
||||||
)
|
)
|
||||||
kirimKeN8n(activity, absensi)
|
kirimKeN8n(activity, absensi)
|
||||||
absensiList.add(absensi)
|
absensiList.add(0, absensi)
|
||||||
|
isLoading = false
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Mohon ambil foto dan pastikan lokasi aktif",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = primaryColor)
|
.fillMaxWidth()
|
||||||
) { Text("Kirim Absensi") }
|
.height(60.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32)),
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
shape = RoundedCornerShape(12.dp),
|
||||||
waktuAbsensi?.let { Text("Waktu Absensi: $it", color = primaryColor) }
|
enabled = !isLoading,
|
||||||
|
elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp)
|
||||||
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")
|
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(16.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
Text("Riwayat Kehadiran", color = primaryColor)
|
|
||||||
|
// 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))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
if (absensiList.isEmpty()) Text("Belum ada absensi", color = primaryColor)
|
if (absensiList.isEmpty()) {
|
||||||
else LazyColumn {
|
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 ->
|
items(absensiList) { item ->
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 4.dp),
|
.padding(vertical = 6.dp),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Row(
|
||||||
Text("Nama: ${item.nama}", color = primaryColor)
|
modifier = Modifier.padding(16.dp),
|
||||||
Text("NPM: ${item.npm}", color = primaryColor)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
Text("Waktu: ${item.waktu}", color = primaryColor)
|
) {
|
||||||
Text("Lat: ${item.latitude}, Lon: ${item.longitude}", color = primaryColor)
|
// Foto Kecil
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Image(
|
Image(
|
||||||
bitmap = item.foto.asImageBitmap(),
|
bitmap = item.foto.asImageBitmap(),
|
||||||
contentDescription = "Foto Absensi",
|
contentDescription = "Foto Absensi",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.size(80.dp)
|
||||||
.height(150.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) {
|
class SessionManager(context: Context) {
|
||||||
private val prefs: SharedPreferences =
|
private val prefs: SharedPreferences =
|
||||||
context.getSharedPreferences("user_session", Context.MODE_PRIVATE)
|
context.getSharedPreferences("AbsensiSession", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_IS_LOGGED_IN = "is_logged_in"
|
private const val KEY_IS_LOGGED_IN = "isLoggedIn"
|
||||||
private const val KEY_NPM = "npm"
|
private const val KEY_USER_NAME = "userName"
|
||||||
private const val KEY_NAME = "name"
|
private const val KEY_USER_NPM = "userNpm"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLogin(npm: String, name: String) {
|
fun saveLoginSession(nama: String, npm: String) {
|
||||||
prefs.edit().apply {
|
val editor = prefs.edit()
|
||||||
putBoolean(KEY_IS_LOGGED_IN, true)
|
editor.putBoolean(KEY_IS_LOGGED_IN, true)
|
||||||
putString(KEY_NPM, npm)
|
editor.putString(KEY_USER_NAME, nama)
|
||||||
putString(KEY_NAME, name)
|
editor.putString(KEY_USER_NPM, npm)
|
||||||
apply()
|
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() {
|
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