Perbaikan UI/UX

This commit is contained in:
202310715320 AHMAR RAFLY MARYADI 2026-01-10 00:05:29 +07:00
parent 6602e0e143
commit 2365e0b907

View File

@ -21,6 +21,7 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -30,6 +31,7 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@ -50,6 +52,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
@ -66,6 +69,7 @@ import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
@ -90,7 +94,22 @@ data class JournalEntry(
val dateString: String = "" val dateString: String = ""
) )
data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean) data class AssessmentEntry(
val id: String = "",
val userId: String = "",
val totalScore: Int = 0,
val level: String = "",
val timestamp: com.google.firebase.Timestamp? = null,
val dateString: String = ""
)
data class Badge(
val title: String,
val description: String,
val icon: ImageVector,
val isUnlocked: Boolean,
val dateUnlocked: String? = null
)
// --- Main Activity --- // --- Main Activity ---
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -161,6 +180,9 @@ fun LoginScreen(navController: NavController) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text("Lacak kesehatan mental, latih otak, dan temukan ketenangan.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp)) Text("Lacak kesehatan mental, latih otak, dan temukan ketenangan.", style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp))
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
// Login Google dinonaktifkan sementara sesuai permintaan
/*
if (isLoading) { if (isLoading) {
CircularProgressIndicator() CircularProgressIndicator()
} else { } else {
@ -170,6 +192,14 @@ fun LoginScreen(navController: NavController) {
Text("Masuk dengan Google") Text("Masuk dengan Google")
} }
} }
*/
Text("Opsi login sedang tidak tersedia", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
// Tombol bypass untuk keperluan pengembangan jika diperlukan
Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = { navController.navigate("main") { popUpTo("login") { inclusive = true } } }) {
Text("Masuk sebagai Tamu (Dev Mode)")
}
} }
} }
@ -222,23 +252,25 @@ fun AppBottomNavigation(navController: NavHostController) {
} }
} }
fun calculateDailyStreak(journals: List<JournalEntry>): Int { fun calculateDailyStreak(dates: List<Date>): Int {
if (journals.isEmpty()) return 0 if (dates.isEmpty()) return 0
val entryDates = journals.map { it.timestamp?.toDate() ?: Date(0) }.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }.distinct().sortedDescending() val entryDates = dates.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }.distinct().sortedDescending()
var streak = 0 var streak = 0
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if (todayStr in entryDates) { if (todayStr in entryDates) {
streak++ streak++
calendar.add(Calendar.DATE, -1) calendar.add(Calendar.DATE, -1)
} else { } else {
calendar.add(Calendar.DATE, -1) calendar.add(Calendar.DATE, -1)
val yesterdayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) val yesterdayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if(yesterdayStr !in entryDates) return 0 if (yesterdayStr !in entryDates) return 0
} }
for (i in 1 until entryDates.size) {
while (true) {
val expectedDateStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) val expectedDateStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if (entryDates.getOrNull(i) == expectedDateStr) { if (expectedDateStr in entryDates) {
streak++ streak++
calendar.add(Calendar.DATE, -1) calendar.add(Calendar.DATE, -1)
} else { } else {
@ -248,16 +280,20 @@ fun calculateDailyStreak(journals: List<JournalEntry>): Int {
return streak return streak
} }
fun getBadges(journals: List<JournalEntry>, streak: Int): List<Badge> { fun getBadges(journals: List<JournalEntry>, assessments: List<AssessmentEntry>, streak: Int): List<Badge> {
val totalEntries = journals.size val totalActivities = journals.size + assessments.size
val reflectionEntries = journals.count { it.type == "reflection" } val reflectionEntries = journals.count { it.type == "reflection" }
val highStressEntries = journals.count { it.mentalScore > 60 }
// Untuk demo, kita ambil tanggal hari ini jika lencana terbuka
val today = SimpleDateFormat("d MMM yyyy", Locale("id", "ID")).format(Date())
return listOf( return listOf(
Badge("Jurnalis Pertama", "Menulis jurnal pertamamu", Icons.Default.Edit, totalEntries >= 1), Badge("Langkah Awal", "Lakukan aktivitas pertamamu untuk memulai perjalanan kesehatan mental.", Icons.Default.DirectionsRun, totalActivities >= 1, if(totalActivities >= 1) today else null),
Badge("Seminggu Penuh", "Streak jurnaling 7 hari", Icons.Default.CalendarToday, streak >= 7), Badge("Penulis Rutin", "Berhasil menulis 5 jurnal bebas sebagai bentuk ekspresi diri.", Icons.Default.EditNote, journals.size >= 5, if(journals.size >= 5) today else null),
Badge("Reflektor", "Selesaikan 5 refleksi", Icons.Default.SelfImprovement, reflectionEntries >= 5), Badge("Jiwa Reflektif", "Selesaikan 3 refleksi diri untuk mengenal dirimu lebih dalam.", Icons.Default.SelfImprovement, reflectionEntries >= 3, if(reflectionEntries >= 3) today else null),
Badge("Sadar Diri", "Menganalisis 10 jurnal", Icons.Default.Insights, totalEntries >= 10), Badge("Disiplin Diri", "Pertahankan aktivitas selama 7 hari tanpa terputus.", Icons.Default.LocalFireDepartment, streak >= 7, if(streak >= 7) today else null),
Badge("Pejuang Tangguh", "Mengatasi 3 hari skor tinggi", Icons.Default.Shield, highStressEntries >= 3) Badge("Pencari Jawaban", "Lakukan 5 penilaian harian untuk memantau kondisi mentalmu.", Icons.Default.Psychology, assessments.size >= 5, if(assessments.size >= 5) today else null),
Badge("Master Fokus", "Selesaikan total 10 aktivitas apa saja di dalam aplikasi.", Icons.Default.EmojiEvents, totalActivities >= 10, if(totalActivities >= 10) today else null)
) )
} }
@ -268,43 +304,214 @@ fun ProfileScreen(navController: NavController) {
val user = auth.currentUser val user = auth.currentUser
val db = Firebase.firestore val db = Firebase.firestore
var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) } var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
var assessmentList by remember { mutableStateOf<List<AssessmentEntry>>(emptyList()) }
var dailyStreak by remember { mutableIntStateOf(0) } var dailyStreak by remember { mutableIntStateOf(0) }
var badges by remember { mutableStateOf<List<Badge>>(emptyList()) } var selectedBadge by remember { mutableStateOf<Badge?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(user) { LaunchedEffect(user) {
if (user != null) { if (user != null) {
db.collection("journals").whereEqualTo("userId", user.uid).orderBy("timestamp").get().addOnSuccessListener { result -> scope.launch {
val journals = result.toObjects(JournalEntry::class.java) val journalTask = db.collection("journals").whereEqualTo("userId", user.uid).get()
journalList = journals val assessmentTask = db.collection("assessments").whereEqualTo("userId", user.uid).get()
dailyStreak = calculateDailyStreak(journals)
badges = getBadges(journals, dailyStreak) try {
val journalSnap = journalTask.await()
val assessmentSnap = assessmentTask.await()
journalList = journalSnap.toObjects(JournalEntry::class.java)
assessmentList = assessmentSnap.toObjects(AssessmentEntry::class.java)
val allDates = (journalList.mapNotNull { it.timestamp?.toDate() } +
assessmentList.mapNotNull { it.timestamp?.toDate() })
dailyStreak = calculateDailyStreak(allDates)
} catch (e: Exception) {
Log.e("ProfileScreen", "Error loading data", e)
}
} }
} }
} }
val badges = remember(journalList, assessmentList, dailyStreak) {
getBadges(journalList, assessmentList, dailyStreak)
}
if (selectedBadge != null) {
BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null })
}
LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) {
item { if (user != null) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage(model = user.photoUrl, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp).clip(CircleShape)); Spacer(modifier = Modifier.width(16.dp)); Column { Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall); Text(user.email ?: "", style = MaterialTheme.typography.bodyMedium) } } } } item { if (user != null) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage(model = user.photoUrl, contentDescription = "Profile Picture", modifier = Modifier.size(64.dp).clip(CircleShape)); Spacer(modifier = Modifier.width(16.dp)); Column { Text(user.displayName ?: "Pengguna", style = MaterialTheme.typography.headlineSmall); Text(user.email ?: "Tamu", style = MaterialTheme.typography.bodyMedium) } } } else { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.AccountCircle, null, modifier = Modifier.size(64.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("Mode Tamu", style = MaterialTheme.typography.headlineSmall); Text("Masuk untuk simpan data", style = MaterialTheme.typography.bodyMedium) } } } }
item { Card(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium); Text("Streak Jurnaling Harian", style = MaterialTheme.typography.bodySmall) } } } }
item { Card(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocalFireDepartment, contentDescription = "Streak", tint = Color(0xFFFFA500), modifier = Modifier.size(40.dp)); Spacer(modifier = Modifier.width(16.dp)); Column { Text("$dailyStreak Hari", style = MaterialTheme.typography.headlineMedium); Text("Streak Aktivitas Harian", style = MaterialTheme.typography.bodySmall) } } } }
item { item {
Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth()) Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth())
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 100.dp), contentPadding = PaddingValues(top = 8.dp), modifier = Modifier.height(120.dp).fillMaxWidth()) { Spacer(modifier = Modifier.height(8.dp))
items(badges) { BadgeItem(badge = it) } }
item {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.height(240.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(badges) { badge ->
BadgeItem(badge = badge, onClick = { selectedBadge = badge })
}
} }
} }
item { item {
val currentMonth = Calendar.getInstance().get(Calendar.MONTH) Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth())
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) Spacer(modifier = Modifier.height(8.dp))
val isReportAvailable = currentMonth == Calendar.DECEMBER && currentDay >= 15 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Card(onClick = { /* Navigasi ke raport */ }, enabled = isReportAvailable, modifier = Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)){ Text("Raport Tahunan", fontWeight = FontWeight.Bold); Text(if(isReportAvailable) "Raport tahun ini sudah tersedia!" else "Tersedia setiap 15 Desember.", style = MaterialTheme.typography.bodySmall) } } ReportCard("Mingguan", "Analisis 7 hari", Modifier.weight(1f))
ReportCard("Bulanan", "Analisis 30 hari", Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(8.dp))
ReportCard("Tahunan", "Tersedia 15 Des", Modifier.fillMaxWidth(), enabled = false)
} }
item { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth()) { Text("Lihat Riwayat Jurnal Lengkap") } } item { OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth()) { Text("Lihat Riwayat Jurnal Lengkap") } }
} }
} }
@Composable @Composable
fun BadgeItem(badge: Badge) { fun BadgeItem(badge: Badge, onClick: () -> Unit) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp).alpha(if (badge.isUnlocked) 1f else 0.4f)) { Column(
Icon(badge.icon, contentDescription = badge.title, modifier = Modifier.size(48.dp), tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray) horizontalAlignment = Alignment.CenterHorizontally,
Spacer(modifier = Modifier.height(4.dp)) modifier = Modifier
Text(badge.title, fontSize = 12.sp, textAlign = TextAlign.Center, lineHeight = 14.sp) .fillMaxWidth()
.clickable { onClick() }
.alpha(if (badge.isUnlocked) 1f else 0.4f)
) {
Box(
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f))
.border(2.dp, if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.5f), CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
badge.icon,
contentDescription = badge.title,
modifier = Modifier.size(32.dp),
tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
badge.title,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
lineHeight = 14.sp
)
}
}
@Composable
fun BadgeDetailDialog(badge: Badge, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(
shape = RoundedCornerShape(24.dp),
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier.padding(24.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, contentDescription = "Close") }
}
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primaryContainer else Color.LightGray.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Icon(
badge.icon,
contentDescription = badge.title,
modifier = Modifier.size(60.dp),
tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
)
}
Spacer(modifier = Modifier.height(24.dp))
if (badge.isUnlocked && badge.dateUnlocked != null) {
Surface(
color = Color(0xFF333333),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = badge.dateUnlocked,
color = Color.White,
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(16.dp))
}
Text(
text = if (badge.isUnlocked) "Kamu meraih pencapaian" else "Pencapaian Terkunci",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = badge.title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = badge.description,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(50)
) {
Text("Tutup")
}
}
}
}
}
@Composable
fun ReportCard(title: String, subtitle: String, modifier: Modifier = Modifier, enabled: Boolean = true) {
Card(
onClick = { /* Navigasi ke laporan terkait */ },
enabled = enabled,
modifier = modifier,
colors = if (enabled) CardDefaults.cardColors() else CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
) {
Column(Modifier.padding(12.dp)) {
Text(title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleSmall)
Text(subtitle, style = MaterialTheme.typography.labelSmall, color = if(enabled) MaterialTheme.colorScheme.onSurfaceVariant else Color.Gray)
}
} }
} }
@ -361,7 +568,57 @@ fun JournalScreen() {
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun AssessmentScreen() { val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri", "Sulit Konsentrasi", "Lelah", "Pikiran Bunuh Diri") }; val sliderValues = remember { mutableStateMapOf<String, Float>().apply { indicators.forEach { put(it, 0f) } } }; val totalScore = sliderValues.values.sum().toInt(); val assessmentLevel = when (totalScore) { in 0..4 -> "Normal"; in 5..9 -> "Ringan"; in 10..14 -> "Sedang"; else -> "Berat" }; Scaffold(topBar = { TopAppBar(title = { Text("Penilaian Harian") }) }) { innerPadding -> LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) } } }; items(indicators) { indicator -> IndicatorItem(indicatorName = indicator, value = sliderValues[indicator] ?: 0f) { sliderValues[indicator] = it.roundToInt().toFloat() } }; item { Button(onClick = { /* Save logic */ }, modifier = Modifier.fillMaxWidth()) { Text("Selesai") } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun AssessmentScreen() {
val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri", "Sulit Konsentrasi", "Lelah", "Pikiran Bunuh Diri") }
val sliderValues = remember { mutableStateMapOf<String, Float>().apply { indicators.forEach { put(it, 0f) } } }
val totalScore = sliderValues.values.sum().toInt()
val assessmentLevel = when (totalScore) { in 0..4 -> "Normal"; in 5..9 -> "Ringan"; in 10..14 -> "Sedang"; else -> "Berat" }
val auth = Firebase.auth
val db = Firebase.firestore
val context = LocalContext.current
var isSaving by remember { mutableStateOf(false) }
Scaffold(topBar = { TopAppBar(title = { Text("Penilaian Harian") }) }) { innerPadding ->
LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
item { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)) { Column(modifier = Modifier.padding(16.dp)) { Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium); Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium); Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) } } }
items(indicators) { indicator -> IndicatorItem(indicatorName = indicator, value = sliderValues[indicator] ?: 0f) { sliderValues[indicator] = it.roundToInt().toFloat() } }
item {
Button(
onClick = {
val user = auth.currentUser
if (user != null) {
isSaving = true
val entry = hashMapOf(
"userId" to user.uid,
"totalScore" to totalScore,
"level" to assessmentLevel,
"timestamp" to FieldValue.serverTimestamp(),
"dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date())
)
db.collection("assessments").add(entry)
.addOnSuccessListener {
Toast.makeText(context, "Penilaian berhasil disimpan!", Toast.LENGTH_SHORT).show()
isSaving = false
}
.addOnFailureListener {
Toast.makeText(context, "Gagal menyimpan penilaian", Toast.LENGTH_SHORT).show()
isSaving = false
}
} else {
Toast.makeText(context, "Login dulu untuk menyimpan", Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isSaving
) {
if (isSaving) CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)
else Text("Selesai & Simpan")
}
}
}
}
}
@Composable fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { val description = when (value.toInt()) { 0 -> "Tidak sama sekali"; 1 -> "Beberapa hari"; 2 -> "Separuh hari"; 3 -> "Hampir setiap hari"; else -> "" }; Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium); Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) }; Slider(value = value, onValueChange = onValueChange, valueRange = 0f..3f, steps = 2); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally)) } } } @Composable fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { val description = when (value.toInt()) { 0 -> "Tidak sama sekali"; 1 -> "Beberapa hari"; 2 -> "Separuh hari"; 3 -> "Hampir setiap hari"; else -> "" }; Card(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(indicatorName, style = MaterialTheme.typography.titleMedium); Text(text = value.toInt().toString(), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary) }; Slider(value = value, onValueChange = onValueChange, valueRange = 0f..3f, steps = 2); Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally)) } } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun CognitiveTestScreen(navController: NavController) { Column(modifier = Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Pilih Tes Kognitif", style = MaterialTheme.typography.headlineSmall); TestCard("Tes Memori", "Uji memori jangka pendek", Icons.Default.Memory, "memory_test", navController); TestCard("Tes Fokus", "Uji fokus & atensi", Icons.Default.CenterFocusStrong, "focus_test", navController); TestCard("Tes Kecepatan Reaksi", "Uji refleks visual", Icons.Default.Speed, "reaction_test", navController); TestCard("Tes Logika", "Uji pola pikir & logika", Icons.Default.Psychology, "logical_test", navController) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun CognitiveTestScreen(navController: NavController) { Column(modifier = Modifier.fillMaxSize().padding(16.dp).verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { Text("Pilih Tes Kognitif", style = MaterialTheme.typography.headlineSmall); TestCard("Tes Memori", "Uji memori jangka pendek", Icons.Default.Memory, "memory_test", navController); TestCard("Tes Fokus", "Uji fokus & atensi", Icons.Default.CenterFocusStrong, "focus_test", navController); TestCard("Tes Kecepatan Reaksi", "Uji refleks visual", Icons.Default.Speed, "reaction_test", navController); TestCard("Tes Logika", "Uji pola pikir & logika", Icons.Default.Psychology, "logical_test", navController) } }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun TestCard(title: String, description: String, icon: ImageVector, route: String, navController: NavController) { Card(onClick = { navController.navigate(route) }, modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp)); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium); Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun TestCard(title: String, description: String, icon: ImageVector, route: String, navController: NavController) { Card(onClick = { navController.navigate(route) }, modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, modifier = Modifier.size(40.dp)); Spacer(Modifier.width(16.dp)); Column { Text(title, style = MaterialTheme.typography.titleMedium); Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray) } } } }