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.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,12 +252,13 @@ 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)
|
||||||
@ -236,9 +267,10 @@ fun calculateDailyStreak(journals: List<JournalEntry>): Int {
|
|||||||
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 {
|
item {
|
||||||
val currentMonth = Calendar.getInstance().get(Calendar.MONTH)
|
LazyVerticalGrid(
|
||||||
val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
|
columns = GridCells.Fixed(3),
|
||||||
val isReportAvailable = currentMonth == Calendar.DECEMBER && currentDay >= 15
|
modifier = Modifier.height(240.dp).fillMaxWidth(),
|
||||||
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) } }
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(badges) { badge ->
|
||||||
|
BadgeItem(badge = badge, onClick = { selectedBadge = badge })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
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") } }
|
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) } } } }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user