diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f0c6ad0
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/markdown.xml b/.idea/markdown.xml
new file mode 100644
index 0000000..c61ea33
--- /dev/null
+++ b/.idea/markdown.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4619836..d058e92 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
@@ -19,17 +20,27 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SistemAkademik">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
new file mode 100644
index 0000000..bcd97fa
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt
@@ -0,0 +1,44 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.graphics.Bitmap
+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,
+ val latitude: Double,
+ val longitude: Double,
+ val waktu: String,
+ val foto: Bitmap? // ⬅️ HARUS nullable
+) {
+ /**
+ * Convert objek Absensi menjadi JSON
+ */
+ fun toJson(): JSONObject {
+ val json = JSONObject()
+ json.put("npm", npm)
+ json.put("nama", nama)
+ json.put("latitude", latitude)
+ json.put("longitude", longitude)
+ json.put("waktu", waktu)
+ json.put("timestamp", System.currentTimeMillis())
+ json.put("foto_base64", foto?.let { bitmapToBase64(it) })
+ return json
+ }
+
+ companion object {
+ fun bitmapToBase64(bitmap: Bitmap): String {
+ val outputStream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+ return Base64.encodeToString(
+ outputStream.toByteArray(),
+ Base64.NO_WRAP
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt
new file mode 100644
index 0000000..dda7d73
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt
@@ -0,0 +1,182 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.animation.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Lock
+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.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+class LoginActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+
+ val typography = Typography(
+ titleLarge = MaterialTheme.typography.titleLarge.copy(
+ fontFamily = FontFamily.SansSerif,
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp
+ )
+ )
+
+ MaterialTheme(
+ typography = typography,
+ colorScheme = if (isSystemInDarkTheme())
+ darkColorScheme()
+ else
+ lightColorScheme()
+ ) {
+ LoginScreen()
+ }
+ }
+ }
+}
+
+@Composable
+fun LoginScreen() {
+ val context = LocalContext.current
+ var npm by remember { mutableStateOf("") }
+ var password by remember { mutableStateOf("") }
+ var showCard by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) { showCard = true }
+
+ val buttonGradient = Brush.horizontalGradient(
+ listOf(Color(0xFF1565C0), Color(0xFF42A5F5))
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black) // 🔥 BACKGROUND HITAM
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+
+ AnimatedVisibility(
+ visible = showCard,
+ enter = fadeIn() + scaleIn(initialScale = 0.9f)
+ ) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(22.dp),
+ elevation = CardDefaults.cardElevation(14.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(28.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Image(
+ painter = painterResource(id = R.drawable.logo),
+ contentDescription = "Logo",
+ modifier = Modifier
+ .size(120.dp)
+ .clip(CircleShape)
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(
+ "Absensi PPB",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Text(
+ "Login Mahasiswa",
+ fontSize = 14.sp,
+ color = Color.Gray
+ )
+
+ Spacer(modifier = Modifier.height(28.dp))
+
+ OutlinedTextField(
+ value = npm,
+ onValueChange = { npm = it },
+ label = { Text("NPM") },
+ leadingIcon = {
+ Icon(Icons.Default.AccountCircle, contentDescription = null)
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ OutlinedTextField(
+ value = password,
+ onValueChange = { password = it },
+ label = { Text("Password") },
+ leadingIcon = {
+ Icon(Icons.Default.Lock, contentDescription = null)
+ },
+ visualTransformation = PasswordVisualTransformation(),
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Button(
+ onClick = {
+ if (npm == "202310715307" && password == "yoo23") {
+ context.startActivity(
+ Intent(context, MainActivity::class.java)
+ )
+ } else {
+ Toast.makeText(
+ context,
+ "NPM atau password salah",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ },
+ colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
+ contentPadding = PaddingValues(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ .background(
+ brush = buttonGradient,
+ shape = RoundedCornerShape(14.dp)
+ )
+ ) {
+ Text(
+ "Login",
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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 c774502..92642e4 100644
--- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt
@@ -3,110 +3,203 @@ package id.ac.ubharajaya.sistemakademik
import android.Manifest
import android.app.Activity
import android.content.Intent
-import android.content.pm.PackageManager
import android.graphics.Bitmap
+import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
-import android.util.Base64
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+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.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.google.android.gms.location.LocationServices
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
-import org.json.JSONObject
-import java.io.ByteArrayOutputStream
-import java.net.HttpURLConnection
-import java.net.URL
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.text.SimpleDateFormat
+import java.util.*
import kotlin.concurrent.thread
-/* ================= UTIL ================= */
+/* ================== KONFIG LOCKOUT ================== */
+const val LOCKOUT_DURATION = 10 * 60 * 1000L // 1 menit
-fun bitmapToBase64(bitmap: Bitmap): String {
- val outputStream = ByteArrayOutputStream()
- bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
- return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
-}
+/* ================== REPOSITORY ================== */
+class AbsensiRepository {
+ fun kirimAbsensi(absensi: Absensi, onResult: (Boolean) -> Unit) {
+ thread {
+ try {
+ val url = java.net.URL(
+ "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
+ )
+ val conn = url.openConnection() as java.net.HttpURLConnection
+ conn.requestMethod = "POST"
+ conn.setRequestProperty("Content-Type", "application/json")
+ conn.doOutput = true
-fun kirimKeN8n(
- context: ComponentActivity,
- latitude: Double,
- longitude: Double,
- foto: Bitmap
-) {
- thread {
- try {
- val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
-// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
- val conn = url.openConnection() as HttpURLConnection
+ conn.outputStream.use {
+ it.write(absensi.toJson().toString().toByteArray())
+ }
- conn.requestMethod = "POST"
- conn.setRequestProperty("Content-Type", "application/json")
- conn.doOutput = true
-
- val json = JSONObject().apply {
- put("npm", "12345")
- put("nama","Arif R D")
- put("latitude", latitude)
- put("longitude", longitude)
- put("timestamp", System.currentTimeMillis())
- put("foto_base64", bitmapToBase64(foto))
- }
-
- conn.outputStream.use {
- it.write(json.toString().toByteArray())
- }
-
- val responseCode = conn.responseCode
-
- context.runOnUiThread {
- Toast.makeText(
- context,
- if (responseCode == 200)
- "Absensi diterima server"
- else
- "Absensi ditolak server",
- Toast.LENGTH_SHORT
- ).show()
- }
-
- conn.disconnect()
-
- } catch (_: Exception) {
- context.runOnUiThread {
- Toast.makeText(
- context,
- "Gagal kirim ke server",
- Toast.LENGTH_SHORT
- ).show()
+ onResult(conn.responseCode == 200)
+ conn.disconnect()
+ } catch (e: Exception) {
+ onResult(false)
}
}
}
}
-/* ================= ACTIVITY ================= */
+/* ================== VIEWMODEL ================== */
+class AbsensiViewModel(
+ private val repo: AbsensiRepository,
+ private val activity: ComponentActivity
+) : ViewModel() {
+ var lokasi by mutableStateOf("Koordinat: -")
+ private set
+
+ var latitude by mutableStateOf(null)
+ private set
+
+ var longitude by mutableStateOf(null)
+ private set
+
+ var foto by mutableStateOf(null)
+ private set
+
+ var absensiList = mutableStateListOf()
+
+ var waktuAbsensi by mutableStateOf(null)
+ private set
+
+ var isLoading by mutableStateOf(false)
+ private set
+
+ var sisaLockout by mutableStateOf(0L)
+ private set
+
+ init {
+ startLockoutCountdown()
+ }
+
+ fun updateLokasi(lat: Double, lon: Double) {
+ latitude = lat
+ longitude = lon
+ lokasi = "Lat: $lat\nLon: $lon"
+ }
+
+ fun updateFoto(bitmap: Bitmap) {
+ foto = bitmap
+ }
+
+ private fun getLastAbsensiTime(): Long {
+ val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
+ return prefs.getLong("last_absensi_time", 0L)
+ }
+
+ private fun simpanWaktuAbsensi() {
+ val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
+ prefs.edit().putLong("last_absensi_time", System.currentTimeMillis()).apply()
+ }
+
+ private fun startLockoutCountdown() {
+ viewModelScope.launch {
+ while (true) {
+ val diff = System.currentTimeMillis() - getLastAbsensiTime()
+ sisaLockout =
+ if (diff < LOCKOUT_DURATION) LOCKOUT_DURATION - diff else 0L
+ delay(1000)
+ }
+ }
+ }
+
+ fun kirimAbsensi() {
+ if (sisaLockout > 0) {
+ Toast.makeText(
+ activity,
+ "Sudah absen, tunggu ${(sisaLockout / 1000) / 60} menit",
+ Toast.LENGTH_LONG
+ ).show()
+ return
+ }
+
+ if (latitude == null || longitude == null || foto == null) {
+ Toast.makeText(activity, "Lokasi atau foto belum lengkap", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val waktu = SimpleDateFormat(
+ "dd MMM yyyy HH:mm:ss",
+ Locale.getDefault()
+ ).format(System.currentTimeMillis())
+
+ val absensi = Absensi(
+ npm = "202310715307",
+ nama = "Satrio Putra Wardani",
+ latitude = latitude!!,
+ longitude = longitude!!,
+ waktu = waktu,
+ foto = foto
+ )
+
+ isLoading = true
+ repo.kirimAbsensi(absensi) { success ->
+ viewModelScope.launch {
+ isLoading = false
+ if (success) {
+ absensiList.add(absensi)
+ waktuAbsensi = waktu
+ simpanWaktuAbsensi()
+ Toast.makeText(activity, "Absensi berhasil", Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(activity, "Gagal kirim absensi", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+}
+
+/* ================== MAIN ACTIVITY ================== */
+@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
+ private lateinit var viewModel: AbsensiViewModel
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+ viewModel = AbsensiViewModel(AbsensiRepository(), this)
+
setContent {
SistemAkademikTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Scaffold(
+ topBar = { TopBarLogout(this) }
+ ) { padding ->
AbsensiScreen(
- modifier = Modifier.padding(innerPadding),
- activity = this
+ modifier = Modifier.padding(padding),
+ viewModel = viewModel
)
}
}
@@ -114,161 +207,174 @@ class MainActivity : ComponentActivity() {
}
}
-/* ================= UI ================= */
-
+/* ================== TOP BAR ================== */
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun AbsensiScreen(
- modifier: Modifier = Modifier,
- activity: ComponentActivity
-) {
+fun TopBarLogout(activity: ComponentActivity) {
+ val context = LocalContext.current
+ TopAppBar(
+ title = {
+ Text(
+ " Sistem Akademik ",
+ style = MaterialTheme.typography.titleLarge.copy(
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White
+ )
+ )
+ },
+ actions = {
+ TextButton(onClick = {
+ val intent = Intent(context, LoginActivity::class.java)
+ intent.flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ context.startActivity(intent)
+ }) {
+ Text(
+ "Logout",
+ style = MaterialTheme.typography.labelLarge,
+ color = Color.Red
+ )
+ }
+ }
+ )
+}
+
+/* ================== UI ================== */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AbsensiScreen(modifier: Modifier, viewModel: AbsensiViewModel) {
+
val context = LocalContext.current
- var lokasi by remember { mutableStateOf("Koordinat: -") }
- var latitude by remember { mutableStateOf(null) }
- var longitude by remember { mutableStateOf(null) }
- var foto by remember { mutableStateOf(null) }
-
- val fusedLocationClient =
- LocationServices.getFusedLocationProviderClient(context)
-
- /* ===== Permission Lokasi ===== */
-
val locationPermissionLauncher =
- rememberLauncherForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { granted ->
+ rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
-
- if (
- ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
- ) {
-
- fusedLocationClient.lastLocation
- .addOnSuccessListener { location ->
- if (location != null) {
- latitude = location.latitude
- longitude = location.longitude
- lokasi =
- "Lat: ${location.latitude}\nLon: ${location.longitude}"
- } else {
- lokasi = "Lokasi tidak tersedia"
- }
+ LocationServices.getFusedLocationProviderClient(context)
+ .lastLocation.addOnSuccessListener {
+ it?.let { loc ->
+ viewModel.updateLokasi(loc.latitude, loc.longitude)
}
- .addOnFailureListener {
- lokasi = "Gagal mengambil lokasi"
- }
- }
-
- } else {
- Toast.makeText(
- context,
- "Izin lokasi ditolak",
- Toast.LENGTH_SHORT
- ).show()
+ }
}
}
- /* ===== Kamera ===== */
-
val cameraLauncher =
- rememberLauncherForActivityResult(
- ActivityResultContracts.StartActivityForResult()
- ) { result ->
- if (result.resultCode == Activity.RESULT_OK) {
+ rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == Activity.RESULT_OK) {
val bitmap =
- result.data?.extras?.getParcelable("data", Bitmap::class.java)
- if (bitmap != null) {
- foto = bitmap
- Toast.makeText(
- context,
- "Foto berhasil diambil",
- Toast.LENGTH_SHORT
- ).show()
- }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ it.data?.extras?.getParcelable("data", Bitmap::class.java)
+ else it.data?.extras?.getParcelable("data")
+ bitmap?.let { bmp -> viewModel.updateFoto(bmp) }
}
}
val cameraPermissionLauncher =
- rememberLauncherForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { granted ->
- if (granted) {
- val intent =
- Intent(MediaStore.ACTION_IMAGE_CAPTURE)
- cameraLauncher.launch(intent)
- } else {
- Toast.makeText(
- context,
- "Izin kamera ditolak",
- Toast.LENGTH_SHORT
- ).show()
- }
+ rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
}
- /* ===== Request Awal ===== */
-
LaunchedEffect(Unit) {
- locationPermissionLauncher.launch(
- Manifest.permission.ACCESS_FINE_LOCATION
- )
+ locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
- /* ===== UI ===== */
-
Column(
modifier = modifier
.fillMaxSize()
- .padding(24.dp),
- verticalArrangement = Arrangement.Center
+ .background(Color.White)
+ .padding(24.dp)
) {
Text(
text = "Absensi Akademik",
- style = MaterialTheme.typography.titleLarge
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.headlineSmall.copy(
+ fontWeight = FontWeight.Bold,
+ color = Color.Black
+ )
)
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(Modifier.height(16.dp))
- Text(text = lokasi)
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(6.dp)
+ ) {
+ Column(Modifier.padding(16.dp)) {
+ Text(
+ "Lokasi:",
+ style = MaterialTheme.typography.bodyLarge.copy(
+ fontWeight = FontWeight.SemiBold,
+ color = Color.White
+ )
+ )
+ Text(
+ viewModel.lokasi,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = Color.White
+ )
+ )
+ }
+ }
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(Modifier.height(16.dp))
Button(
- onClick = {
- cameraPermissionLauncher.launch(
- Manifest.permission.CAMERA
- )
- },
+ onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
modifier = Modifier.fillMaxWidth()
) {
Text("Ambil Foto")
}
- Spacer(modifier = Modifier.height(12.dp))
+ Spacer(Modifier.height(12.dp))
Button(
- onClick = {
- if (latitude != null && longitude != null && foto != null) {
- kirimKeN8n(
- activity,
- latitude!!,
- longitude!!,
- foto!!
- )
- } else {
- Toast.makeText(
- context,
- "Lokasi atau foto belum lengkap",
- Toast.LENGTH_SHORT
- ).show()
- }
- },
- modifier = Modifier.fillMaxWidth()
+ onClick = { viewModel.kirimAbsensi() },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !viewModel.isLoading
) {
- Text("Kirim Absensi")
+ if (viewModel.isLoading)
+ CircularProgressIndicator(
+ modifier = Modifier.size(22.dp),
+ strokeWidth = 2.dp
+ )
+ else Text("Kirim Absensi")
+ }
+
+ Spacer(Modifier.height(24.dp))
+
+ LazyColumn {
+ items(viewModel.absensiList) { item ->
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp)
+ .animateContentSize()
+ ) {
+ Column(Modifier.padding(16.dp)) {
+ Text(
+ item.nama,
+ style = MaterialTheme.typography.bodyLarge.copy(
+ fontWeight = FontWeight.Bold,
+ color = Color.White
+ )
+ )
+ Text(item.npm, color = Color.White)
+ Text(item.waktu, color = Color.White)
+ item.foto?.let {
+ Image(
+ bitmap = it.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(150.dp)
+ .clip(RoundedCornerShape(12.dp))
+ )
+ }
+ }
+ }
+ }
}
}
}
diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/SplashActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SplashActivity.kt
new file mode 100644
index 0000000..94c32ea
--- /dev/null
+++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SplashActivity.kt
@@ -0,0 +1,62 @@
+package id.ac.ubharajaya.sistemakademik
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+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.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+
+class SplashActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ SplashScreen {
+ startActivity(Intent(this, LoginActivity::class.java))
+ finish()
+ }
+ }
+ }
+}
+
+@Composable
+fun SplashScreen(onFinish: () -> Unit) {
+ LaunchedEffect(Unit) {
+ delay(2000) // 2 detik
+ onFinish()
+ }
+
+ // 🔥 GRADIENT ABU-ABU MODERN
+ val gradient = Brush.verticalGradient(
+ listOf(
+ Color(0xFF2C2C2C), // Dark Grey
+ Color(0xFF757575) // Light Grey
+ )
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(gradient),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.logo),
+ contentDescription = "Logo",
+ modifier = Modifier
+ .size(140.dp)
+ .clip(CircleShape)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png
new file mode 100644
index 0000000..9064055
Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ
diff --git a/app/src/main/res/logo.png b/app/src/main/res/logo.png
new file mode 100644
index 0000000..e51154f
Binary files /dev/null and b/app/src/main/res/logo.png differ