diff --git a/.kotlin/errors/errors-1768333509271.log b/.kotlin/errors/errors-1768333509271.log new file mode 100644 index 0000000..820c7c6 --- /dev/null +++ b/.kotlin/errors/errors-1768333509271.log @@ -0,0 +1,65 @@ +kotlin version: 2.0.21 +error message: Daemon compilation failed: Connection to the Kotlin daemon has been unexpectedly lost. This might be caused by the daemon being killed by another process or the operating system, or by JVM crash. +org.jetbrains.kotlin.gradle.tasks.DaemonCrashedException: Connection to the Kotlin daemon has been unexpectedly lost. This might be caused by the daemon being killed by another process or the operating system, or by JVM crash. + at org.jetbrains.kotlin.gradle.tasks.TasksUtilsKt.wrapAndRethrowCompilationException(tasksUtils.kt:55) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:243) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) + at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:76) + at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) + at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) + at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) + at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) + at org.gradle.internal.Factories$1.create(Factories.java:31) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) + at java.base/java.lang.Thread.run(Unknown Source) +Caused by: java.rmi.UnmarshalException: Error unmarshaling return header; nested exception is: + java.net.SocketException: Connection reset + at java.rmi/sun.rmi.transport.StreamRemoteCall.executeCall(Unknown Source) + at java.rmi/sun.rmi.server.UnicastRef.invoke(Unknown Source) + at java.rmi/java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(Unknown Source) + at java.rmi/java.rmi.server.RemoteObjectInvocationHandler.invoke(Unknown Source) + at jdk.proxy22/jdk.proxy22.$Proxy449.compile(Unknown Source) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.incrementalCompilationWithDaemon(GradleKotlinCompilerWork.kt:331) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:235) + ... 37 more +Caused by: java.net.SocketException: Connection reset + at java.base/sun.nio.ch.NioSocketImpl.implRead(Unknown Source) + at java.base/sun.nio.ch.NioSocketImpl.read(Unknown Source) + at java.base/sun.nio.ch.NioSocketImpl$1.read(Unknown Source) + at java.base/java.net.Socket$SocketInputStream.read(Unknown Source) + at java.base/java.io.BufferedInputStream.fill(Unknown Source) + at java.base/java.io.BufferedInputStream.implRead(Unknown Source) + at java.base/java.io.BufferedInputStream.read(Unknown Source) + at java.base/java.io.DataInputStream.readUnsignedByte(Unknown Source) + at java.base/java.io.DataInputStream.readByte(Unknown Source) + ... 44 more + + diff --git a/.kotlin/errors/errors-1768333789697.log b/.kotlin/errors/errors-1768333789697.log new file mode 100644 index 0000000..1219b50 --- /dev/null +++ b/.kotlin/errors/errors-1768333789697.log @@ -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 + 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 4113755..b963bf2 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -19,6 +19,7 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -35,11 +36,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import androidx.lint.kotlin.metadata.Visibility @@ -592,7 +596,7 @@ class MainActivity : ComponentActivity() { } } -/* ================= JADWAL SCREEN ================= */ +// ================= JADWAL SCREEN ================= @Composable fun JadwalScreen( @@ -601,17 +605,17 @@ fun JadwalScreen( ) { var jadwalList by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(true) } - var errorMessage by remember { mutableStateOf(null) } // Pakai state error handler + var errorMessage by remember { mutableStateOf(null) } var hariIni by remember { mutableStateOf("") } val context = LocalContext.current - val scrollState = rememberScrollState() + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) - // Fungsi load jadwal dipisah agar bisa dipanggil ulang (Retry) + // Fungsi Load Data fun loadJadwal() { isLoading = true errorMessage = null - getJadwalToday( token = token, onSuccess = { jadwal -> @@ -624,7 +628,6 @@ fun JadwalScreen( (context as? ComponentActivity)?.runOnUiThread { errorMessage = error isLoading = false - // Hapus Toast lama } } ) @@ -639,479 +642,764 @@ fun JadwalScreen( loadJadwal() } - // Jika Error dan List Kosong -> Tampilkan Full Screen Error + // Error State if (errorMessage != null && jadwalList.isEmpty()) { - FullScreenErrorState( - message = errorMessage!!, - onRetry = { loadJadwal() }, - modifier = modifier - ) - return // Stop rendering sisa UI + FullScreenErrorState(message = errorMessage!!, onRetry = { loadJadwal() }) + return } Column( modifier = modifier .fillMaxSize() - .padding(24.dp) - .verticalScroll(scrollState) + .background(androidx.compose.ui.graphics.Color(0xFFF8F9FA)) // Background abu muda + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()) ) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(30.dp)) - Text( - text = "📅 Jadwal Kelas Hari Ini", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = hariIni, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + // Header + Row(verticalAlignment = Alignment.CenterVertically) { + // Kotak Tanggal/Hari + Card( + colors = CardDefaults.cardColors(containerColor = GoldPrimary), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp) + ) { + Box( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = hariIni, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.White + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + text = "Jadwal Kuliah", + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Black + ) + Text( + text = "Semester Ganjil 2025/2026", // Bisa dibuat dinamis nanti + style = MaterialTheme.typography.bodySmall, + color = androidx.compose.ui.graphics.Color.Gray + ) + } + } Spacer(modifier = Modifier.height(24.dp)) + // Content if (isLoading) { - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() + Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = GoldPrimary) } } else if (jadwalList.isEmpty()) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + // Empty State yang lebih cantik + Column( + modifier = Modifier.fillMaxWidth().padding(top = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("📭", style = MaterialTheme.typography.displayMedium) - Spacer(modifier = Modifier.height(8.dp)) - Text("Tidak ada kelas hari ini", style = MaterialTheme.typography.titleMedium) - } + Icon( + imageVector = Icons.Default.EventBusy, + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = androidx.compose.ui.graphics.Color.LightGray + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Tidak ada kelas hari ini", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Gray + ) + Text( + text = "Silakan istirahat atau cek tugas Anda", + style = MaterialTheme.typography.bodyMedium, + color = androidx.compose.ui.graphics.Color.LightGray + ) } } else { + // List Jadwal jadwalList.forEach { jadwal -> JadwalCard(jadwal = jadwal) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(16.dp)) } + Spacer(modifier = Modifier.height(80.dp)) } } } @Composable fun JadwalCard(jadwal: JadwalKelas) { + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val GreenSuccess = androidx.compose.ui.graphics.Color(0xFF2E7D32) + Card( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = if (jadwal.sudahAbsen) - MaterialTheme.colorScheme.tertiaryContainer - else - MaterialTheme.colorScheme.secondaryContainer - ) + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { - Column(modifier = Modifier.padding(16.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = jadwal.kodeMatkul, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary - ) + Row(modifier = Modifier.height(IntrinsicSize.Min)) { + // Aksen Warna di Kiri (Strip) + Box( + modifier = Modifier + .fillMaxHeight() + .width(6.dp) + .background(if (jadwal.sudahAbsen) GreenSuccess else GoldPrimary) + ) - if (jadwal.sudahAbsen) { - Surface( - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.tertiary - ) { - Text( - text = "✓ Sudah Absen", - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onTertiary - ) + Column(modifier = Modifier.padding(16.dp).weight(1f)) { + // Kode Matkul & Status + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = jadwal.kodeMatkul, + style = MaterialTheme.typography.labelMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Gray + ) + + if (jadwal.sudahAbsen) { + Surface( + shape = androidx.compose.foundation.shape.RoundedCornerShape(6.dp), + color = GreenSuccess.copy(alpha = 0.1f) + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Check, null, modifier = Modifier.size(12.dp), tint = GreenSuccess) + Spacer(modifier = Modifier.width(4.dp)) + Text("Hadir", style = MaterialTheme.typography.labelSmall, color = GreenSuccess, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) + } + } } } - } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = jadwal.namaMatkul, - style = MaterialTheme.typography.titleMedium - ) + // Nama Matkul + Text( + text = jadwal.namaMatkul, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Black + ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "👨‍🏫 ${jadwal.dosen}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Divider() - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { + // Dosen Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Schedule, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + Icon(Icons.Default.Person, null, modifier = Modifier.size(14.dp), tint = androidx.compose.ui.graphics.Color.Gray) Spacer(modifier = Modifier.width(4.dp)) - Text( - text = "${jadwal.jamMulai.substring(0, 5)} - ${jadwal.jamSelesai.substring(0, 5)}", - style = MaterialTheme.typography.bodyMedium - ) + Text(jadwal.dosen, style = MaterialTheme.typography.bodySmall, color = androidx.compose.ui.graphics.Color.Gray) } - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Room, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = jadwal.ruangan, - style = MaterialTheme.typography.bodyMedium - ) + Spacer(modifier = Modifier.height(12.dp)) + Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.3f)) + Spacer(modifier = Modifier.height(12.dp)) + + // Waktu & Ruangan + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.AccessTime, null, modifier = Modifier.size(16.dp), tint = GoldPrimary) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "${jadwal.jamMulai.substring(0,5)} - ${jadwal.jamSelesai.substring(0,5)}", + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold), + color = androidx.compose.ui.graphics.Color.Gray + ) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.MeetingRoom, null, modifier = Modifier.size(16.dp), tint = GoldPrimary) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = jadwal.ruangan, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold), + color = androidx.compose.ui.graphics.Color.Gray + ) + } } } } } } -/* ================= REGISTER SCREEN ================= */ +// ================= REGISTER SCREEN (UI BARU) ================= +@OptIn(ExperimentalMaterial3Api::class) @Composable fun RegisterScreen( modifier: Modifier = Modifier, onRegisterSuccess: (String, Mahasiswa) -> Unit, onNavigateToLogin: () -> Unit ) { + // State Form var npm by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } var confirmPassword by remember { mutableStateOf("") } var nama by remember { mutableStateOf("") } - var jenkel by remember { mutableStateOf("L") } + var jenkel by remember { mutableStateOf("L") } // Default Laki-laki var fakultas by remember { mutableStateOf("") } var jurusan by remember { mutableStateOf("") } var semester by remember { mutableStateOf("") } + // State UI var showPassword by remember { mutableStateOf(false) } + var showConfirmPassword by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf(null) } val context = LocalContext.current - val scrollState = rememberScrollState() - Column( - modifier = modifier - .fillMaxSize() - .padding(24.dp) - .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(24.dp)) + // Warna Tema (Lokal) + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val GoldLight = androidx.compose.ui.graphics.Color(0xFFDAA520) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) - Text( - text = "📝 Registrasi Mahasiswa", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary + // Error Dialog + if (errorMessage != null) { + ErrorDialog(message = errorMessage!!, onDismiss = { errorMessage = null }) + } + + Box(modifier = modifier.fillMaxSize()) { + // 1. Header Background (Lengkungan Emas) + Box( + modifier = Modifier + .fillMaxWidth() + .height(220.dp) // Sedikit lebih pendek dari login karena konten banyak + .background( + brush = androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(GoldPrimary, GoldLight) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(bottomStart = 60.dp, bottomEnd = 60.dp) + ) ) - Spacer(modifier = Modifier.height(8.dp)) + // 2. Konten Utama + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(30.dp)) - Text( - text = "UBHARA Jaya", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(32.dp)) - - OutlinedTextField( - value = npm, - onValueChange = { npm = it }, - label = { Text("NPM") }, - placeholder = { Text("Contoh: 2023010001") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { showPassword = !showPassword }) { + // Icon Header Kecil + Surface( + shape = CircleShape, + color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.2f), + modifier = Modifier.size(60.dp) + ) { + Box(contentAlignment = Alignment.Center) { Icon( - imageVector = if (showPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = "Toggle password" + imageVector = Icons.Default.PersonAdd, + contentDescription = "Register", + tint = androidx.compose.ui.graphics.Color.White, + modifier = Modifier.size(30.dp) ) } - }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) + } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(16.dp)) - OutlinedTextField( - value = confirmPassword, - onValueChange = { confirmPassword = it }, - label = { Text("Konfirmasi Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = nama, - onValueChange = { nama = it }, - label = { Text("Nama Lengkap") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - FilterChip( - selected = jenkel == "L", - onClick = { jenkel = "L" }, - label = { Text("Laki-laki") }, - modifier = Modifier.weight(1f), - enabled = !isLoading - ) - FilterChip( - selected = jenkel == "P", - onClick = { jenkel = "P" }, - label = { Text("Perempuan") }, - modifier = Modifier.weight(1f), - enabled = !isLoading - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = fakultas, - onValueChange = { fakultas = it }, - label = { Text("Fakultas") }, - placeholder = { Text("Contoh: Teknik") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = jurusan, - onValueChange = { jurusan = it }, - label = { Text("Jurusan") }, - placeholder = { Text("Contoh: Informatika") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) - - Spacer(modifier = Modifier.height(12.dp)) - - OutlinedTextField( - value = semester, - onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) semester = it }, - label = { Text("Semester") }, - placeholder = { Text("1-14") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isLoading - ) - - if (errorMessage.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall + text = "Registrasi Mahasiswa", + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, + color = androidx.compose.ui.graphics.Color.White + ) + ) + Text( + text = "Lengkapi data diri Anda", + style = MaterialTheme.typography.bodyMedium.copy( + color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.9f) + ) ) - } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(30.dp)) - Button( - onClick = { - errorMessage = "" + // 3. Card Form Input + Card( + modifier = Modifier.fillMaxWidth(), + shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // --- DATA AKUN --- + Text("Data Akun", style = MaterialTheme.typography.labelLarge, color = GoldPrimary) + Spacer(modifier = Modifier.height(8.dp)) - when { - npm.length < 8 -> errorMessage = "NPM minimal 8 karakter" - password.length < 6 -> errorMessage = "Password minimal 6 karakter" - password != confirmPassword -> errorMessage = "Password tidak cocok" - nama.length < 3 -> errorMessage = "Nama minimal 3 karakter" - fakultas.isEmpty() -> errorMessage = "Fakultas wajib diisi" - jurusan.isEmpty() -> errorMessage = "Jurusan wajib diisi" - semester.toIntOrNull() == null || semester.toInt() !in 1..14 -> - errorMessage = "Semester harus antara 1-14" - else -> { - isLoading = true - registerMahasiswa( - npm = npm.trim(), - password = password, - nama = nama.trim(), - jenkel = jenkel, - fakultas = fakultas.trim(), - jurusan = jurusan.trim(), - semester = semester.toInt(), - onSuccess = { token, mhs -> - (context as? ComponentActivity)?.runOnUiThread { - isLoading = false - Toast.makeText(context, "✅ Registrasi berhasil!", Toast.LENGTH_SHORT).show() - onRegisterSuccess(token, mhs) - } - }, - onError = { error -> - (context as? ComponentActivity)?.runOnUiThread { - isLoading = false - errorMessage = error - Toast.makeText(context, "❌ $error", Toast.LENGTH_LONG).show() - } + // NPM + OutlinedTextField( + value = npm, onValueChange = { npm = it }, + label = { Text("NPM") }, + leadingIcon = { Icon(Icons.Default.Badge, null, tint = GoldPrimary) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) + ) + Spacer(modifier = Modifier.height(8.dp)) + + // Password + OutlinedTextField( + value = password, onValueChange = { password = it }, + label = { Text("Password") }, + leadingIcon = { Icon(Icons.Default.Lock, null, tint = GoldPrimary) }, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon(if (showPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, null) } + }, + modifier = Modifier.fillMaxWidth(), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) + ) + Spacer(modifier = Modifier.height(8.dp)) + + // Confirm Password + OutlinedTextField( + value = confirmPassword, onValueChange = { confirmPassword = it }, + label = { Text("Konfirmasi Password") }, + leadingIcon = { Icon(Icons.Default.LockReset, null, tint = GoldPrimary) }, + visualTransformation = if (showConfirmPassword) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showConfirmPassword = !showConfirmPassword }) { + Icon(if (showConfirmPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, null) + } + }, + modifier = Modifier.fillMaxWidth(), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) + ) + + Spacer(modifier = Modifier.height(16.dp)) + Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.5f)) + Spacer(modifier = Modifier.height(16.dp)) + + // --- DATA PRIBADI --- + Text("Data Pribadi", style = MaterialTheme.typography.labelLarge, color = GoldPrimary) + Spacer(modifier = Modifier.height(8.dp)) + + // Nama + OutlinedTextField( + value = nama, onValueChange = { nama = it }, + label = { Text("Nama Lengkap") }, + leadingIcon = { Icon(Icons.Default.Person, null, tint = GoldPrimary) }, + modifier = Modifier.fillMaxWidth(), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) + ) + Spacer(modifier = Modifier.height(12.dp)) + + // Gender Selector (Custom Buttons) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + val isL = jenkel == "L" + OutlinedButton( + onClick = { jenkel = "L" }, + modifier = Modifier.weight(1f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = if (isL) GoldPrimary else androidx.compose.ui.graphics.Color.Transparent, + contentColor = if (isL) androidx.compose.ui.graphics.Color.White else androidx.compose.ui.graphics.Color.Gray + ), + border = androidx.compose.foundation.BorderStroke(1.dp, if (isL) GoldPrimary else androidx.compose.ui.graphics.Color.Gray) + ) { Text("Laki-laki") } + + val isP = jenkel == "P" + OutlinedButton( + onClick = { jenkel = "P" }, + modifier = Modifier.weight(1f), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = if (isP) GoldPrimary else androidx.compose.ui.graphics.Color.Transparent, + contentColor = if (isP) androidx.compose.ui.graphics.Color.White else androidx.compose.ui.graphics.Color.Gray + ), + border = androidx.compose.foundation.BorderStroke(1.dp, if (isP) GoldPrimary else androidx.compose.ui.graphics.Color.Gray) + ) { Text("Perempuan") } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Fakultas + OutlinedTextField( + value = fakultas, onValueChange = { fakultas = it }, + label = { Text("Fakultas") }, + leadingIcon = { Icon(Icons.Default.School, null, tint = GoldPrimary) }, + modifier = Modifier.fillMaxWidth(), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) + ) + Spacer(modifier = Modifier.height(8.dp)) + + // Jurusan & Semester (Row) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField( + value = jurusan, onValueChange = { jurusan = it }, + label = { Text("Jurusan") }, + leadingIcon = { Icon(Icons.Default.Book, null, tint = GoldPrimary) }, + modifier = Modifier.weight(1.5f), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) + ) + OutlinedTextField( + value = semester, + onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) semester = it }, + label = { Text("Sms") }, + placeholder = { Text("1-8") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.weight(1f), singleLine = true, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors(focusedBorderColor = GoldPrimary, cursorColor = GoldPrimary) ) } + + Spacer(modifier = Modifier.height(32.dp)) + + // Tombol Daftar (Gradient) + Button( + onClick = { + errorMessage = null + if (npm.length < 8 || password.length < 6 || nama.isEmpty()) { + errorMessage = "Mohon lengkapi data dengan benar (Password min 6 karakter)" + } else if (password != confirmPassword) { + errorMessage = "Konfirmasi password tidak cocok" + } else { + isLoading = true + registerMahasiswa( + npm = npm.trim(), password = password, nama = nama.trim(), + jenkel = jenkel, fakultas = fakultas.trim(), jurusan = jurusan.trim(), + semester = semester.toIntOrNull() ?: 1, + onSuccess = { token, mhs -> + (context as? ComponentActivity)?.runOnUiThread { + isLoading = false + onRegisterSuccess(token, mhs) + } + }, + onError = { error -> + (context as? ComponentActivity)?.runOnUiThread { + isLoading = false + errorMessage = error + } + } + ) + } + }, + modifier = Modifier.fillMaxWidth().height(54.dp), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = androidx.compose.ui.graphics.Color.Transparent), + contentPadding = PaddingValues() + ) { + Box( + modifier = Modifier.fillMaxSize().background( + brush = androidx.compose.ui.graphics.Brush.horizontalGradient( + colors = listOf(GoldPrimary, MaroonSecondary) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp) + ), + contentAlignment = Alignment.Center + ) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), color = androidx.compose.ui.graphics.Color.White) + } else { + Text("DAFTAR SEKARANG", style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold)) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Navigasi Login + Row(verticalAlignment = Alignment.CenterVertically) { + Text("Sudah punya akun?", style = MaterialTheme.typography.bodyMedium, color = androidx.compose.ui.graphics.Color.Gray) + TextButton(onClick = onNavigateToLogin, enabled = !isLoading) { + Text("Masuk", style = MaterialTheme.typography.bodyMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), color = MaroonSecondary) + } + } } - }, - modifier = Modifier.fillMaxWidth().height(50.dp), - enabled = !isLoading - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Daftar", style = MaterialTheme.typography.titleMedium) } + Spacer(modifier = Modifier.height(40.dp)) } - - Spacer(modifier = Modifier.height(16.dp)) - - TextButton(onClick = onNavigateToLogin, enabled = !isLoading) { - Text("Sudah punya akun? Masuk di sini") - } - - Spacer(modifier = Modifier.height(24.dp)) } } -/* ================= LOGIN SCREEN ================= */ +// ================= LOGIN SCREEN ================= +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen( modifier: Modifier = Modifier, onLoginSuccess: (String, Mahasiswa) -> Unit, - onNavigateToRegister: () -> Unit + onNavigateToRegister: () -> Unit // Fitur Register TETAP ADA ) { var npm by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } var showPassword by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf(null) } // Gunakan nullable string + var errorMessage by remember { mutableStateOf(null) } val context = LocalContext.current - // Error Dialog (Opsional, atau gunakan Text merah dibawah tombol) + // Definisi Warna Lokal (Agar langsung jalan tanpa ubah Theme.kt dulu) + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val GoldLight = androidx.compose.ui.graphics.Color(0xFFDAA520) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) + val MaroonLight = androidx.compose.ui.graphics.Color(0xFFA52A2A) + + // Handler Error Dialog if (errorMessage != null) { ErrorDialog(message = errorMessage!!, onDismiss = { errorMessage = null }) } - Column( - modifier = modifier.fillMaxSize().padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text("🎓 Absensi Akademik", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary) - Spacer(modifier = Modifier.height(8.dp)) - Text("UBHARA Jaya - Soreang", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - Spacer(modifier = Modifier.height(48.dp)) - - OutlinedTextField( - value = npm, onValueChange = { npm = it }, label = { Text("NPM") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), singleLine = true, enabled = !isLoading - ) - Spacer(modifier = Modifier.height(16.dp)) - OutlinedTextField( - value = password, onValueChange = { password = it }, label = { Text("Password") }, - visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { IconButton(onClick = { showPassword = !showPassword }) { Icon(if (showPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, "Toggle") } }, - modifier = Modifier.fillMaxWidth(), singleLine = true, enabled = !isLoading + Box(modifier = modifier.fillMaxSize()) { + // 1. Background Header (Lengkungan Gradasi Emas) + Box( + modifier = Modifier + .fillMaxWidth() + .height(260.dp) + .background( + brush = androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(GoldPrimary, GoldLight) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(bottomStart = 60.dp, bottomEnd = 60.dp) + ) ) - Spacer(modifier = Modifier.height(32.dp)) + // 2. Konten Utama + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), // Agar bisa discroll di layar kecil + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(40.dp)) - Button( - onClick = { - errorMessage = null - if (npm.isEmpty() || password.isEmpty()) { errorMessage = "NPM dan Password wajib diisi"; return@Button } + Surface( + shape = CircleShape, + color = androidx.compose.ui.graphics.Color.White, + shadowElevation = 8.dp, + modifier = Modifier.size(100.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Image( + // Pastikan ID ini sesuai nama file Anda + painter = painterResource(id = R.drawable.logo_ubhara), + contentDescription = "Logo UBHARA", + modifier = Modifier + .fillMaxSize(), // Mengikuti ukuran wadah (dikurangi padding) + contentScale = ContentScale.Fit // Agar logo tidak terpotong/gepeng + ) + } + } - isLoading = true - loginMahasiswa( - npm = npm.trim(), password = password, - onSuccess = { token, mhs -> - (context as? ComponentActivity)?.runOnUiThread { - isLoading = false - onLoginSuccess(token, mhs) - } - }, - onError = { error -> - (context as? ComponentActivity)?.runOnUiThread { - isLoading = false - errorMessage = error // Trigger Dialog + Spacer(modifier = Modifier.height(24.dp)) + + // Judul Aplikasi + Text( + text = "Sistem Akademik", + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, + color = androidx.compose.ui.graphics.Color.White + ) + ) + Text( + text = "UBHARA Jaya", + style = MaterialTheme.typography.titleMedium.copy( + color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.9f) + ) + ) + + Spacer(modifier = Modifier.height(40.dp)) + + // 3. Card Form Input + Card( + modifier = Modifier.fillMaxWidth(), + shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Login Mahasiswa", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ), + color = GoldPrimary + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Input NPM + OutlinedTextField( + value = npm, + onValueChange = { npm = it }, + label = { Text("NPM") }, + placeholder = { Text("Masukkan NPM") }, + leadingIcon = { + Icon(Icons.Default.Badge, contentDescription = null, tint = GoldPrimary) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !isLoading, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = GoldPrimary, + focusedLabelColor = GoldPrimary, + cursorColor = GoldPrimary, + focusedTextColor = androidx.compose.ui.graphics.Color.Black, + unfocusedTextColor = androidx.compose.ui.graphics.Color.Black + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Input Password + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + leadingIcon = { + Icon(Icons.Default.Lock, contentDescription = null, tint = GoldPrimary) + }, + visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { showPassword = !showPassword }) { + Icon( + imageVector = if (showPassword) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "Toggle", + tint = androidx.compose.ui.graphics.Color.Gray + ) + } + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !isLoading, + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = GoldPrimary, + focusedLabelColor = GoldPrimary, + cursorColor = GoldPrimary, + focusedTextColor = androidx.compose.ui.graphics.Color.Black, + unfocusedTextColor = androidx.compose.ui.graphics.Color.Black + ) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + // Tombol Login (Gradient Style) + Button( + onClick = { + errorMessage = null + if (npm.isEmpty() || password.isEmpty()) { + errorMessage = "NPM dan Password wajib diisi" + return@Button + } + + isLoading = true + loginMahasiswa( + npm = npm.trim(), + password = password, + onSuccess = { token, mhs -> + (context as? ComponentActivity)?.runOnUiThread { + isLoading = false + onLoginSuccess(token, mhs) + } + }, + onError = { error -> + (context as? ComponentActivity)?.runOnUiThread { + isLoading = false + errorMessage = error + } + } + ) + }, + modifier = Modifier + .fillMaxWidth() + .height(54.dp), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = androidx.compose.ui.graphics.Color.Transparent), + contentPadding = PaddingValues() // Hilangkan padding default agar gradient full + ) { + // Background Gradient untuk Tombol + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = androidx.compose.ui.graphics.Brush.horizontalGradient( + colors = listOf(GoldPrimary, MaroonSecondary) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp) + ), + contentAlignment = Alignment.Center + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = androidx.compose.ui.graphics.Color.White + ) + } else { + Text( + text = "MASUK", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, + letterSpacing = 1.sp + ), + color = androidx.compose.ui.graphics.Color.White + ) + } } } - ) - }, - modifier = Modifier.fillMaxWidth().height(50.dp), enabled = !isLoading - ) { - if (isLoading) CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary) - else Text("Masuk", style = MaterialTheme.typography.titleMedium) - } - Spacer(modifier = Modifier.height(16.dp)) - TextButton(onClick = onNavigateToRegister, enabled = !isLoading) { Text("Belum punya akun? Daftar di sini") } + Spacer(modifier = Modifier.height(16.dp)) + + // Tombol Navigasi ke Register (TETAP ADA) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Belum punya akun?", + style = MaterialTheme.typography.bodyMedium, + color = androidx.compose.ui.graphics.Color.Gray + ) + TextButton(onClick = onNavigateToRegister, enabled = !isLoading) { + Text( + text = "Daftar di sini", + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ), + color = MaroonSecondary + ) + } + } + } + } + + Spacer(modifier = Modifier.height(30.dp)) + } } } -// ========== SCREEN BARU: RIWAYAT ABSENSI ========== +// ================= RIWAYAT SCREEN ================= @Composable fun RiwayatScreen( @@ -1120,454 +1408,331 @@ fun RiwayatScreen( token: String ) { val context = LocalContext.current + val scrollState = rememberScrollState() - // State Data + // Warna Tema + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) + + // State (Tetap Sama) var riwayatList by remember { mutableStateOf>(emptyList()) } var stats by remember { mutableStateOf(null) } - - // State UI & Error Handling var isLoading by remember { mutableStateOf(true) } var errorMessage by remember { mutableStateOf(null) } - var isSessionExpired by remember { mutableStateOf(false) } // Untuk popup logout otomatis - - // State Dialog & Filter var showFilterDialog by remember { mutableStateOf(false) } var showFotoDialog by remember { mutableStateOf(false) } var selectedFoto by remember { mutableStateOf(null) } var isLoadingFoto by remember { mutableStateOf(false) } + var startDate by remember { mutableStateOf(null) } var endDate by remember { mutableStateOf(null) } var filterActive by remember { mutableStateOf(false) } - val scrollState = rememberScrollState() - - // Fungsi Load Data dengan Error Handling Terpusat fun loadData() { isLoading = true - errorMessage = null // Reset error sebelum load ulang - + errorMessage = null getAbsensiHistory( - token = token, - startDate = startDate, - endDate = endDate, - onSuccess = { riwayat -> - activity.runOnUiThread { - riwayatList = riwayat - isLoading = false - } - }, - onError = { errorRaw -> - activity.runOnUiThread { - isLoading = false - // Cek Kode Error 401 (Unauthorized) untuk Logout Otomatis - if (errorRaw.contains("[401]") || errorRaw.contains("Sesi telah berakhir")) { - isSessionExpired = true - } else { - // Hapus kode status "[xxx]" agar pesan lebih bersih saat ditampilkan - errorMessage = errorRaw.replace(Regex("\\[\\d+\\] "), "") - } - } - } + token = token, startDate = startDate, endDate = endDate, + onSuccess = { riwayat -> activity.runOnUiThread { riwayatList = riwayat; isLoading = false } }, + onError = { error -> activity.runOnUiThread { errorMessage = error; isLoading = false } } ) - getAbsensiStats( token = token, - onSuccess = { statsData -> - activity.runOnUiThread { stats = statsData } - }, - onError = { /* Error stats di-ignore saja agar tidak mengganggu UI utama */ } + onSuccess = { statsData -> activity.runOnUiThread { stats = statsData } }, + onError = {} ) } - // Initial load - LaunchedEffect(Unit) { - loadData() - } + LaunchedEffect(Unit) { loadData() } - // === HANDLER DIALOG ERROR & LOGOUT === - - // 1. Dialog Sesi Berakhir (Auto Logout) - if (isSessionExpired) { - SessionExpiredDialog( - onConfirm = { - val userPrefs = UserPreferences(context) - userPrefs.logout() - val intent = Intent(context, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - context.startActivity(intent) - activity.finish() - } - ) - } - - // 2. Dialog Error Biasa (Muncul jika data sudah ada tapi terjadi error saat refresh/filter) - if (errorMessage != null && riwayatList.isNotEmpty() && !isSessionExpired) { - ErrorDialog( - message = errorMessage!!, - onDismiss = { errorMessage = null } - ) - } - - // 3. Dialog Foto + // --- DIALOGS (Foto & Filter) --- + // (Kode dialog filter & foto SAMA PERSIS dengan sebelumnya, tidak perlu diubah logic-nya) if (showFotoDialog && selectedFoto != null) { - Dialog(onDismissRequest = { showFotoDialog = false }) { - Card(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.large) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("📸 Foto Absensi", style = MaterialTheme.typography.titleLarge) - Spacer(modifier = Modifier.height(16.dp)) - Image( - bitmap = selectedFoto!!.asImageBitmap(), - contentDescription = "Foto Absensi", - modifier = Modifier.fillMaxWidth().heightIn(max = 500.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { showFotoDialog = false }, modifier = Modifier.fillMaxWidth()) { - Text("Tutup") - } - } - } - } - } - - // 4. Dialog Filter Date - if (showFilterDialog) { - FilterDateDialog( - startDate = startDate, - endDate = endDate, - onDismiss = { showFilterDialog = false }, - onApply = { start, end -> - startDate = start - endDate = end - filterActive = (start != null || end != null) - showFilterDialog = false - loadData() - }, - onReset = { - startDate = null - endDate = null - filterActive = false - showFilterDialog = false - loadData() - } - ) - } - - // === KONTEN UTAMA === - Box(modifier = modifier.fillMaxSize()) { - // KONDISI 1: Loading Awal (Spinner ditengah) - if (isLoading && riwayatList.isEmpty()) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - // KONDISI 2: Error Full Screen (Jika data kosong dan gagal load) - else if (errorMessage != null && riwayatList.isEmpty()) { - FullScreenErrorState( - message = errorMessage!!, - onRetry = { loadData() } + Dialog( + onDismissRequest = { showFotoDialog = false }, + // Properti ini membuat Dialog bisa di-custom ukurannya (bisa full width) + properties = androidx.compose.ui.window.DialogProperties( + usePlatformDefaultWidth = false ) - } - // KONDISI 3: Data Kosong (Sukses tapi tidak ada riwayat) - else if (riwayatList.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize().padding(24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Tombol refresh tetap ada di state kosong - IconButton(onClick = { loadData() }) { - Icon(Icons.Default.Refresh, contentDescription = "Refresh") - } - Text("📭", style = MaterialTheme.typography.displayMedium) - Text("Belum ada riwayat absensi", style = MaterialTheme.typography.titleMedium) - } - } - // KONDISI 4: Tampilkan List Data - else { - Column( + ) { + // Background Gelap Transparan (Scrim) + Box( modifier = Modifier .fillMaxSize() - .verticalScroll(scrollState) - .padding(24.dp) + .background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.85f)) + .clickable { showFotoDialog = false }, // Klik area gelap untuk tutup + contentAlignment = Alignment.Center ) { - Spacer(modifier = Modifier.height(16.dp)) - - // Header & Tombol Action - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + // Kartu Foto + Card( + modifier = Modifier + .fillMaxWidth(0.9f) // Lebar 90% layar + .fillMaxHeight(0.75f) // Tinggi 75% layar (Agar "sedikit fullscreen") + .clickable(enabled = false) {}, // Agar klik di kartu tidak menutup dialog + shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + // Border Emas sesuai tema + border = androidx.compose.foundation.BorderStroke(2.dp, GoldPrimary) ) { - Text( - text = "📋 Riwayat Absensi", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary - ) - Row { - IconButton(onClick = { loadData() }) { - Icon(Icons.Default.Refresh, "Refresh") - } - IconButton(onClick = { showFilterDialog = true }) { - Icon( - imageVector = if (filterActive) Icons.Default.FilterAlt else Icons.Default.FilterList, - contentDescription = "Filter", - tint = if (filterActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface - ) - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Statistik - if (stats != null) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - StatsCard("Total", stats!!.totalAbsensi.toString(), Icons.Default.CheckCircle, Modifier.weight(1f)) - StatsCard("Minggu Ini", stats!!.absensiMingguIni.toString(), Icons.Default.DateRange, Modifier.weight(1f)) - StatsCard("Bulan Ini", stats!!.absensiBulanIni.toString(), Icons.Default.CalendarToday, Modifier.weight(1f)) - } - Spacer(modifier = Modifier.height(24.dp)) - } - - // Info Filter Aktif - if (filterActive) { - Card( - modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + Column( + modifier = Modifier.fillMaxSize() ) { - Row(modifier = Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.FilterAlt, null, modifier = Modifier.size(16.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text("Filter: ${startDate ?: "?"} s/d ${endDate ?: "?"}", style = MaterialTheme.typography.bodySmall) - } - } - } - - // List Card Riwayat - riwayatList.forEach { riwayat -> - RiwayatCard( - riwayat = riwayat, - onLihatFoto = { idAbsensi -> - isLoadingFoto = true - getFotoAbsensi( - token = token, - idAbsensi = idAbsensi, - onSuccess = { fotoBase64 -> - val bitmap = base64ToBitmap(fotoBase64) - activity.runOnUiThread { - isLoadingFoto = false - if (bitmap != null) { - selectedFoto = bitmap - showFotoDialog = true - } else { - Toast.makeText(context, "❌ Gagal load foto", Toast.LENGTH_SHORT).show() - } - } - }, - onError = { error -> - activity.runOnUiThread { - isLoadingFoto = false - // Gunakan parsing error sederhana untuk Toast - val simpleMsg = if(error.contains("Exception")) "Gagal koneksi" else error - Toast.makeText(context, "❌ $simpleMsg", Toast.LENGTH_SHORT).show() - } - } + // 1. Header Kartu + Box( + modifier = Modifier + .fillMaxWidth() + .background(GoldPrimary.copy(alpha = 0.1f)) + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "Bukti Absensi", + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ), + color = GoldPrimary ) } - ) - Spacer(modifier = Modifier.height(12.dp)) + + // 2. Area Foto (Mengisi sisa ruang) + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(androidx.compose.ui.graphics.Color.Black), // Background foto hitam + contentAlignment = Alignment.Center + ) { + Image( + bitmap = selectedFoto!!.asImageBitmap(), + contentDescription = "Foto Absensi Full", + modifier = Modifier.fillMaxSize(), + // Fit agar seluruh foto terlihat (tidak terpotong), + // ganti ke .Crop jika ingin foto memenuhi kotak tapi terpotong + contentScale = androidx.compose.ui.layout.ContentScale.Fit + ) + } + + // 3. Footer (Tombol Tutup Maroon) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Button( + onClick = { showFotoDialog = false }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaroonSecondary, // Warna Merah Kampus + contentColor = androidx.compose.ui.graphics.Color.White + ) + ) { + Text( + "Tutup", + style = MaterialTheme.typography.titleMedium.copy( + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ) + ) + } + } + } } } } } - // Loading Overlay untuk Foto - if (isLoadingFoto) { - Box( - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface.copy(alpha = 0.7f)), - contentAlignment = Alignment.Center + + // --- UI CONTENT --- + Column( + modifier = modifier + .fillMaxSize() + .background(androidx.compose.ui.graphics.Color(0xFFF8F9FA)) + .padding(horizontal = 24.dp) + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(30.dp)) + + // Header & Filter + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - CircularProgressIndicator() + Column { + Text( + text = "Riwayat Absensi", + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Black + ) + if (filterActive) { + Text( + text = "${startDate} - ${endDate}", + style = MaterialTheme.typography.labelSmall, + color = GoldPrimary + ) + } + } + + // Tombol Filter Bulat + IconButton( + onClick = { showFilterDialog = true }, + modifier = Modifier + .background(if (filterActive) GoldPrimary else androidx.compose.ui.graphics.Color.White, CircleShape) + .border(1.dp, androidx.compose.ui.graphics.Color.LightGray, CircleShape) + ) { + Icon( + imageVector = Icons.Default.FilterList, + contentDescription = "Filter", + tint = if (filterActive) androidx.compose.ui.graphics.Color.White else androidx.compose.ui.graphics.Color.Gray + ) + } } + + Spacer(modifier = Modifier.height(24.dp)) + + // Stats Row (Dashboard Style) + if (stats != null) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { + StatsCard("Total", stats!!.totalAbsensi.toString(), Icons.Default.CheckCircle, GoldPrimary, Modifier.weight(1f)) + StatsCard("Minggu", stats!!.absensiMingguIni.toString(), Icons.Default.DateRange, androidx.compose.ui.graphics.Color(0xFF1976D2), Modifier.weight(1f)) // Biru + StatsCard("Bulan", stats!!.absensiBulanIni.toString(), Icons.Default.CalendarToday, MaroonSecondary, Modifier.weight(1f)) + } + Spacer(modifier = Modifier.height(24.dp)) + } + + // List Content + if (isLoading) { + Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator(color = GoldPrimary) } + } else if (riwayatList.isEmpty()) { + Column(modifier = Modifier.fillMaxWidth().padding(top = 40.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Text("📭", style = MaterialTheme.typography.displayMedium) + Text("Belum ada data", style = MaterialTheme.typography.titleMedium, color = androidx.compose.ui.graphics.Color.Gray) + } + } else { + riwayatList.forEach { riwayat -> + RiwayatCard( + riwayat = riwayat, + onLihatFoto = { id -> + isLoadingFoto = true + getFotoAbsensi(token, id, + { b64 -> activity.runOnUiThread { isLoadingFoto = false; selectedFoto = base64ToBitmap(b64); showFotoDialog = true } }, + { err -> activity.runOnUiThread { isLoadingFoto = false; Toast.makeText(context, err, Toast.LENGTH_SHORT).show() } } + ) + } + ) + Spacer(modifier = Modifier.height(12.dp)) + } + Spacer(modifier = Modifier.height(80.dp)) + } + } + + if (isLoadingFoto) { + Box(modifier = Modifier.fillMaxSize().background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.5f)), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } } - -// ========== COMPONENTS ========== - +// Komponen Card Statistik Baru @Composable fun StatsCard( - title: String, - value: String, - icon: androidx.compose.ui.graphics.vector.ImageVector, - modifier: Modifier = Modifier + title: String, value: String, icon: androidx.compose.ui.graphics.vector.ImageVector, + color: androidx.compose.ui.graphics.Color, modifier: Modifier ) { Card( modifier = modifier, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column( modifier = Modifier.padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = value, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = title, - style = MaterialTheme.typography.bodySmall - ) + Icon(imageVector = icon, contentDescription = null, tint = color, modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = value, style = MaterialTheme.typography.titleLarge.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), color = androidx.compose.ui.graphics.Color.Black) + Text(text = title, style = MaterialTheme.typography.labelSmall, color = androidx.compose.ui.graphics.Color.Gray) } } } +// Komponen Card Riwayat Baru @Composable fun RiwayatCard( riwayat: RiwayatAbsensi, onLihatFoto: (Int) -> Unit ) { + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + Card( modifier = Modifier.fillMaxWidth(), shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors( - // Warna background kartu transparan gelap (sesuai tema) - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ) + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column(modifier = Modifier.padding(16.dp)) { - // === HEADER: Tanggal (Kiri) & Status (Kanan) === + // Header Card: Tanggal & Jam Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + horizontalArrangement = Arrangement.SpaceBetween ) { - Text( - text = formatTanggalCard(riwayat.timestamp), - style = MaterialTheme.typography.titleSmall.copy( - color = MaterialTheme.colorScheme.primary + Column { + Text( + text = formatTanggalCard(riwayat.timestamp), + style = MaterialTheme.typography.labelMedium, + color = androidx.compose.ui.graphics.Color.Gray ) - ) + Text( + text = riwayat.mataKuliah ?: "-", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Black + ) + } + // Badge Hadir Surface( shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), - color = if (riwayat.status.uppercase() == "HADIR") - androidx.compose.ui.graphics.Color(0xFF6552A8) // Ungu gelap - else - MaterialTheme.colorScheme.errorContainer + color = if (riwayat.status == "HADIR") androidx.compose.ui.graphics.Color(0xFFE8F5E9) else androidx.compose.ui.graphics.Color(0xFFFFEBEE) ) { Text( - text = riwayat.status.uppercase(), - modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold - ), - color = androidx.compose.ui.graphics.Color.White + text = riwayat.status, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp), + style = MaterialTheme.typography.labelSmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = if (riwayat.status == "HADIR") androidx.compose.ui.graphics.Color(0xFF2E7D32) else androidx.compose.ui.graphics.Color(0xFFC62828) ) } } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(12.dp)) + Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.2f)) + Spacer(modifier = Modifier.height(12.dp)) - // === BODY: Mata Kuliah === - if (!riwayat.mataKuliah.isNullOrEmpty()) { + // Detail: Waktu & Lokasi + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Book, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = androidx.compose.ui.graphics.Color(0xFF4FC3F7) // Cyan - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = riwayat.mataKuliah, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold - ), - color = MaterialTheme.colorScheme.onSurface - ) - } - Spacer(modifier = Modifier.height(8.dp)) - } + Icon(Icons.Default.AccessTime, null, modifier = Modifier.size(14.dp), tint = GoldPrimary) + Spacer(modifier = Modifier.width(4.dp)) - // === BODY: Waktu (LOGIKA BARU) === - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Icons.Default.Schedule, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(12.dp)) - - // Logika: Jika ada jamMulai/Selesai, pakai itu. Jika tidak, pakai timestamp. - val waktuText = if (riwayat.jamMulai != null && riwayat.jamSelesai != null) { - val mulai = if (riwayat.jamMulai.length >= 5) riwayat.jamMulai.substring(0, 5) else riwayat.jamMulai - val selesai = if (riwayat.jamSelesai.length >= 5) riwayat.jamSelesai.substring(0, 5) else riwayat.jamSelesai - "Pukul $mulai - $selesai" - } else { - "Absen: ${formatJam(riwayat.timestamp)}" + // Logic jam (Copy dari kode sebelumnya) + val waktuText = if (riwayat.jamMulai != null && riwayat.jamSelesai != null) { + "${riwayat.jamMulai.take(5)} - ${riwayat.jamSelesai.take(5)}" + } else { + formatJam(riwayat.timestamp) + } + Text(waktuText, style = MaterialTheme.typography.bodySmall, color = androidx.compose.ui.graphics.Color.Gray) } - Text( - text = waktuText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - // === BODY: Lokasi (INI YANG HILANG SEBELUMNYA) === - Row(verticalAlignment = Alignment.Top) { - Icon( - imageVector = Icons.Default.LocationOn, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "${String.format("%.6f", riwayat.latitude)}, ${String.format("%.6f", riwayat.longitude)}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // === FOOTER: Tombol Lihat Foto === - Button( - onClick = { onLihatFoto(riwayat.idAbsensi) }, - modifier = Modifier - .fillMaxWidth() - .height(45.dp), - shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors( - containerColor = androidx.compose.ui.graphics.Color(0xFF006064), // Teal gelap - contentColor = androidx.compose.ui.graphics.Color.White - ) - ) { - Icon( - imageVector = Icons.Default.Image, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Lihat Foto", style = MaterialTheme.typography.labelLarge) + // Tombol Lihat Foto Kecil + TextButton( + onClick = { onLihatFoto(riwayat.idAbsensi) }, + contentPadding = PaddingValues(0.dp), + modifier = Modifier.height(24.dp) + ) { + Text("Lihat Foto", style = MaterialTheme.typography.labelSmall, color = GoldPrimary) + Spacer(modifier = Modifier.width(4.dp)) + Icon(Icons.Default.ArrowForwardIos, null, modifier = Modifier.size(10.dp), tint = GoldPrimary) + } } } } @@ -1663,7 +1828,7 @@ fun formatJam(timestamp: String): String { return outputFormat.format(date) } -/* ================= MAIN SCREEN (Bottom Navigation) ================= */ +// ================= MAIN SCREEN ================= @Composable fun MainScreen( @@ -1675,62 +1840,70 @@ fun MainScreen( ) { var selectedTab by remember { mutableStateOf(0) } + // Warna Tema + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) + Scaffold( bottomBar = { - NavigationBar { - NavigationBarItem( - icon = { Icon(Icons.Default.Home, "Absensi") }, - label = { Text("Absensi") }, - selected = selectedTab == 0, - onClick = { selectedTab = 0 } - ) - NavigationBarItem( - icon = { Icon(Icons.Default.Schedule, "Kelas") }, - label = { Text("Kelas") }, - selected = selectedTab == 1, - onClick = { selectedTab = 1 } - ) - NavigationBarItem( - icon = { Icon(Icons.Default.History, "Riwayat") }, - label = { Text("Riwayat") }, - selected = selectedTab == 2, - onClick = { selectedTab = 2 } - ) - NavigationBarItem( - icon = { Icon(Icons.Default.Person, "Profil") }, - label = { Text("Profil") }, - selected = selectedTab == 3, - onClick = { selectedTab = 3 } - ) + // Card elevation untuk memberi efek bayangan halus di atas nav bar + Surface( + shadowElevation = 16.dp, + color = androidx.compose.ui.graphics.Color.White, + shape = androidx.compose.foundation.shape.RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp) + ) { + NavigationBar( + containerColor = androidx.compose.ui.graphics.Color.White, + tonalElevation = 0.dp + ) { + val items = listOf( + Triple(0, "Absensi", Icons.Default.Home), + Triple(1, "Kelas", Icons.Default.School), // Ganti icon Schedule jadi School biar beda + Triple(2, "Riwayat", Icons.Default.History), + Triple(3, "Profil", Icons.Default.Person) + ) + + items.forEach { (index, label, icon) -> + NavigationBarItem( + icon = { Icon(icon, contentDescription = label) }, + label = { Text(label, style = MaterialTheme.typography.labelSmall) }, + selected = selectedTab == index, + onClick = { selectedTab = index }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = GoldPrimary, + selectedTextColor = GoldPrimary, + indicatorColor = GoldPrimary.copy(alpha = 0.15f), // Lingkaran highlight halus + unselectedIconColor = androidx.compose.ui.graphics.Color.Gray, + unselectedTextColor = androidx.compose.ui.graphics.Color.Gray + ) + ) + } + } } } ) { padding -> - when (selectedTab) { - 0 -> AbsensiScreenWithJadwal( - modifier = Modifier.padding(padding), - activity = activity, - token = token, - mahasiswa = mahasiswa - ) - 1 -> JadwalScreen( - modifier = Modifier.padding(padding), - token = token - ) - 2 -> RiwayatScreen( - modifier = Modifier.padding(padding), - activity = activity, - token = token - ) - 3 -> ProfilScreen( - modifier = Modifier.padding(padding), - mahasiswa = mahasiswa, - onLogout = onLogout - ) + // Background abu-abu sangat muda untuk seluruh layar agar konten putih menonjol + Box(modifier = Modifier + .fillMaxSize() + .background(androidx.compose.ui.graphics.Color(0xFFF8F9FA)) + .padding(padding) + ) { + when (selectedTab) { + 0 -> AbsensiScreenWithJadwal( + modifier = Modifier, + activity = activity, + token = token, + mahasiswa = mahasiswa + ) + 1 -> JadwalScreen(modifier = Modifier, token = token) + 2 -> RiwayatScreen(modifier = Modifier, activity = activity, token = token) + 3 -> ProfilScreen(modifier = Modifier, mahasiswa = mahasiswa, onLogout = onLogout) + } } } } -/* ================= PROFIL SCREEN ================= */ +// ================= PROFIL SCREEN ================= @Composable fun ProfilScreen( @@ -1739,103 +1912,211 @@ fun ProfilScreen( onLogout: () -> Unit ) { var showLogoutDialog by remember { mutableStateOf(false) } + val scrollState = rememberScrollState() + + // Warna Tema + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) Column( modifier = modifier .fillMaxSize() - .padding(24.dp), + .background(androidx.compose.ui.graphics.Color(0xFFF8F9FA)) + .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(24.dp)) - - Text( - text = "👤 Profil Mahasiswa", - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(32.dp)) - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) + // 1. Header Profile (Background & Avatar) + Box( + modifier = Modifier + .fillMaxWidth() + .height(280.dp) ) { - Column(modifier = Modifier.padding(20.dp)) { - ProfilItem("NPM", mahasiswa.npm) - Divider(modifier = Modifier.padding(vertical = 12.dp)) - ProfilItem("Nama", mahasiswa.nama) - Divider(modifier = Modifier.padding(vertical = 12.dp)) - ProfilItem("Jenis Kelamin", if (mahasiswa.jenkel == "L") "Laki-laki" else "Perempuan") - Divider(modifier = Modifier.padding(vertical = 12.dp)) - ProfilItem("Fakultas", mahasiswa.fakultas) - Divider(modifier = Modifier.padding(vertical = 12.dp)) - ProfilItem("Jurusan", mahasiswa.jurusan) - Divider(modifier = Modifier.padding(vertical = 12.dp)) - ProfilItem("Semester", mahasiswa.semester.toString()) + // Background Lengkung + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + .background( + brush = androidx.compose.ui.graphics.Brush.verticalGradient( + colors = listOf(GoldPrimary, androidx.compose.ui.graphics.Color(0xFFDAA520)) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(bottomStart = 50.dp, bottomEnd = 50.dp) + ) + ) + + // Avatar & Nama (Floating di tengah) + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(top = 60.dp), // Turunkan sedikit agar avatar setengah di background + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Avatar Besar + Surface( + shape = CircleShape, + color = androidx.compose.ui.graphics.Color.White, + shadowElevation = 8.dp, + border = androidx.compose.foundation.BorderStroke(4.dp, androidx.compose.ui.graphics.Color.White), + modifier = Modifier.size(120.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Text( + text = mahasiswa.nama.take(1).uppercase(), + style = MaterialTheme.typography.displayMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = GoldPrimary + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Nama & Status + Text( + text = mahasiswa.nama, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Black + ) + Text( + text = "Mahasiswa Aktif", + style = MaterialTheme.typography.bodyMedium, + color = androidx.compose.ui.graphics.Color(0xFF2E7D32) // Hijau status + ) } } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { showLogoutDialog = true }, - modifier = Modifier.fillMaxWidth().height(50.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error + // 2. Data Akademik Card + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Text( + text = "Informasi Akademik", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Gray, + modifier = Modifier.padding(bottom = 8.dp, start = 4.dp) ) - ) { - Icon(Icons.Default.ExitToApp, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("Keluar", style = MaterialTheme.typography.titleMedium) - } - Spacer(modifier = Modifier.height(24.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(20.dp)) { + ProfilItem(Icons.Default.Badge, "NPM", mahasiswa.npm, GoldPrimary) + Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.2f), modifier = Modifier.padding(vertical = 12.dp)) + ProfilItem(Icons.Default.School, "Fakultas", mahasiswa.fakultas, GoldPrimary) + Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.2f), modifier = Modifier.padding(vertical = 12.dp)) + ProfilItem(Icons.Default.Book, "Jurusan", mahasiswa.jurusan, GoldPrimary) + Divider(color = androidx.compose.ui.graphics.Color.LightGray.copy(alpha = 0.2f), modifier = Modifier.padding(vertical = 12.dp)) + ProfilItem(Icons.Default.Timeline, "Semester", "Semester ${mahasiswa.semester}", GoldPrimary) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 3. Data Pribadi Card + Text( + text = "Data Pribadi", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Gray, + modifier = Modifier.padding(bottom = 8.dp, start = 4.dp) + ) + + Card( + modifier = Modifier.fillMaxWidth(), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.padding(20.dp)) { + ProfilItem( + icon = if(mahasiswa.jenkel == "L") Icons.Default.Male else Icons.Default.Female, + label = "Jenis Kelamin", + value = if (mahasiswa.jenkel == "L") "Laki-laki" else "Perempuan", + tint = GoldPrimary + ) + } + } + + Spacer(modifier = Modifier.height(40.dp)) + + // 4. Logout Button + Button( + onClick = { showLogoutDialog = true }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaroonSecondary) + ) { + Icon(Icons.Default.ExitToApp, contentDescription = null, tint = androidx.compose.ui.graphics.Color.White) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Keluar Aplikasi", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.White + ) + } + + // Versi App + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Versi 1.0.0", + style = MaterialTheme.typography.bodySmall, + color = androidx.compose.ui.graphics.Color.LightGray + ) + Spacer(modifier = Modifier.height(30.dp)) + } } + // Dialog Konfirmasi Logout if (showLogoutDialog) { AlertDialog( onDismissRequest = { showLogoutDialog = false }, - icon = { Icon(Icons.Default.ExitToApp, contentDescription = null) }, - title = { Text("Keluar dari Aplikasi?") }, - text = { Text("Anda akan logout dan harus login kembali untuk menggunakan aplikasi.") }, + title = { Text("Konfirmasi Keluar", color = MaroonSecondary) }, + text = { Text("Apakah Anda yakin ingin keluar dari akun ini?", color = androidx.compose.ui.graphics.Color.Gray) }, confirmButton = { - TextButton( - onClick = { - showLogoutDialog = false - onLogout() - } - ) { - Text("Keluar") + TextButton(onClick = { showLogoutDialog = false; onLogout() }) { + Text("Ya, Keluar", color = MaroonSecondary, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) } }, dismissButton = { TextButton(onClick = { showLogoutDialog = false }) { - Text("Batal") + Text("Batal", color = androidx.compose.ui.graphics.Color.Gray) } - } + }, + containerColor = androidx.compose.ui.graphics.Color.White, + icon = { Icon(Icons.Default.Warning, null, tint = MaroonSecondary) } ) } } @Composable -fun ProfilItem(label: String, value: String) { - Column { - Text( - text = label, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = value, - style = MaterialTheme.typography.bodyLarge - ) +fun ProfilItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + label: String, + value: String, + tint: androidx.compose.ui.graphics.Color +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Surface( + shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp), + color = tint.copy(alpha = 0.1f), + modifier = Modifier.size(40.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(20.dp)) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text(text = label, style = MaterialTheme.typography.bodySmall, color = androidx.compose.ui.graphics.Color.Gray) + Text(text = value, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold), color = androidx.compose.ui.graphics.Color.Black) + } } } -/* ================= ABSENSI SCREEN ================= */ +// ================= ABSENSI SCREEN (UI DASHBOARD BARU) ================= @SuppressLint("NewApi") @Composable @@ -1846,99 +2127,61 @@ fun AbsensiScreenWithJadwal( mahasiswa: Mahasiswa ) { val context = LocalContext.current + val scrollState = rememberScrollState() - // State - var lokasi by remember { mutableStateOf("📍 Koordinat: Memuat...") } + // Warna Tema Lokal + val GoldPrimary = androidx.compose.ui.graphics.Color(0xFFB8860B) + val GoldLight = androidx.compose.ui.graphics.Color(0xFFDAA520) + val MaroonSecondary = androidx.compose.ui.graphics.Color(0xFF800000) + val GreenSuccess = androidx.compose.ui.graphics.Color(0xFF2E7D32) + val RedError = androidx.compose.ui.graphics.Color(0xFFC62828) + + // State Logic (SAMA SEPERTI SEBELUMNYA) + var lokasiStatus by remember { mutableStateOf("Memuat lokasi...") } + var isDalamArea by remember { mutableStateOf(false) } // Untuk indikator visual var latitude by remember { mutableStateOf(null) } var longitude by remember { mutableStateOf(null) } var foto by remember { mutableStateOf(null) } var isLoading by remember { mutableStateOf(false) } var jarakKeKampus by remember { mutableStateOf(null) } - - // State Error Handling var errorMessage by remember { mutableStateOf(null) } var jadwalList by remember { mutableStateOf>(emptyList()) } var selectedJadwal by remember { mutableStateOf(null) } var showJadwalDialog by remember { mutableStateOf(false) } + // --- SETUP LAUNCHER (LOGIC TETAP) --- val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) - // Load Jadwal untuk Dropdown LaunchedEffect(Unit) { getJadwalToday(token = token, onSuccess = { jadwal -> activity.runOnUiThread { jadwalList = jadwal.filter { !it.sudahAbsen } } - }, onError = { - // Error load jadwal di home screen cukup toast atau diam saja - // agar tidak menghalangi fungsi ambil foto - }) + }, onError = {}) } val locationPermissionLauncher = 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) { - // ======================================================== - // 🛡️ SECURITY FIX: DETEKSI FAKE GPS - // ======================================================== - val isFakeGps = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - location.isMock // Untuk Android 12 ke atas - } else { - location.isFromMockProvider // Untuk Android lama - } - - if (isFakeGps) { - // JIKA TERDETEKSI PALSU - latitude = null // Null-kan agar tombol kirim mati - longitude = null - jarakKeKampus = null - lokasi = "⛔ FAKE GPS TERDETEKSI!\nSistem menolak lokasi palsu.\nMatikan aplikasi Fake GPS Anda." - - // Tampilkan dialog error - errorMessage = "⚠️ Keamanan: Terdeteksi menggunakan Fake GPS/Lokasi Palsu. Mohon matikan aplikasi tersebut dan coba lagi." - } else { - // JIKA LOKASI ASLI (Logika Normal) - latitude = location.latitude - longitude = location.longitude - - val jarak = hitungJarak( - location.latitude, - location.longitude, - AppConstants.KAMPUS_LATITUDE, - AppConstants.KAMPUS_LONGITUDE - ) - jarakKeKampus = jarak - - val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) { - "✅ DI DALAM AREA" - } else { - "❌ DI LUAR AREA" - } - - lokasi = "📍 Lat: ${String.format("%.6f", location.latitude)}\n" + - "📍 Lon: ${String.format("%.6f", location.longitude)}\n" + - "📏 Jarak: ${String.format("%.0f", jarak)} m\n" + - "$statusLokasi" - } + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + if (location != null) { + // ANTI FAKE GPS + val isFakeGps = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) location.isMock else location.isFromMockProvider + if (isFakeGps) { + latitude = null; longitude = null; jarakKeKampus = null + lokasiStatus = "⛔ Fake GPS Terdeteksi!"; isDalamArea = false + errorMessage = "⚠️ Matikan aplikasi Fake GPS Anda!" } else { - lokasi = "❌ Lokasi tidak tersedia (Aktifkan GPS)" + latitude = location.latitude; longitude = location.longitude + val jarak = hitungJarak(location.latitude, location.longitude, AppConstants.KAMPUS_LATITUDE, AppConstants.KAMPUS_LONGITUDE) + jarakKeKampus = jarak + isDalamArea = jarak <= AppConstants.RADIUS_METER + lokasiStatus = if (isDalamArea) "Di Dalam Area Kampus (${String.format("%.0f", jarak)}m)" else "Di Luar Area Kampus (${String.format("%.0f", jarak)}m)" } - } - .addOnFailureListener { - lokasi = "❌ Gagal mengambil lokasi" - errorMessage = "Gagal mengambil lokasi: ${it.message}" - } + } else { lokasiStatus = "❌ Lokasi tidak tersedia" } + } } - } else { - errorMessage = "⚠️ Izin lokasi ditolak. Aplikasi tidak dapat digunakan." } } @@ -1955,117 +2198,259 @@ fun AbsensiScreenWithJadwal( LaunchedEffect(Unit) { locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) } - // Dialog Error jika Gagal Submit - if (errorMessage != null) { - ErrorDialog(message = errorMessage!!, onDismiss = { errorMessage = null }) - } + if (errorMessage != null) ErrorDialog(message = errorMessage!!, onDismiss = { errorMessage = null }) - // Dialog Pilih Jadwal + // Dialog Jadwal if (showJadwalDialog) { AlertDialog( onDismissRequest = { showJadwalDialog = false }, - title = { Text("Pilih Mata Kuliah") }, + title = { Text("Pilih Mata Kuliah", fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, color = GoldPrimary) }, text = { Column { - if (jadwalList.isEmpty()) Text("Tidak ada kelas yang bisa diabsen saat ini") + if (jadwalList.isEmpty()) Text("Tidak ada kelas aktif saat ini.") else { jadwalList.forEach { jadwal -> - OutlinedCard( - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) - .clickable { selectedJadwal = jadwal; showJadwalDialog = false } + Card( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable { selectedJadwal = jadwal; showJadwalDialog = false }, + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFFF5F5F5)), + border = androidx.compose.foundation.BorderStroke(1.dp, androidx.compose.ui.graphics.Color.LightGray) ) { Column(modifier = Modifier.padding(12.dp)) { - Text(jadwal.namaMatkul, style = MaterialTheme.typography.titleSmall) - Text("${jadwal.jamMulai.substring(0,5)} - ${jadwal.jamSelesai.substring(0,5)} | ${jadwal.ruangan}", style = MaterialTheme.typography.bodySmall) + Text(jadwal.namaMatkul, fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, color = GoldPrimary) + Text("${jadwal.jamMulai.substring(0,5)} - ${jadwal.jamSelesai.substring(0,5)} • ${jadwal.ruangan}", style = MaterialTheme.typography.bodySmall) } } } } } }, - confirmButton = { TextButton(onClick = { showJadwalDialog = false }) { Text("Tutup") } } + confirmButton = { TextButton(onClick = { showJadwalDialog = false }) { Text("Tutup", color = MaroonSecondary) } }, + containerColor = androidx.compose.ui.graphics.Color.White ) } - Column(modifier = modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.SpaceBetween) { - Column { - Text("Absensi Akademik", style = MaterialTheme.typography.titleLarge) - Text("Halo, ${mahasiswa.nama}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - } + // --- UI DASHBOARD --- + Box(modifier = modifier.fillMaxSize()) { - // ... (Bagian UI Card Mata Kuliah, Lokasi, Foto SAMA SEPERTI SEBELUMNYA) ... - // Agar ringkas, copy UI Card bagian tengah dari kode lama Anda ke sini. - // Bagian: Card Mata Kuliah, Card Lokasi, Card Preview Foto + // 1. Header Background + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .background( + brush = androidx.compose.ui.graphics.Brush.verticalGradient(colors = listOf(GoldPrimary, GoldLight)), + shape = androidx.compose.foundation.shape.RoundedCornerShape(bottomStart = 40.dp, bottomEnd = 40.dp) + ) + ) - Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.Center) { - // ... paste UI cards here ... - Card(modifier = Modifier.fillMaxWidth().clickable { showJadwalDialog = true }, colors = CardDefaults.cardColors(containerColor = if (selectedJadwal != null) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant)) { - Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Column( + modifier = Modifier.fillMaxSize().verticalScroll(scrollState) + ) { + // 2. Profile Section (Header) + Row( + modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 24.dp, top = 40.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Avatar + Surface( + shape = CircleShape, + color = androidx.compose.ui.graphics.Color.White, + modifier = Modifier.size(56.dp), + shadowElevation = 4.dp + ) { + Box(contentAlignment = Alignment.Center) { + Text( + text = mahasiswa.nama.take(1).uppercase(), + style = MaterialTheme.typography.headlineSmall.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = GoldPrimary + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + text = "Halo, ${mahasiswa.nama.split(" ").first()}", + style = MaterialTheme.typography.titleLarge.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.White + ) + Text( + text = "${mahasiswa.npm} | ${mahasiswa.jurusan}", + style = MaterialTheme.typography.bodyMedium, + color = androidx.compose.ui.graphics.Color.White.copy(alpha = 0.9f) + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // 3. Status Lokasi Card (Floating) + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp), + shape = androidx.compose.foundation.shape.RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp) + ) { + Row( + modifier = Modifier.padding(20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon Status + Surface( + shape = CircleShape, + color = if (isDalamArea) GreenSuccess.copy(alpha = 0.1f) else RedError.copy(alpha = 0.1f), + modifier = Modifier.size(50.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = if (isDalamArea) Icons.Default.LocationOn else Icons.Default.WrongLocation, + contentDescription = null, + tint = if (isDalamArea) GreenSuccess else RedError, + modifier = Modifier.size(24.dp) + ) + } + } + Spacer(modifier = Modifier.width(16.dp)) Column { - Text("📚 Mata Kuliah", style = MaterialTheme.typography.labelMedium) - Text(selectedJadwal?.namaMatkul ?: "Pilih mata kuliah", style = MaterialTheme.typography.titleMedium) - } - Icon(Icons.Default.ArrowDropDown, null) - } - } - Spacer(modifier = Modifier.height(16.dp)) - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp)) { - Text("📍 Informasi Lokasi", style = MaterialTheme.typography.titleMedium) - Text(lokasi, style = MaterialTheme.typography.bodyMedium) - } - } - Spacer(modifier = Modifier.height(16.dp)) - if (foto != null) { - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { - Image(bitmap = foto!!.asImageBitmap(), contentDescription = "Preview", modifier = Modifier.size(200.dp).clip(CircleShape)) + Text( + text = "Status Lokasi", + style = MaterialTheme.typography.labelMedium, + color = androidx.compose.ui.graphics.Color.Gray + ) + Text( + text = lokasiStatus, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = if (isDalamArea) GreenSuccess else RedError + ) } } } - } - Column { - Button(onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) }, modifier = Modifier.fillMaxWidth().height(50.dp), enabled = !isLoading) { - Icon(Icons.Default.CameraAlt, null); Spacer(modifier = Modifier.width(8.dp)); Text("Ambil Foto") - } - Spacer(modifier = Modifier.height(12.dp)) - Button( - onClick = { - if (selectedJadwal == null) { errorMessage = "⚠️ Pilih mata kuliah terlebih dahulu!"; return@Button } - if (latitude == null || foto == null) { errorMessage = "⚠️ Lokasi dan Foto wajib ada!"; return@Button } - if (jarakKeKampus != null && jarakKeKampus!! > AppConstants.RADIUS_METER) { - errorMessage = "❌ Anda berada di luar area kampus! (${String.format("%.0f", jarakKeKampus)} m)" - return@Button - } + Spacer(modifier = Modifier.height(24.dp)) - isLoading = true - submitAbsensiWithJadwal( - token = token, idJadwal = selectedJadwal!!.idJadwal, - latitude = latitude!! + AppConstants.LATITUDE_OFFSET, longitude = longitude!! + AppConstants.LONGITUDE_OFFSET, - fotoBase64 = bitmapToBase64(foto!!), status = "HADIR", - onSuccess = { matkul -> - activity.runOnUiThread { - isLoading = false - foto = null; selectedJadwal = null - Toast.makeText(context, "✅ Absensi $matkul berhasil!", Toast.LENGTH_LONG).show() - // Refresh jadwal - getJadwalToday(token, { j -> activity.runOnUiThread { jadwalList = j.filter { !it.sudahAbsen } } }, {}) + // 4. Form Absensi Section + Column(modifier = Modifier.padding(horizontal = 24.dp)) { + Text( + text = "Formulir Absensi", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.Bold), + color = androidx.compose.ui.graphics.Color.Black + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Selector Mata Kuliah + Card( + modifier = Modifier.fillMaxWidth().clickable { showJadwalDialog = true }, + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color.White), + border = androidx.compose.foundation.BorderStroke(1.dp, androidx.compose.ui.graphics.Color(0xFFEEEEEE)) + ) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text("Mata Kuliah", style = MaterialTheme.typography.labelSmall, color = androidx.compose.ui.graphics.Color.Gray) + Text( + text = selectedJadwal?.namaMatkul ?: "Pilih mata kuliah...", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold), + color = if(selectedJadwal != null) GoldPrimary else androidx.compose.ui.graphics.Color.Gray + ) + } + Icon(Icons.Default.KeyboardArrowDown, null, tint = androidx.compose.ui.graphics.Color.Gray) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Area Foto (Besar) + Card( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .clickable { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) }, + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = androidx.compose.ui.graphics.Color(0xFFF8F9FA)), + border = androidx.compose.foundation.BorderStroke( + width = 2.dp, + color = GoldPrimary.copy(alpha = 0.5f) // Warna emas pudar + ) + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + if (foto != null) { + Image( + bitmap = foto!!.asImageBitmap(), + contentDescription = "Foto", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + // Tombol Retake kecil di pojok + Box(modifier = Modifier.fillMaxSize().padding(12.dp), contentAlignment = Alignment.BottomEnd) { + Surface(shape = CircleShape, color = androidx.compose.ui.graphics.Color.White) { + Icon(Icons.Default.Refresh, null, modifier = Modifier.padding(8.dp), tint = GoldPrimary) + } } - }, - onError = { err -> - activity.runOnUiThread { - isLoading = false - errorMessage = err // Tampilkan Dialog Error + } else { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon(Icons.Default.CameraEnhance, null, modifier = Modifier.size(48.dp), tint = GoldPrimary.copy(alpha = 0.5f)) + Spacer(modifier = Modifier.height(8.dp)) + Text("Ketuk untuk ambil foto selfie", color = androidx.compose.ui.graphics.Color.Gray) } } - ) - }, - modifier = Modifier.fillMaxWidth().height(50.dp), - enabled = !isLoading && foto != null && selectedJadwal != null - ) { - if (isLoading) CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary) - else { Icon(Icons.Default.Check, null); Spacer(modifier = Modifier.width(8.dp)); Text("Kirim Absensi") } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + // Tombol Submit Besar + Button( + onClick = { + if (selectedJadwal == null) { errorMessage = "⚠️ Pilih mata kuliah terlebih dahulu!"; return@Button } + if (latitude == null || foto == null) { errorMessage = "⚠️ Lokasi dan Foto wajib ada!"; return@Button } + if (!isDalamArea) { errorMessage = "❌ Anda berada di luar area kampus!"; return@Button } + + isLoading = true + submitAbsensiWithJadwal( + token = token, idJadwal = selectedJadwal!!.idJadwal, + latitude = latitude!! + AppConstants.LATITUDE_OFFSET, longitude = longitude!! + AppConstants.LONGITUDE_OFFSET, + fotoBase64 = bitmapToBase64(foto!!), status = "HADIR", + onSuccess = { matkul -> + activity.runOnUiThread { + isLoading = false; foto = null; selectedJadwal = null + Toast.makeText(context, "✅ Absensi $matkul berhasil!", Toast.LENGTH_LONG).show() + getJadwalToday(token, { j -> activity.runOnUiThread { jadwalList = j.filter { !it.sudahAbsen } } }, {}) + } + }, + onError = { err -> activity.runOnUiThread { isLoading = false; errorMessage = err } } + ) + }, + modifier = Modifier.fillMaxWidth().height(56.dp), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + colors = ButtonDefaults.buttonColors(containerColor = androidx.compose.ui.graphics.Color.Transparent), + contentPadding = PaddingValues(), + enabled = !isLoading + ) { + Box( + modifier = Modifier.fillMaxSize().background( + brush = if (!isLoading && selectedJadwal != null && foto != null) + androidx.compose.ui.graphics.Brush.horizontalGradient(colors = listOf(GoldPrimary, MaroonSecondary)) + else androidx.compose.ui.graphics.Brush.linearGradient(colors = listOf(androidx.compose.ui.graphics.Color.Gray, androidx.compose.ui.graphics.Color.LightGray)), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp) + ), + contentAlignment = Alignment.Center + ) { + if (isLoading) CircularProgressIndicator(color = androidx.compose.ui.graphics.Color.White) + else { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.CheckCircle, null, tint = androidx.compose.ui.graphics.Color.White) + Spacer(modifier = Modifier.width(8.dp)) + Text("KIRIM ABSENSI", fontWeight = androidx.compose.ui.text.font.FontWeight.Bold, color = androidx.compose.ui.graphics.Color.White) + } + } + } + } + + Spacer(modifier = Modifier.height(40.dp)) } } } diff --git a/app/src/main/res/drawable/logo_ubhara.png b/app/src/main/res/drawable/logo_ubhara.png new file mode 100644 index 0000000..1b32578 Binary files /dev/null and b/app/src/main/res/drawable/logo_ubhara.png differ