Perbaikan UI/UX
This commit is contained in:
parent
6602e0e143
commit
2365e0b907
@ -21,6 +21,7 @@ import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
@ -66,6 +69,7 @@ import com.google.firebase.ktx.Firebase
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
@ -90,7 +94,22 @@ data class JournalEntry(
|
||||
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 ---
|
||||
class MainActivity : ComponentActivity() {
|
||||
@ -161,6 +180,9 @@ fun LoginScreen(navController: NavController) {
|
||||
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))
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Login Google dinonaktifkan sementara sesuai permintaan
|
||||
/*
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
@ -170,6 +192,14 @@ fun LoginScreen(navController: NavController) {
|
||||
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 {
|
||||
if (journals.isEmpty()) return 0
|
||||
val entryDates = journals.map { it.timestamp?.toDate() ?: Date(0) }.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }.distinct().sortedDescending()
|
||||
fun calculateDailyStreak(dates: List<Date>): Int {
|
||||
if (dates.isEmpty()) return 0
|
||||
val entryDates = dates.map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }.distinct().sortedDescending()
|
||||
var streak = 0
|
||||
val calendar = Calendar.getInstance()
|
||||
val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
|
||||
|
||||
if (todayStr in entryDates) {
|
||||
streak++
|
||||
calendar.add(Calendar.DATE, -1)
|
||||
} else {
|
||||
calendar.add(Calendar.DATE, -1)
|
||||
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)
|
||||
if (entryDates.getOrNull(i) == expectedDateStr) {
|
||||
if (expectedDateStr in entryDates) {
|
||||
streak++
|
||||
calendar.add(Calendar.DATE, -1)
|
||||
} else {
|
||||
@ -248,16 +280,20 @@ fun calculateDailyStreak(journals: List<JournalEntry>): Int {
|
||||
return streak
|
||||
}
|
||||
|
||||
fun getBadges(journals: List<JournalEntry>, streak: Int): List<Badge> {
|
||||
val totalEntries = journals.size
|
||||
fun getBadges(journals: List<JournalEntry>, assessments: List<AssessmentEntry>, streak: Int): List<Badge> {
|
||||
val totalActivities = journals.size + assessments.size
|
||||
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(
|
||||
Badge("Jurnalis Pertama", "Menulis jurnal pertamamu", Icons.Default.Edit, totalEntries >= 1),
|
||||
Badge("Seminggu Penuh", "Streak jurnaling 7 hari", Icons.Default.CalendarToday, streak >= 7),
|
||||
Badge("Reflektor", "Selesaikan 5 refleksi", Icons.Default.SelfImprovement, reflectionEntries >= 5),
|
||||
Badge("Sadar Diri", "Menganalisis 10 jurnal", Icons.Default.Insights, totalEntries >= 10),
|
||||
Badge("Pejuang Tangguh", "Mengatasi 3 hari skor tinggi", Icons.Default.Shield, highStressEntries >= 3)
|
||||
Badge("Langkah Awal", "Lakukan aktivitas pertamamu untuk memulai perjalanan kesehatan mental.", Icons.Default.DirectionsRun, totalActivities >= 1, if(totalActivities >= 1) today else null),
|
||||
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("Jiwa Reflektif", "Selesaikan 3 refleksi diri untuk mengenal dirimu lebih dalam.", Icons.Default.SelfImprovement, reflectionEntries >= 3, if(reflectionEntries >= 3) today else null),
|
||||
Badge("Disiplin Diri", "Pertahankan aktivitas selama 7 hari tanpa terputus.", Icons.Default.LocalFireDepartment, streak >= 7, if(streak >= 7) today else null),
|
||||
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 db = Firebase.firestore
|
||||
var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
|
||||
var assessmentList by remember { mutableStateOf<List<AssessmentEntry>>(emptyList()) }
|
||||
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) {
|
||||
if (user != null) {
|
||||
db.collection("journals").whereEqualTo("userId", user.uid).orderBy("timestamp").get().addOnSuccessListener { result ->
|
||||
val journals = result.toObjects(JournalEntry::class.java)
|
||||
journalList = journals
|
||||
dailyStreak = calculateDailyStreak(journals)
|
||||
badges = getBadges(journals, dailyStreak)
|
||||
scope.launch {
|
||||
val journalTask = db.collection("journals").whereEqualTo("userId", user.uid).get()
|
||||
val assessmentTask = db.collection("assessments").whereEqualTo("userId", user.uid).get()
|
||||
|
||||
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)) {
|
||||
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 { 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 { 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 Aktivitas Harian", style = MaterialTheme.typography.bodySmall) } } } }
|
||||
|
||||
item {
|
||||
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()) {
|
||||
items(badges) { BadgeItem(badge = it) }
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
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 {
|
||||
val currentMonth = Calendar.getInstance().get(Calendar.MONTH)
|
||||
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
|
||||
val isReportAvailable = currentMonth == Calendar.DECEMBER && currentDay >= 15
|
||||
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) } }
|
||||
Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth())
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
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") } }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BadgeItem(badge: Badge) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp).alpha(if (badge.isUnlocked) 1f else 0.4f)) {
|
||||
Icon(badge.icon, contentDescription = badge.title, modifier = Modifier.size(48.dp), tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(badge.title, fontSize = 12.sp, textAlign = TextAlign.Center, lineHeight = 14.sp)
|
||||
fun BadgeItem(badge: Badge, onClick: () -> Unit) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.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)) } } }
|
||||
@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) } } } }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user