EAS-202310715060-MuhammadYusronAmrullah.update
This commit is contained in:
parent
8326d50979
commit
2b93266ee1
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
50
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
4
.kotlin/errors/errors-1768292897859.log
Normal file
4
.kotlin/errors/errors-1768292897859.log
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
kotlin version: 2.0.21
|
||||||
|
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
|
||||||
|
1. Kotlin compile daemon is ready
|
||||||
|
|
||||||
@ -15,18 +15,40 @@ 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.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
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.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
|
import com.google.android.gms.location.Priority
|
||||||
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
@ -37,13 +59,9 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
const val KAMPUS_LAT = -6.222967558410948
|
const val KAMPUS_LAT = -6.222967558410948
|
||||||
const val KAMPUS_LON = 107.00931291609834
|
const val KAMPUS_LON = 107.00931291609834
|
||||||
const val MAX_RADIUS = 50
|
const val MAX_RADIUS = 50 // meter
|
||||||
// meter
|
|
||||||
|
|
||||||
fun hitungJarak(
|
fun hitungJarak(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Float {
|
||||||
lat1: Double, lon1: Double,
|
|
||||||
lat2: Double, lon2: Double
|
|
||||||
): Float {
|
|
||||||
val hasil = FloatArray(1)
|
val hasil = FloatArray(1)
|
||||||
Location.distanceBetween(lat1, lon1, lat2, lon2, hasil)
|
Location.distanceBetween(lat1, lon1, lat2, lon2, hasil)
|
||||||
return hasil[0]
|
return hasil[0]
|
||||||
@ -58,62 +76,39 @@ fun bitmapToBase64(bitmap: Bitmap): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun kirimKeN8n(
|
fun kirimKeN8n(
|
||||||
context: ComponentActivity,
|
context: ComponentActivity, npm: String, nama: String, mataKuliah: String,
|
||||||
npm: String,
|
latitude: Double, longitude: Double, foto: Bitmap, status: String,
|
||||||
nama: String,
|
onFinished: () -> Unit
|
||||||
mataKuliah: String,
|
|
||||||
latitude: Double,
|
|
||||||
longitude: Double,
|
|
||||||
foto: Bitmap,
|
|
||||||
status: String
|
|
||||||
) {
|
) {
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
|
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
|
||||||
val conn = url.openConnection() as HttpURLConnection
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
|
||||||
conn.requestMethod = "POST"
|
conn.requestMethod = "POST"
|
||||||
conn.setRequestProperty("Content-Type", "application/json")
|
conn.setRequestProperty("Content-Type", "application/json")
|
||||||
conn.doOutput = true
|
conn.doOutput = true
|
||||||
|
|
||||||
val json = JSONObject().apply {
|
val json = JSONObject().apply {
|
||||||
put("npm", npm)
|
put("npm", npm); put("nama", nama); put("mata_kuliah", mataKuliah)
|
||||||
put("nama", nama)
|
put("latitude", latitude); put("longitude", longitude)
|
||||||
put("mata_kuliah", mataKuliah)
|
put("timestamp", System.currentTimeMillis()); put("status", status)
|
||||||
put("latitude", latitude)
|
|
||||||
put("longitude", longitude)
|
|
||||||
put("timestamp", System.currentTimeMillis())
|
|
||||||
put("status", status)
|
|
||||||
put("foto_base64", bitmapToBase64(foto))
|
put("foto_base64", bitmapToBase64(foto))
|
||||||
}
|
}
|
||||||
|
conn.outputStream.use { it.write(json.toString().toByteArray()) }
|
||||||
conn.outputStream.use {
|
|
||||||
it.write(json.toString().toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseCode = conn.responseCode
|
val responseCode = conn.responseCode
|
||||||
|
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
if (responseCode == 200)
|
if (responseCode == 200) "Absensi $status berhasil dikirim" else "Server menolak absensi",
|
||||||
"Absensi $status"
|
|
||||||
else
|
|
||||||
"Server menolak absensi",
|
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
Toast.makeText(
|
Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show()
|
||||||
context,
|
|
||||||
"Gagal kirim ke server",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
context.runOnUiThread(onFinished)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,247 +116,234 @@ fun kirimKeN8n(
|
|||||||
/* ================= ACTIVITY ================= */
|
/* ================= ACTIVITY ================= */
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
SistemAkademikTheme {
|
SistemAkademikTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
AbsensiScreen(activity = this)
|
||||||
AbsensiScreen(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
activity = this
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= UI ================= */
|
/* ================= UI UTAMA ================= */
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AbsensiScreen(
|
fun AbsensiScreen(activity: ComponentActivity) {
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
activity: ComponentActivity
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// States
|
||||||
var npm by remember { mutableStateOf("") }
|
var npm by remember { mutableStateOf("") }
|
||||||
var nama by remember { mutableStateOf("") }
|
var nama by remember { mutableStateOf("") }
|
||||||
var mataKuliah by remember { mutableStateOf("") }
|
var mataKuliah by remember { mutableStateOf("") }
|
||||||
|
var lokasi by remember { mutableStateOf<String?>(null) }
|
||||||
var lokasi by remember { mutableStateOf("Koordinat: -") }
|
|
||||||
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 isUploading by remember { mutableStateOf(false) }
|
||||||
|
var isFetchingLocation by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val fusedLocationClient =
|
val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) }
|
||||||
LocationServices.getFusedLocationProviderClient(context)
|
|
||||||
|
|
||||||
/* ===== Permission Lokasi ===== */
|
fun requestLocationUpdate() {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
val locationPermissionLauncher =
|
lokasi = "Izin lokasi belum diberikan."
|
||||||
rememberLauncherForActivityResult(
|
return
|
||||||
ActivityResultContracts.RequestPermission()
|
}
|
||||||
) { granted ->
|
isFetchingLocation = true
|
||||||
if (granted) {
|
lokasi = "Mencari lokasi..."
|
||||||
if (
|
fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
|
||||||
ContextCompat.checkSelfPermission(
|
.addOnSuccessListener { location: Location? ->
|
||||||
context,
|
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
fusedLocationClient.lastLocation
|
|
||||||
.addOnSuccessListener { location ->
|
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
latitude = location.latitude
|
latitude = location.latitude
|
||||||
longitude = location.longitude
|
longitude = location.longitude
|
||||||
lokasi =
|
lokasi = "Lat: ${String.format("%.6f", location.latitude)}\nLon: ${String.format("%.6f", location.longitude)}"
|
||||||
"Lat: ${location.latitude}\nLon: ${location.longitude}"
|
|
||||||
} else {
|
} else {
|
||||||
lokasi = "Lokasi tidak tersedia"
|
lokasi = "Gagal mendapatkan lokasi. Coba lagi."
|
||||||
}
|
}
|
||||||
|
isFetchingLocation = false
|
||||||
}
|
}
|
||||||
}
|
.addOnFailureListener {
|
||||||
} else {
|
lokasi = "Gagal mendapatkan lokasi. Coba lagi."
|
||||||
Toast.makeText(
|
isFetchingLocation = false
|
||||||
context,
|
|
||||||
"Izin lokasi ditolak",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Kamera ===== */
|
val locationPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) requestLocationUpdate() else {
|
||||||
|
Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
lokasi = "Izin lokasi ditolak."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val cameraLauncher =
|
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
val bitmap = result.data?.extras?.get("data") as? Bitmap
|
val bitmap = result.data?.extras?.get("data") as? Bitmap
|
||||||
if (bitmap != null) {
|
if (bitmap != null) foto = bitmap else Toast.makeText(context, "Gagal mengambil foto", Toast.LENGTH_SHORT).show()
|
||||||
foto = bitmap
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Foto berhasil diambil",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Gagal mengambil foto",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
locationPermissionLauncher.launch(
|
if (granted) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
else Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }
|
||||||
|
|
||||||
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
|
val gradient = Brush.linearGradient(
|
||||||
|
colors = listOf(MaterialTheme.colorScheme.primary.copy(alpha = 0.18f), Color.Transparent),
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
|
||||||
)
|
)
|
||||||
}
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize().background(gradient).padding(WindowInsets.systemBars.asPaddingValues()),
|
||||||
/* ===== UI ===== */
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp)
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(24.dp),
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
|
item { AnimatedHeader(delay = 0) }
|
||||||
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
|
||||||
Text(
|
item { AnimatedInputCard(npm, nama, mataKuliah, { npm = it }, { nama = it }, { mataKuliah = it }) }
|
||||||
text = "Absensi Akademik",
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
style = MaterialTheme.typography.titleLarge
|
|
||||||
|
item {
|
||||||
|
AnimatedActionSection(isFetchingLocation, { requestLocationUpdate() }, { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) }, lokasi)
|
||||||
|
}
|
||||||
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
|
||||||
|
item { AnimatedPhotoPreview(foto) }
|
||||||
|
item { Spacer(modifier = Modifier.height(24.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
val canSubmit = npm.isNotEmpty() && nama.isNotEmpty() && mataKuliah.isNotEmpty() && latitude != null && longitude != null && foto != null
|
||||||
|
AnimatedSubmitButton(
|
||||||
|
enabled = canSubmit && !isUploading,
|
||||||
|
isUploading = isUploading,
|
||||||
|
onClick = {
|
||||||
|
isUploading = true
|
||||||
|
val jarak = hitungJarak(latitude!!, longitude!!, KAMPUS_LAT, KAMPUS_LON)
|
||||||
|
val statusAbsen = if (jarak <= MAX_RADIUS) "HADIR" else "DITOLAK"
|
||||||
|
if (statusAbsen == "HADIR") {
|
||||||
|
kirimKeN8n(activity, npm, nama, mataKuliah, latitude!!, longitude!!, foto!!, statusAbsen) { isUploading = false }
|
||||||
|
} else {
|
||||||
|
val jarakKm = String.format("%.2f", jarak / 1000)
|
||||||
|
Toast.makeText(context, "Absensi DITOLAK: Anda berada ${jarakKm}km dari kampus.", Toast.LENGTH_LONG).show()
|
||||||
|
isUploading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= KOMPONEN UI DENGAN ANIMASI ================= */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedHeader(delay: Long = 0) {
|
||||||
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) { delay(delay); visible = true }
|
||||||
|
AnimatedVisibility(visible = visible, enter = fadeIn(spring(stiffness = Spring.StiffnessLow)) + slideInVertically(spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow), initialOffsetY = { -it })) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("E-ABSENSI", style = MaterialTheme.typography.displaySmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Text("Universitas Bhayangkara Jakarta Raya", style = MaterialTheme.typography.titleMedium, color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedInputCard(
|
||||||
|
npm: String, nama: String, mataKuliah: String,
|
||||||
|
onNpmChange: (String) -> Unit, onNamaChange: (String) -> Unit, onMataKuliahChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var cardVisible by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) { delay(200); cardVisible = true }
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = cardVisible, enter = fadeIn(spring(stiffness = Spring.StiffnessLow)) + slideInVertically(spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow), initialOffsetY = { it / 2 })) {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), elevation = CardDefaults.cardElevation(4.dp)) {
|
||||||
|
Column(modifier = Modifier.padding(20.dp)) {
|
||||||
|
Text("Data Mahasiswa", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(value = npm, onValueChange = onNpmChange, label = { Text("NPM") }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), leadingIcon = { Icon(Icons.Default.Person, "NPM") })
|
||||||
OutlinedTextField(
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
value = npm,
|
OutlinedTextField(value = nama, onValueChange = onNamaChange, label = { Text("Nama Lengkap") }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), leadingIcon = { Icon(Icons.Default.Badge, "Nama") })
|
||||||
onValueChange = { npm = it },
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
label = { Text("NPM") },
|
OutlinedTextField(value = mataKuliah, onValueChange = onMataKuliahChange, label = { Text("Mata Kuliah") }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), leadingIcon = { Icon(Icons.Default.Book, "Mata Kuliah") })
|
||||||
modifier = Modifier.fillMaxWidth()
|
}
|
||||||
)
|
}
|
||||||
|
}
|
||||||
OutlinedTextField(
|
|
||||||
value = nama,
|
|
||||||
onValueChange = { nama = it },
|
|
||||||
label = { Text("Nama") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = mataKuliah,
|
|
||||||
onValueChange = { mataKuliah = it },
|
|
||||||
label = { Text("Mata Kuliah") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Text(text = lokasi)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
cameraPermissionLauncher.launch(
|
|
||||||
Manifest.permission.CAMERA
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Ambil Foto")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== PREVIEW FOTO ===== */
|
@Composable
|
||||||
|
fun AnimatedActionSection(isFetchingLocation: Boolean, onRefresh: () -> Unit, onTakePhoto: () -> Unit, lokasi: String?) {
|
||||||
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) { delay(400); visible = true }
|
||||||
|
|
||||||
if (foto != null) {
|
AnimatedVisibility(visible = visible, enter = fadeIn(spring(stiffness = Spring.StiffnessLow)) + slideInVertically(spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow), initialOffsetY = { it / 2 })) {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Column {
|
||||||
Image(
|
StatusRowWithRefresh(icon = Icons.Default.LocationOn, title = "Lokasi GPS", status = lokasi ?: "Meminta izin...", isRefreshing = isFetchingLocation, onRefresh = onRefresh)
|
||||||
bitmap = foto!!.asImageBitmap(),
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
contentDescription = "Preview Foto Absensi",
|
ActionButton(text = "Ambil Foto Kehadiran", icon = Icons.Default.CameraAlt, onClick = onTakePhoto)
|
||||||
modifier = Modifier
|
}
|
||||||
.fillMaxWidth()
|
}
|
||||||
.height(220.dp),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
@Composable
|
||||||
|
fun AnimatedPhotoPreview(foto: Bitmap?) {
|
||||||
|
AnimatedVisibility(visible = foto != null, enter = fadeIn(spring(stiffness = Spring.StiffnessMedium)) + slideInVertically(spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessMedium), initialOffsetY = { it / 2 })) {
|
||||||
|
Card(shape = RoundedCornerShape(16.dp), elevation = CardDefaults.cardElevation(4.dp)) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Image(bitmap = foto!!.asImageBitmap(), "Preview Foto Absensi", modifier = Modifier.fillMaxWidth().height(250.dp).clip(RoundedCornerShape(16.dp)), contentScale = ContentScale.Crop)
|
||||||
|
Icon(Icons.Default.CheckCircle, "Foto Diambil", tint = Color.White.copy(alpha = 0.8f), modifier = Modifier.size(60.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button(
|
@Composable
|
||||||
onClick = {
|
fun AnimatedSubmitButton(enabled: Boolean, isUploading: Boolean, onClick: () -> Unit) {
|
||||||
if (
|
var visible by remember { mutableStateOf(false) }
|
||||||
npm.isNotEmpty() &&
|
LaunchedEffect(Unit) { delay(600); visible = true }
|
||||||
nama.isNotEmpty() &&
|
|
||||||
mataKuliah.isNotEmpty() &&
|
|
||||||
latitude != null &&
|
|
||||||
longitude != null &&
|
|
||||||
foto != null
|
|
||||||
) {
|
|
||||||
|
|
||||||
val jarak = hitungJarak(
|
AnimatedVisibility(visible = visible, enter = fadeIn(spring(stiffness = Spring.StiffnessLow)) + slideInVertically(spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessLow), initialOffsetY = { it / 2 })) {
|
||||||
latitude!!,
|
Button(onClick = onClick, enabled = enabled, modifier = Modifier.fillMaxWidth().height(56.dp)) {
|
||||||
longitude!!,
|
if (isUploading) {
|
||||||
KAMPUS_LAT,
|
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(24.dp))
|
||||||
KAMPUS_LON
|
|
||||||
)
|
|
||||||
|
|
||||||
val status =
|
|
||||||
if (jarak <= MAX_RADIUS) "HADIR" else "DITOLAK"
|
|
||||||
|
|
||||||
if (status == "HADIR") {
|
|
||||||
kirimKeN8n(
|
|
||||||
activity,
|
|
||||||
npm,
|
|
||||||
nama,
|
|
||||||
mataKuliah,
|
|
||||||
latitude!!,
|
|
||||||
longitude!!,
|
|
||||||
foto!!,
|
|
||||||
status
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Text("KIRIM ABSENSI", fontWeight = FontWeight.Bold)
|
||||||
context,
|
}
|
||||||
"Absensi ditolak (di luar area)",
|
}
|
||||||
Toast.LENGTH_LONG
|
}
|
||||||
).show()
|
}
|
||||||
|
|
||||||
|
/* ================= KOMPONEN UI STANDAR ================= */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatusRowWithRefresh(icon: ImageVector, title: String, status: String, isRefreshing: Boolean, onRefresh: () -> Unit) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Icon(icon, contentDescription = title, tint = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(title, fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||||
|
Text(status, style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
||||||
|
}
|
||||||
|
if (isRefreshing) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = onRefresh) {
|
||||||
|
Icon(Icons.Default.Refresh, "Cari Ulang Lokasi")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
@Composable
|
||||||
Toast.makeText(
|
fun ActionButton(text: String, icon: ImageVector, onClick: () -> Unit) {
|
||||||
context,
|
FilledTonalButton(onClick = onClick, modifier = Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(16.dp)) {
|
||||||
"Data absensi belum lengkap",
|
Icon(icon, contentDescription = null)
|
||||||
Toast.LENGTH_SHORT
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
).show()
|
Text(text, fontWeight = FontWeight.Bold)
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Kirim Absensi")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user