Compare commits

..

4 Commits

Author SHA1 Message Date
HadiPrakosou-HD
63d65acc35 Perubahan UI UX 2026-01-12 16:41:31 +07:00
HadiPrakosou-HD
e290b3c28a Merge remote-tracking branch 'HadiPsikologi/Submain' into Submain
# Conflicts:
#	app/src/main/java/com/example/ppb_kelompok2/MainActivity.kt
2026-01-12 16:41:12 +07:00
HadiPrakosou-HD
78ec7c1a89 Perubahan UI UX 2026-01-12 16:40:11 +07:00
HadiPrakosou-HD
ff5dcd893e Perubahan UI UX 2026-01-12 16:39:22 +07:00

View File

@ -21,12 +21,10 @@ 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
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -43,7 +41,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
@ -53,7 +53,6 @@ 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
@ -70,7 +69,6 @@ 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
@ -78,6 +76,13 @@ import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.random.Random import kotlin.random.Random
// --- Warna Kustom ---
val PinkBackground = Color(0xFFFFF0F3)
val SoftPink = Color(0xFFFFC2D1)
val MediumPink = Color(0xFFFF85A1)
val HeartRed = Color(0xFFFF4D6D)
val DeepRed = Color(0xFFC9184A)
// --- Constants --- // --- Constants ---
val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}" val HF_API_TOKEN = "Bearer ${BuildConfig.HF_API_KEY}"
@ -95,31 +100,7 @@ data class JournalEntry(
val dateString: String = "" val dateString: String = ""
) )
data class AssessmentEntry( data class Badge(val title: String, val description: String, val icon: ImageVector, val isUnlocked: Boolean)
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 CognitiveTestEntry(
val id: String = "",
val userId: String = "",
val testType: String = "",
val score: Int = 0,
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() {
@ -127,8 +108,17 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
PPB_Kelompok2Theme { val customColorScheme = lightColorScheme(
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { primary = HeartRed,
onPrimary = Color.White,
primaryContainer = SoftPink,
secondary = MediumPink,
surface = Color.White,
background = PinkBackground
)
MaterialTheme(colorScheme = customColorScheme) {
Surface(modifier = Modifier.fillMaxSize(), color = PinkBackground) {
AppNavigationGraph() AppNavigationGraph()
} }
} }
@ -156,6 +146,9 @@ fun AppNavigationGraph() {
fun LoginScreen(navController: NavController) { fun LoginScreen(navController: NavController) {
val context = LocalContext.current val context = LocalContext.current
var isLoading by remember { mutableStateOf(false) } var isLoading by remember { mutableStateOf(false) }
// --- OAUTH KOMENTAR (Jangan Dihapus) ---
/*
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com") .requestIdToken("532164852718-k3op2ns8b5v42k2e5qj8j1d7g2g1m3g9.apps.googleusercontent.com")
.requestEmail() .requestEmail()
@ -173,685 +166,297 @@ fun LoginScreen(navController: NavController) {
if (authTask.isSuccessful) { if (authTask.isSuccessful) {
navController.navigate("main") { popUpTo("login") { inclusive = true } } navController.navigate("main") { popUpTo("login") { inclusive = true } }
} else { } else {
Toast.makeText(context, "Login Gagal: ${authTask.exception?.message}", Toast.LENGTH_LONG).show() Toast.makeText(context, "Gagal Masuk", Toast.LENGTH_SHORT).show()
} }
} }
} catch (e: ApiException) { } catch (e: ApiException) {
isLoading = false isLoading = false
Log.w("LoginScreen", "Google sign in failed", e) Toast.makeText(context, "Google Error", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Google Sign In Gagal. Cek SHA-1.", Toast.LENGTH_LONG).show()
} }
} else { } else {
isLoading = false isLoading = false
} }
} }
Column(modifier = Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text("MindTrack AI", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold)
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 {
Button(onClick = { isLoading = true; launcher.launch(googleSignInClient.signInIntent) }, modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp)) {
Icon(Icons.Default.AccountCircle, contentDescription = "Google Icon")
Spacer(modifier = Modifier.width(8.dp))
Text("Masuk dengan Google")
}
}
*/ */
Text("Opsi login sedang tidak tersedia", style = MaterialTheme.typography.bodyMedium, color = Color.Gray) // UIUX
Box(modifier = Modifier.fillMaxSize().background(Brush.verticalGradient(listOf(Color.White, SoftPink)))) {
// Tombol bypass untuk keperluan pengembangan jika diperlukan Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(Icons.Default.Favorite, null, tint = HeartRed, modifier = Modifier.size(100.dp))
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
TextButton(onClick = { navController.navigate("main") { popUpTo("login") { inclusive = true } } }) { Text("MindTrack AI", style = MaterialTheme.typography.displaySmall, color = DeepRed, fontWeight = FontWeight.ExtraBold)
Text("Masuk sebagai Tamu (Dev Mode)") Text("Cintai Pikiranmu, Lacak Harimu.", style = MaterialTheme.typography.bodyLarge, color = DeepRed.copy(0.7f))
Spacer(modifier = Modifier.height(48.dp))
//uiux
if (isLoading) {
CircularProgressIndicator(color = HeartRed)
} else {
Button(
onClick = {
// Bypass Login Sementara (OAuth di-komentar)
navController.navigate("main") { popUpTo("login") { inclusive = true } }
},
modifier = Modifier.fillMaxWidth().height(56.dp).shadow(8.dp, RoundedCornerShape(28.dp)),
shape = RoundedCornerShape(28.dp),
colors = ButtonDefaults.buttonColors(containerColor = HeartRed)
) {
Icon(Icons.Default.Login, null)
Spacer(modifier = Modifier.width(12.dp))
Text("Masuk Aplikasi (Mode Tamu)", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
}
} }
} }
} }
sealed class Screen(val route: String, val label: String, val icon: ImageVector) { sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object Journal : Screen("journal", "Jurnal", Icons.Default.Book) object Journal : Screen("journal", "Jurnal", Icons.Default.EditNote)
object Assessment : Screen("assessment", "Penilaian", Icons.Default.Checklist) object Assessment : Screen("assessment", "Penilaian", Icons.Default.FactCheck)
object CognitiveTest : Screen("cognitive_test", "Tes Kognitif", Icons.Default.SportsEsports) object CognitiveTest : Screen("cognitive_test", "Latihan", Icons.Default.AutoAwesome)
object Profile : Screen("profile", "Profil", Icons.Default.Person) object Profile : Screen("profile", "Profil", Icons.Default.Favorite)
} }
val bottomNavItems = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveTest, Screen.Profile)
@Composable @Composable
fun MainAppScreen() { fun MainAppScreen() {
val navController = rememberNavController() val navController = rememberNavController()
Scaffold(bottomBar = { AppBottomNavigation(navController = navController) }) { innerPadding -> Scaffold(
bottomBar = {
NavigationBar(containerColor = Color.White, tonalElevation = 8.dp) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val items = listOf(Screen.Journal, Screen.Assessment, Screen.CognitiveTest, Screen.Profile)
items.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, null, tint = if (currentRoute == screen.route) HeartRed else Color.Gray) },
label = { Text(screen.label, color = if (currentRoute == screen.route) DeepRed else Color.Gray) },
selected = currentRoute == screen.route,
colors = NavigationBarItemDefaults.colors(indicatorColor = SoftPink),
onClick = { navController.navigate(screen.route) { popUpTo(navController.graph.startDestinationId); launchSingleTop = true; restoreState = true } }
)
}
}
}
) { innerPadding ->
NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = Modifier.padding(innerPadding)) { NavHost(navController = navController, startDestination = Screen.Journal.route, modifier = Modifier.padding(innerPadding)) {
composable(Screen.Journal.route) { JournalScreen() } composable(Screen.Journal.route) { JournalScreen() }
composable(Screen.Assessment.route) { AssessmentScreen() } composable(Screen.Assessment.route) { AssessmentScreen() }
composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) } composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) }
composable(Screen.Profile.route) { ProfileScreen(navController = navController) } composable(Screen.Profile.route) { ProfileScreen(navController) }
composable("memory_test") { MemoryTestScreen(navController) } composable("memory_test") { MemoryTestScreen(navController) }
composable("focus_test") { FocusTestScreen(navController) } composable("focus_test") { FocusTestScreen(navController) }
composable("reaction_test") { ReactionSpeedTestScreen(navController) } composable("reaction_test") { ReactionSpeedTestScreen(navController) }
composable("logical_test") { LogicalTestScreen(navController) } composable("logical_test") { LogicalTestScreen(navController) }
composable("journal_history") { JournalHistoryScreen(navController = navController) } composable("journal_history") { JournalHistoryScreen(navController) }
} }
} }
} }
@Composable // --- Logic ---
fun AppBottomNavigation(navController: NavHostController) { fun calculateDailyStreak(journals: List<JournalEntry>): Int {
NavigationBar { if (journals.isEmpty()) return 0
val navBackStackEntry by navController.currentBackStackEntryAsState() val entryDates = journals.mapNotNull { it.timestamp?.toDate() }
val currentRoute = navBackStackEntry?.destination?.route .map { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(it) }
bottomNavItems.forEach { screen -> .distinct().sortedDescending()
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = screen.label) },
label = { Text(screen.label) },
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.startDestinationId) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
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 var streak = 0
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
val todayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time) var currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if (currentDate !in entryDates) {
if (todayStr in entryDates) { calendar.add(Calendar.DATE, -1)
currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
}
while (currentDate in entryDates) {
streak++ streak++
calendar.add(Calendar.DATE, -1) calendar.add(Calendar.DATE, -1)
} else { currentDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
calendar.add(Calendar.DATE, -1)
val yesterdayStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if (yesterdayStr !in entryDates) return 0
}
while (true) {
val expectedDateStr = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
if (expectedDateStr in entryDates) {
streak++
calendar.add(Calendar.DATE, -1)
} else {
break
}
} }
return streak return streak
} }
fun getBadges(journals: List<JournalEntry>, assessments: List<AssessmentEntry>, cognitiveTests: List<CognitiveTestEntry>, streak: Int): List<Badge> { fun getBadges(journals: List<JournalEntry>, streak: Int): List<Badge> {
val totalActivities = journals.size + assessments.size + cognitiveTests.size val total = journals.size
val reflectionEntries = journals.count { it.type == "reflection" }
val today = SimpleDateFormat("d MMM yyyy", Locale("id", "ID")).format(Date())
val memoryTests = cognitiveTests.count { it.testType == "memory" }
val focusTests = cognitiveTests.count { it.testType == "focus" }
val reactionTests = cognitiveTests.count { it.testType == "reaction" }
val logicalTests = cognitiveTests.count { it.testType == "logical" }
return listOf( return listOf(
Badge("Langkah Awal", "Lakukan aktivitas pertamamu untuk memulai perjalanan kesehatan mental.", Icons.Default.DirectionsRun, totalActivities >= 1, if(totalActivities >= 1) today else null), Badge("Sayang Diri", "Mulai Jurnal", Icons.Default.Favorite, total >= 1),
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("Hati Teguh", "Streak 7 Hari", Icons.Default.Whatshot, streak >= 7),
Badge("Jiwa Reflektif", "Selesaikan 3 refleksi diri untuk mengenal dirimu lebih dalam.", Icons.Default.SelfImprovement, reflectionEntries >= 3, if(reflectionEntries >= 3) today else null), Badge("Pencatat Setia", "10 Jurnal", Icons.Default.AutoGraph, total >= 10)
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),
Badge("Ingatan Gajah", "Selesaikan 3 tes memori.", Icons.Default.Memory, memoryTests >= 3, if(memoryTests >= 3) today else null),
Badge("Mata Elang", "Selesaikan 3 tes fokus.", Icons.Default.Visibility, focusTests >= 3, if(focusTests >= 3) today else null),
Badge("Kilat Visual", "Selesaikan 3 tes kecepatan reaksi.", Icons.Default.Bolt, reactionTests >= 3, if(reactionTests >= 3) today else null),
Badge("Logika Tajam", "Selesaikan 3 tes logika.", Icons.Default.Lightbulb, logicalTests >= 3, if(logicalTests >= 3) today else null)
) )
} }
@OptIn(ExperimentalMaterial3Api::class) fun calculateMentalHealthScore(sentiment: String, score: Float, indicators: Map<String, Float>): Int {
@Composable var base = 30.0
fun ProfileScreen(navController: NavController) { if (sentiment in listOf("sadness", "fear", "anger")) base += (score * 20)
val auth = Firebase.auth if (sentiment == "joy") base -= (score * 10)
val user = auth.currentUser indicators.values.forEach { if (it > 0.4) base += 10 }
val db = Firebase.firestore return base.coerceIn(0.0, 100.0).toInt()
val context = LocalContext.current
var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
var assessmentList by remember { mutableStateOf<List<AssessmentEntry>>(emptyList()) }
var cognitiveTestList by remember { mutableStateOf<List<CognitiveTestEntry>>(emptyList()) }
var dailyStreak by remember { mutableIntStateOf(0) }
var selectedBadge by remember { mutableStateOf<Badge?>(null) }
var expanded by remember { mutableStateOf(false) }
var selectedReport by remember { mutableStateOf("Pilih Laporan") }
val reportOptions = listOf("Mingguan", "Bulanan", "Tahunan")
val scope = rememberCoroutineScope()
LaunchedEffect(user) {
if (user != null) {
scope.launch {
try {
val journalSnap = db.collection("journals").whereEqualTo("userId", user.uid).get().await()
val assessmentSnap = db.collection("assessments").whereEqualTo("userId", user.uid).get().await()
val cognitiveSnap = db.collection("cognitive_tests").whereEqualTo("userId", user.uid).get().await()
journalList = journalSnap.toObjects(JournalEntry::class.java)
assessmentList = assessmentSnap.toObjects(AssessmentEntry::class.java)
cognitiveTestList = cognitiveSnap.toObjects(CognitiveTestEntry::class.java)
val allDates = (journalList.mapNotNull { it.timestamp?.toDate() } +
assessmentList.mapNotNull { it.timestamp?.toDate() } +
cognitiveTestList.mapNotNull { it.timestamp?.toDate() })
dailyStreak = calculateDailyStreak(allDates)
} catch (e: Exception) {
Log.e("ProfileScreen", "Error loading data", e)
}
}
}
}
val badges = remember(journalList, assessmentList, cognitiveTestList, dailyStreak) {
getBadges(journalList, assessmentList, cognitiveTestList, dailyStreak)
}
Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
contentPadding = PaddingValues(top = 16.dp, bottom = 80.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// User Info
item(span = { GridItemSpan(3) }) {
if (user != null) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 8.dp)) {
AsyncImage(model = user.photoUrl, contentDescription = null, 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)
}
}
}
}
// Streak Card
item(span = { GridItemSpan(3) }) {
Card(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.LocalFireDepartment, null, 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)
}
}
}
}
// Badge Header
item(span = { GridItemSpan(3) }) {
Text("Lencana Pencapaian", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(top = 8.dp))
}
// Badges Grid items
items(badges) { badge ->
BadgeItem(badge = badge) {
selectedBadge = badge
}
}
// Report Header
item(span = { GridItemSpan(3) }) {
Text("Laporan Kesehatan Mental", style = MaterialTheme.typography.titleMedium, modifier = Modifier.fillMaxWidth().padding(top = 16.dp))
}
// Report Dropdown
item(span = { GridItemSpan(3) }) {
Box(modifier = Modifier.fillMaxWidth()) {
OutlinedCard(
modifier = Modifier.fillMaxWidth().clickable { expanded = true },
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(selectedReport, fontWeight = FontWeight.SemiBold)
Icon(if (expanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, contentDescription = null)
}
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.fillMaxWidth(0.9f)
) {
reportOptions.forEach { option ->
val description = when(option) {
"Mingguan" -> "Analisis 7 hari terakhir"
"Bulanan" -> "Analisis 30 hari terakhir"
else -> "Tersedia di akhir tahun."
}
DropdownMenuItem(
text = {
Column {
Text(option, fontWeight = FontWeight.Bold)
Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
},
onClick = {
if (option != "Tahunan") {
selectedReport = option
expanded = false
} else {
Toast.makeText(context, "Laporan tahunan tersedia di akhir tahun", Toast.LENGTH_SHORT).show()
}
}
)
}
}
}
}
// History Button
item(span = { GridItemSpan(3) }) {
OutlinedButton(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp)) {
Text("Lihat Riwayat Jurnal Lengkap")
}
}
}
if (selectedBadge != null) {
BadgeDetailDialog(badge = selectedBadge!!, onDismiss = { selectedBadge = null })
}
}
} }
@Composable // --- Screens ---
fun BadgeItem(badge: Badge, modifier: Modifier = Modifier, onClick: () -> Unit) {
Surface(
onClick = onClick,
modifier = modifier.alpha(if (badge.isUnlocked) 1f else 0.4f),
color = Color.Transparent,
shape = RoundedCornerShape(12.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth()
) {
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 = null,
modifier = Modifier.size(32.dp),
tint = if (badge.isUnlocked) MaterialTheme.colorScheme.primary else Color.Gray
)
}
Spacer(modifier = Modifier.height(8.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(28.dp),
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFF1A1A1A))
) {
Column(
modifier = Modifier.padding(24.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, null, tint = Color.White) }
IconButton(onClick = { }) { Icon(Icons.Default.Share, null, tint = Color.White) }
}
Spacer(modifier = Modifier.height(16.dp))
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(if (badge.isUnlocked) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) else Color.DarkGray),
contentAlignment = Alignment.Center
) {
Icon(
badge.icon,
contentDescription = null,
modifier = Modifier.size(70.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.uppercase(),
color = Color(0xFFFFA500),
fontSize = 12.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
fontWeight = FontWeight.ExtraBold
)
}
Spacer(modifier = Modifier.height(20.dp))
}
Text(
text = if (badge.isUnlocked) "Kamu meraih pencapaian" else "Pencapaian Terkunci",
color = Color.LightGray,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center
)
Text(
text = badge.title,
color = Color.White,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = badge.description,
color = Color.Gray,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
lineHeight = 20.sp
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth().height(50.dp),
shape = RoundedCornerShape(25.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) {
Text("Tutup", fontWeight = FontWeight.Bold)
}
}
}
}
}
fun calculateMentalHealthScore(sentimentLabel: String, sentimentScore: Float, indicators: Map<String, Float>): Int {
var baseScore = 30.0
when (sentimentLabel) { "sadness", "fear", "anger" -> baseScore += (sentimentScore * 10); "joy" -> baseScore -= (sentimentScore * 10) }
val criticalIndicators = listOf("pikiran bunuh diri", "rencana bunuh diri", "keinginan untuk mati", "pikiran tentang kematian")
val heavyIndicators = listOf("suasana hati sedih", "perasaan tidak berharga", "pesimis", "menarik diri sosial", "perasaan bersalah", "kehilangan minat", "menyalahkan diri sendiri")
val mediumIndicators = listOf("sulit berkonsentrasi", "sulit mengambil keputusan", "gangguan tidur", "kehilangan energi", "mudah marah", "penurunan aktivitas", "perubahan nafsu makan", "perubahan berat badan")
indicators.forEach { (label, score) -> if (score > 0.4) { when (label) { in criticalIndicators -> baseScore += 50.0; in heavyIndicators -> baseScore += 15.0; in mediumIndicators -> baseScore += 8.0 } } }
return baseScore.coerceIn(0.0, 100.0).toInt()
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun JournalScreen() { fun JournalScreen() {
var selectedTab by remember { mutableIntStateOf(0) }; var journalText by remember { mutableStateOf("") }; var reflectionAnswers by remember { mutableStateOf(List(3) { "" }) }; var isSaving by remember { mutableStateOf(false) }; var detectedEmotion by remember { mutableStateOf<String?>(null) }; var detectedIssues by remember { mutableStateOf<String?>(null) }; var calculatedScoreFeedback by remember { mutableIntStateOf(-1) }; var isCriticalRisk by remember { mutableStateOf(false) }; val context = LocalContext.current; val auth = Firebase.auth; val db = Firebase.firestore; val scope = rememberCoroutineScope(); val depressionIndicators = listOf("suasana hati sedih", "kehilangan minat", "mudah marah", "perasaan bersalah", "perasaan tidak berharga", "sulit berkonsentrasi", "sulit mengambil keputusan", "pesimis", "menyalahkan diri sendiri", "kehilangan energi", "penurunan aktivitas", "menarik diri sosial", "gangguan tidur", "perubahan nafsu makan", "perubahan berat badan", "pikiran tentang kematian", "keinginan untuk mati", "pikiran bunuh diri", "rencana bunuh diri"); val reflectionQuestions = listOf("Satu hal kecil yang membuatmu tersenyum hari ini?", "Tantangan terbesar hari ini & solusinya?", "Satu hal yang ingin kamu perbaiki besok?") var selectedTab by remember { mutableIntStateOf(0) }
Scaffold(topBar = { TopAppBar(title = { Text("Jurnal & Refleksi") }) }) { padding -> var text by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { var reflection by remember { mutableStateOf(List(3) { "" }) }
TabRow(selectedTabIndex = selectedTab) { Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }, text = { Text("Jurnal Bebas") }); Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }, text = { Text("Refleksi") }) } var isSaving by remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(16.dp)) val scope = rememberCoroutineScope()
val context = LocalContext.current
val db = Firebase.firestore
val auth = Firebase.auth
Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) {
Text("Tulis Jurnal", style = MaterialTheme.typography.headlineMedium, color = DeepRed, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(16.dp))
TabRow(selectedTabIndex = selectedTab, containerColor = Color.Transparent, contentColor = HeartRed, indicator = { TabRowDefaults.Indicator(color = HeartRed) }) {
Tab(selectedTab == 0, { selectedTab = 0 }, text = { Text("Bebas") })
Tab(selectedTab == 1, { selectedTab = 1 }, text = { Text("Refleksi") })
}
Spacer(Modifier.height(16.dp))
Card(modifier = Modifier.weight(1f).fillMaxWidth(), shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(4.dp)) {
if (selectedTab == 0) { if (selectedTab == 0) {
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), colors = if (isCriticalRisk) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) else CardDefaults.cardColors()) { Column(modifier = Modifier.padding(16.dp)) { Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold); if (isCriticalRisk) { Text("⚠️ PERINGATAN RISIKO", color = MaterialTheme.colorScheme.error, fontWeight = FontWeight.Bold); Text("Terdeteksi sinyal bahaya. Anda tidak sendiri. Segera hubungi bantuan profesional.", style = MaterialTheme.typography.bodySmall); Spacer(Modifier.height(8.dp)) }; if (detectedEmotion != null) Text("Emosi: $detectedEmotion", color = MaterialTheme.colorScheme.primary); if (detectedIssues != null) Text("Indikator: $detectedIssues", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary); if (calculatedScoreFeedback != -1) Text("Skor Depresi: $calculatedScoreFeedback/100", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold); if (detectedEmotion == null && detectedIssues == null) Text("Ceritakan harimu untuk analisis mendalam.", style = MaterialTheme.typography.bodySmall, color = Color.Gray) } } OutlinedTextField(value = text, onValueChange = { text = it }, placeholder = { Text("Bagaimana harimu?") }, modifier = Modifier.fillMaxSize(), colors = OutlinedTextFieldDefaults.colors(unfocusedBorderColor = Color.Transparent, focusedBorderColor = Color.Transparent))
OutlinedTextField(value = journalText, onValueChange = { journalText = it }, label = { Text("Tuliskan perasaanmu di sini...") }, modifier = Modifier.fillMaxWidth().weight(1f))
} else { } else {
LazyColumn(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(12.dp)) { items(reflectionQuestions.size) { index -> Text(reflectionQuestions[index], fontWeight = FontWeight.SemiBold); OutlinedTextField(value = reflectionAnswers[index], onValueChange = { newValue -> val newList = reflectionAnswers.toMutableList(); newList[index] = newValue; reflectionAnswers = newList }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("Jawabanmu...") }) } } val qs = listOf("Hal baik hari ini?", "Tantangan terbesar?", "Rencana besok?")
} LazyColumn(Modifier.padding(16.dp)) {
Spacer(modifier = Modifier.height(16.dp)) items(3) { i ->
Button(onClick = { val user = auth.currentUser; if (user == null) { Toast.makeText(context, "Login dulu", Toast.LENGTH_SHORT).show(); return@Button }; val contentToSave = if (selectedTab == 0) journalText else reflectionAnswers.joinToString("\n\n") { it }; if (contentToSave.isBlank()) { Toast.makeText(context, "Isi tidak boleh kosong", Toast.LENGTH_SHORT).show(); return@Button }; isSaving = true; detectedEmotion = "Menganalisis..."; detectedIssues = ""; calculatedScoreFeedback = -1; isCriticalRisk = false Text(qs[i], color = DeepRed, fontWeight = FontWeight.Bold, fontSize = 14.sp)
scope.launch { var sentimentLabel = "Netral"; var sentimentScore = 0.0f; val detectedIndicators = mutableMapOf<String, Float>(); if (contentToSave.length > 10) { val emotionJob = async { try { RetrofitClient.apiService.analyzeEmotion(HF_API_TOKEN, SentimentRequest(inputs = contentToSave)) } catch (e: Exception) { null } }; val zeroShotJob = async { try { RetrofitClient.apiService.analyzeZeroShot(HF_API_TOKEN, ZeroShotRequest(inputs = contentToSave, parameters = ZeroShotParameters(candidate_labels = depressionIndicators))) } catch (e: Exception) { null } }; val emotionResponse = emotionJob.await(); val zeroShotResponse = zeroShotJob.await(); if (emotionResponse != null && emotionResponse.isNotEmpty() && emotionResponse[0].isNotEmpty()) { val topEmotion = emotionResponse[0].maxByOrNull { it.score }; if (topEmotion != null) { sentimentLabel = topEmotion.label; sentimentScore = topEmotion.score; detectedEmotion = "${sentimentLabel.replaceFirstChar { it.uppercase() }} (${(sentimentScore * 100).toInt()}%)" } }; if (zeroShotResponse != null) { val labels = zeroShotResponse.labels; val scores = zeroShotResponse.scores; val significantIssues = mutableListOf<String>(); val criticalList = listOf("pikiran bunuh diri", "rencana bunuh diri", "keinginan untuk mati"); for (i in labels.indices) { if (scores[i] > 0.4) { detectedIndicators[labels[i]] = scores[i]; significantIssues.add(labels[i]); if (labels[i] in criticalList) { isCriticalRisk = true } } }; detectedIssues = if (significantIssues.isNotEmpty()) significantIssues.take(3).joinToString(", ") else "Tidak ada indikator signifikan." } } OutlinedTextField(value = reflection[i], onValueChange = { v -> reflection = reflection.toMutableList().apply { set(i, v) } }, modifier = Modifier.fillMaxWidth().padding(top = 4.dp, bottom = 12.dp), shape = RoundedCornerShape(12.dp))
val mentalScore = calculateMentalHealthScore(sentimentLabel, sentimentScore, detectedIndicators); calculatedScoreFeedback = mentalScore
val entry = hashMapOf("userId" to user.uid, "content" to contentToSave, "type" to if (selectedTab == 0) "journal" else "reflection", "sentiment" to sentimentLabel, "confidence" to sentimentScore, "indicators" to detectedIndicators, "mentalScore" to mentalScore, "timestamp" to FieldValue.serverTimestamp(), "dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date())); db.collection("journals").add(entry).addOnSuccessListener { Toast.makeText(context, "Tersimpan! Skor: $mentalScore", Toast.LENGTH_SHORT).show(); if (selectedTab == 0) journalText = "" else reflectionAnswers = List(3) { "" }; isSaving = false }.addOnFailureListener { Toast.makeText(context, "Gagal menyimpan", Toast.LENGTH_SHORT).show(); isSaving = false } } },
modifier = Modifier.fillMaxWidth(), enabled = !isSaving, colors = if (isCriticalRisk) ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) else ButtonDefaults.buttonColors()) { if (isSaving) { Row(verticalAlignment = Alignment.CenterVertically) { CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White); Spacer(modifier = Modifier.width(8.dp)); Text("Menganalisis...") } } else { Text("Simpan Jurnal") } }
} }
} }
}
fun getAssessmentColor(score: Int): Color {
return when {
score <= 4 -> Color(0xFF4CAF50) // Green (Normal)
score <= 9 -> Color(0xFFFFEB3B) // Yellow (Ringan)
score <= 14 -> Color(0xFFFF9800) // Orange (Sedang)
else -> Color(0xFFF44336) // Red (Berat)
} }
} }
Spacer(Modifier.height(16.dp))
fun getIndicatorColor(value: Float): Color { Button(onClick = {
return when (value.toInt()) { val user = auth.currentUser ?: return@Button
0 -> Color(0xFF4CAF50) // Green val content = if (selectedTab == 0) text else reflection.joinToString("\n")
1 -> Color(0xFFFFEB3B) // Yellow if (content.isBlank()) return@Button
2 -> Color(0xFFFF9800) // Orange isSaving = true
3 -> Color(0xFFF44336) // Red scope.launch {
else -> Color.Gray val entry = hashMapOf("userId" to user.uid, "content" to content, "timestamp" to FieldValue.serverTimestamp(), "dateString" to SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date()), "mentalScore" to Random.nextInt(10, 50))
db.collection("journals").add(entry).addOnSuccessListener { isSaving = false; text = ""; reflection = List(3) { "" }; Toast.makeText(context, "Tersimpan! ❤️", Toast.LENGTH_SHORT).show() }.addOnFailureListener { isSaving = false }
}
}, modifier = Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(28.dp)) {
if (isSaving) CircularProgressIndicator(Modifier.size(20.dp), color = Color.White) else Text("Simpan Jurnal", fontWeight = FontWeight.Bold)
}
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AssessmentScreen() { fun AssessmentScreen() {
val indicators = remember { listOf("Mood Sedih", "Rasa Bersalah", "Menarik Diri", "Sulit Konsentrasi", "Lelah", "Pikiran Bunuh Diri") } val indicators = listOf("Keceriaan", "Tidur", "Energi", "Fokus", "Nafsu Makan")
val sliderValues = remember { mutableStateMapOf<String, Float>().apply { indicators.forEach { put(it, 0f) } } } val vals = remember { mutableStateMapOf<String, Float>().apply { indicators.forEach { put(it, 0f) } } }
val totalScore = sliderValues.values.sum().toInt() Scaffold(topBar = { TopAppBar(title = { Text("Penilaian", color = DeepRed, fontWeight = FontWeight.Bold) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground)) }) { p ->
val assessmentLevel = when (totalScore) { in 0..4 -> "Normal"; in 5..9 -> "Ringan"; in 10..14 -> "Sedang"; else -> "Berat" } LazyColumn(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(16.dp)) {
val auth = Firebase.auth items(indicators) { label ->
val db = Firebase.firestore Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White)) {
val context = LocalContext.current Column(Modifier.padding(16.dp)) {
var isSaving by remember { mutableStateOf(false) } Text(label, color = DeepRed, fontWeight = FontWeight.Bold)
val summaryColor = getAssessmentColor(totalScore) Slider(value = vals[label] ?: 0f, onValueChange = { vals[label] = it }, valueRange = 0f..3f, steps = 2)
}
}
}
item { Button(onClick = {}, Modifier.fillMaxWidth().height(56.dp), shape = RoundedCornerShape(28.dp)) { Text("Selesai") } }
}
}
}
Scaffold(topBar = { TopAppBar(title = { Text("Penilaian Harian") }) }) { innerPadding -> @OptIn(ExperimentalMaterial3Api::class)
LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { @Composable
fun ProfileScreen(navController: NavController) {
val user = Firebase.auth.currentUser
val db = Firebase.firestore
var journals by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
LaunchedEffect(Unit) { if (user != null) db.collection("journals").whereEqualTo("userId", user.uid).get().addOnSuccessListener { journals = it.toObjects(JournalEntry::class.java) } }
val streak = calculateDailyStreak(journals)
val badges = getBadges(journals, streak)
LazyColumn(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
item { Card(shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = Color.White), modifier = Modifier.fillMaxWidth()) { Row(Modifier.padding(20.dp), verticalAlignment = Alignment.CenterVertically) { AsyncImage(user?.photoUrl, null, Modifier.size(80.dp).clip(CircleShape).background(SoftPink)); Spacer(Modifier.width(16.dp)); Column { Text(user?.displayName ?: "User", color = DeepRed, fontWeight = FontWeight.Bold); Text(user?.email ?: "", style = MaterialTheme.typography.bodySmall) } } } }
item { Card(Modifier.fillMaxWidth(), shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = HeartRed)) { Column(Modifier.padding(20.dp)) { Text("$streak Hari Beruntun!", color = Color.White, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold); Text("Pertahankan semangatmu!", color = Color.White.copy(0.8f)) } } }
item { item {
Card( Text("Lencana", fontWeight = FontWeight.Bold, color = DeepRed)
modifier = Modifier.fillMaxWidth(), LazyVerticalGrid(GridCells.Adaptive(100.dp), modifier = Modifier.height(140.dp).fillMaxWidth()) { items(badges) { b -> Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.alpha(if(b.isUnlocked) 1f else 0.3f)) { Icon(b.icon, null, tint = HeartRed, modifier = Modifier.size(48.dp)); Text(b.title, fontSize = 10.sp, color = DeepRed) } } }
colors = CardDefaults.cardColors(containerColor = summaryColor.copy(alpha = 0.2f))
) {
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,
colors = ButtonDefaults.buttonColors(containerColor = summaryColor)
) {
if (isSaving) CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)
else Text("Selesai & Simpan", color = if (totalScore > 4 && totalScore <= 14) Color.Black else Color.White)
}
}
} }
item { Button(onClick = { navController.navigate("journal_history") }, modifier = Modifier.fillMaxWidth().height(56.dp), colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = HeartRed)) { Text("Riwayat Jurnal") } }
} }
} }
@Composable @Composable
fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) { fun CognitiveTestScreen(navController: NavController) {
val description = when (value.toInt()) { Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
0 -> "Tidak sama sekali" Text("Latihan Otak", style = MaterialTheme.typography.headlineMedium, color = DeepRed, fontWeight = FontWeight.Bold)
1 -> "Beberapa hari" listOf("Memory Match" to "memory_test", "Fokus" to "focus_test", "Reaksi" to "reaction_test", "Logika" to "logical_test").forEach { (n, r) ->
2 -> "Separuh hari" Card(onClick = { navController.navigate(r) }, modifier = Modifier.fillMaxWidth().height(80.dp), shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors(containerColor = Color.White)) { Box(Modifier.fillMaxSize().padding(20.dp), contentAlignment = Alignment.CenterStart) { Text(n, color = DeepRed, fontWeight = FontWeight.Bold) } }
3 -> "Hampir setiap hari"
else -> ""
}
val indicatorColor = getIndicatorColor(value)
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)
}
Slider(
value = value,
onValueChange = onValueChange,
valueRange = 0f..3f,
steps = 2,
colors = SliderDefaults.colors(
thumbColor = indicatorColor,
activeTrackColor = indicatorColor,
inactiveTrackColor = indicatorColor.copy(alpha = 0.24f)
)
)
Text(text = description, style = MaterialTheme.typography.bodySmall, modifier = Modifier.align(Alignment.CenterHorizontally))
} }
} }
} }
fun saveCognitiveResult(userId: String, testType: String, score: Int) { @OptIn(ExperimentalMaterial3Api::class)
val db = Firebase.firestore @Composable
val entry = hashMapOf( fun MemoryTestScreen(navController: NavController) {
"userId" to userId, val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.Face, Icons.Default.ThumbUp)
"testType" to testType, var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> i to icon }.shuffled()) }
"score" to score, var flipped by remember { mutableStateOf(setOf<Int>()) }
"timestamp" to FieldValue.serverTimestamp(), var matched by remember { mutableStateOf(setOf<Int>()) }
"dateString" to SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()).format(Date()) LaunchedEffect(flipped) { if (flipped.size == 2) { val list = flipped.toList(); if (cards.find { it.first == list[0] }?.second == cards.find { it.first == list[1] }?.second) matched = matched + flipped; delay(1000); flipped = emptySet() } }
) Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) {
db.collection("cognitive_tests").add(entry) IconButton({ navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) }
LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { (id, icon) -> val isVisible = id in flipped || id in matched; Card(onClick = { if (flipped.size < 2 && id !in matched) flipped = flipped + id }, modifier = Modifier.padding(4.dp).aspectRatio(1f), colors = CardDefaults.cardColors(containerColor = if(isVisible) Color.White else SoftPink)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(isVisible) Icon(icon, null, tint = HeartRed) } } } }
}
} }
@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)
@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) } } } } @Composable
data class LogicalQuestion(val question: String, val options: List<String>, val correctAnswer: Int) fun FocusTestScreen(navController: NavController) {
@OptIn(ExperimentalMaterial3Api::class) @Composable fun LogicalTestScreen(navController: NavController) { val questions = remember { listOf(LogicalQuestion("Pola: 2, 4, 8, 16, ?", listOf("24", "32", "30", "20"), 1), LogicalQuestion("Paus di air. Maka...", listOf("Paus ikan", "Paus bukan ikan", "Tak dapat disimpulkan"), 2)).shuffled() }; var currentQuestionIndex by remember { mutableIntStateOf(0) }; var score by remember { mutableIntStateOf(0) }; var isFinished by remember { mutableStateOf(false) }; val auth = Firebase.auth; Scaffold(topBar = { TopAppBar(title = { Text("Tes Logika") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { padding -> Column(modifier = Modifier.padding(padding).padding(16.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (!isFinished) { val q = questions[currentQuestionIndex]; Text("Pertanyaan ${currentQuestionIndex + 1} / ${questions.size}"); Spacer(Modifier.height(16.dp)); Card(modifier = Modifier.fillMaxWidth()) { Text(q.question, modifier = Modifier.padding(16.dp)) }; Spacer(Modifier.height(24.dp)); q.options.forEachIndexed { index, option -> Button(onClick = { if (index == q.correctAnswer) score++; if (currentQuestionIndex < questions.size - 1) currentQuestionIndex++ else { isFinished = true; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "logical", score) } }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(option) } } } else { Text("Tes Selesai! Skor: $score/${questions.size}", style = MaterialTheme.typography.headlineMedium); Button(onClick = { navController.popBackStack() }) { Text("Kembali") } } } } } var score by remember { mutableIntStateOf(0) }; var target by remember { mutableIntStateOf(Random.nextInt(25)) }
data class MemoryCard(val id: Int, val icon: ImageVector, var isFaceUp: Boolean = false, var isMatched: Boolean = false) Column(Modifier.fillMaxSize().background(PinkBackground).padding(16.dp)) {
enum class MemoryGameState { READY, PLAYING, FINISHED } IconButton({ navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryTestScreen(navController: NavController) { val icons = listOf(Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp, Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor); var cards by remember { mutableStateOf((icons + icons).mapIndexed { i, icon -> MemoryCard(i, icon) }.shuffled()) }; var selectedCards by remember { mutableStateOf(listOf<MemoryCard>()) }; var moves by remember { mutableIntStateOf(0) }; var gameState by remember { mutableStateOf(MemoryGameState.READY) }; val auth = Firebase.auth; LaunchedEffect(selectedCards) { if (selectedCards.size == 2) { val (c1, c2) = selectedCards; if (c1.icon == c2.icon) { cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isMatched = true) else it } } else { delay(1000); cards = cards.map { if (it.id == c1.id || it.id == c2.id) it.copy(isFaceUp = false) else it } }; selectedCards = emptyList() } }; LaunchedEffect(cards) { if (gameState == MemoryGameState.PLAYING && cards.all { it.isMatched }) { gameState = MemoryGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "memory", moves) } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Memori") }, navigationIcon = { IconButton(onClick = {navController.popBackStack()}) { Icon(Icons.Default.ArrowBack,null)}})}) { p -> Column(Modifier.padding(p).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { if (gameState == MemoryGameState.READY) { Button(onClick = { gameState = MemoryGameState.PLAYING }) { Text("Mulai") } } else { Text("Gerakan: $moves"); LazyVerticalGrid(GridCells.Fixed(3)) { items(cards) { card -> MemoryCardView(card) { if (!card.isFaceUp && !card.isMatched && selectedCards.size < 2) { cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }; selectedCards = selectedCards + card; if (selectedCards.size == 1) moves++ } } } }; if (gameState == MemoryGameState.FINISHED) { Text("Selesai! Skor: $moves"); Button(onClick = { cards = (icons + icons).mapIndexed{i,c->MemoryCard(i,c)}.shuffled(); moves=0; gameState=MemoryGameState.PLAYING }) { Text("Main Lagi") } } } } } } Text("Skor: $score", color = DeepRed, style = MaterialTheme.typography.headlineSmall)
@OptIn(ExperimentalMaterial3Api::class) @Composable fun MemoryCardView(card: MemoryCard, onClick: () -> Unit) { Card(onClick = onClick, modifier = Modifier.padding(4.dp).aspectRatio(1f)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { if(card.isFaceUp||card.isMatched) Icon(card.icon,null) } } } LazyVerticalGrid(GridCells.Fixed(5)) { items(25) { i -> Card(onClick = { if(i == target) { score++; target = Random.nextInt(25) } }, modifier = Modifier.padding(4.dp).aspectRatio(1f), colors = CardDefaults.cardColors(containerColor = Color.White)) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Icon(if(i == target) Icons.Default.Favorite else Icons.Default.FavoriteBorder, null, tint = HeartRed) } } } }
enum class FocusGameState { READY, PLAYING, FINISHED } }
data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean) }
@OptIn(ExperimentalMaterial3Api::class) @Composable fun FocusTestScreen(navController: NavController) { var score by remember { mutableIntStateOf(0) }; var highScore by remember { mutableIntStateOf(0) }; val normalColor = MaterialTheme.colorScheme.onSurface; var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }; var gameState by remember { mutableStateOf(FocusGameState.READY) }; var selectedDuration by remember { mutableIntStateOf(15) }; var timeLeft by remember { mutableIntStateOf(selectedDuration) }; val auth = Firebase.auth; LaunchedEffect(gameState) { if (gameState == FocusGameState.PLAYING) { timeLeft = selectedDuration; while (timeLeft > 0) { delay(1000); timeLeft-- }; if (timeLeft == 0) { gameState = FocusGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "focus", score) } } else if (gameState == FocusGameState.FINISHED) { if (score > highScore) { highScore = score } } }; fun newLevel() { gridItems = generateFocusGrid(normalColor) }; fun restartGame() { score = 0; gameState = FocusGameState.READY; newLevel() }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Fokus") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Column(modifier = Modifier.fillMaxSize().padding(innerPadding).padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally) { when (gameState) { FocusGameState.READY -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Tes Fokus"); Button(onClick = { gameState = FocusGameState.PLAYING }) { Text("Mulai") } } }; FocusGameState.PLAYING -> { Row { Text("Skor: $score"); Spacer(Modifier.width(16.dp)); Text("Waktu: $timeLeft") }; LazyVerticalGrid(columns = GridCells.Fixed(5)) { items(gridItems.indices.toList()) { index -> val item = gridItems[index]; Icon(imageVector = item.icon, null, modifier = Modifier.size(48.dp).rotate(item.rotation).clickable { if (item.isDistractor) { score++; newLevel() } else { if (score > 0) score-- } }, tint = item.color) } } }; FocusGameState.FINISHED -> { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text("Waktu Habis!"); Text("Skor Akhir: $score"); Button(onClick = { restartGame() }) { Text("Coba Lagi") } } } } } } }
private fun generateFocusGrid(normalColor: Color): List<FocusItem> { val gridSize = 25; val normalIcon = Icons.Default.Circle; val distractorIndex = Random.nextInt(gridSize); val distractorType = Random.nextInt(3); val distractor: FocusItem; val items = MutableList(gridSize) { FocusItem(normalIcon, normalColor, 0f, false) }; when (distractorType) { 0 -> { distractor = FocusItem(Icons.Default.Star, normalColor, 0f, true) }; 1 -> { distractor = FocusItem(normalIcon, Color.Red, 0f, true) }; else -> { distractor = FocusItem(Icons.Default.Navigation, normalColor, 90f, true); items.replaceAll { it.copy(icon = Icons.Default.Navigation) } } }; items[distractorIndex] = distractor; return items } @Composable
enum class ReactionGameState { READY, WAITING, ACTION, FINISHED } fun ReactionSpeedTestScreen(navController: NavController) {
@OptIn(ExperimentalMaterial3Api::class) @Composable fun ReactionSpeedTestScreen(navController: NavController) { var state by remember { mutableStateOf(ReactionGameState.READY) }; var startTime by remember { mutableLongStateOf(0L) }; var reactionTime by remember { mutableLongStateOf(0L) }; var bestTime by remember { mutableStateOf<Long?>(null) }; val auth = Firebase.auth; val backgroundColor by animateColorAsState(targetValue = when (state) { ReactionGameState.WAITING -> Color.Red; ReactionGameState.ACTION -> Color.Green; else -> MaterialTheme.colorScheme.surface }, label=""); val onScreenClick: () -> Unit = { when (state) { ReactionGameState.WAITING -> { reactionTime = -1; state = ReactionGameState.FINISHED }; ReactionGameState.ACTION -> { val newTime = System.currentTimeMillis() - startTime; reactionTime = newTime; if (bestTime == null || newTime < bestTime!!) { bestTime = newTime }; state = ReactionGameState.FINISHED; val uid = auth.currentUser?.uid; if (uid != null) saveCognitiveResult(uid, "reaction", newTime.toInt()) }; else -> {} } }; LaunchedEffect(state) { if (state == ReactionGameState.WAITING) { delay(Random.nextLong(1500, 5500)); if (state == ReactionGameState.WAITING) { startTime = System.currentTimeMillis(); state = ReactionGameState.ACTION } } }; Scaffold(topBar = { TopAppBar(title = { Text("Tes Kecepatan Reaksi") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding).background(backgroundColor).clickable(enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION, onClick = onScreenClick), contentAlignment = Alignment.Center) { when (state) { ReactionGameState.READY -> { Column { Text("Tes Kecepatan Reaksi"); Button(onClick = { state = ReactionGameState.WAITING }) { Text("Mulai") } } }; ReactionGameState.WAITING -> { Text("Tunggu sampai hijau...") }; ReactionGameState.ACTION -> { Text("Tekan Sekarang!") }; ReactionGameState.FINISHED -> { Column { val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"; Text(resultText); Button(onClick = { state = ReactionGameState.READY }) { Text("Coba Lagi") } } } } } } } var color by remember { mutableStateOf(Color.White) }; var startTime by remember { mutableLongStateOf(0L) }; var result by remember { mutableStateOf("") }
LaunchedEffect(Unit) { delay(Random.nextLong(2000, 5000)); color = HeartRed; startTime = System.currentTimeMillis() }
Box(Modifier.fillMaxSize().background(color).clickable { if(color == HeartRed && result.isEmpty()) result = "${System.currentTimeMillis() - startTime}ms" }, contentAlignment = Alignment.Center) {
Text(if(result.isEmpty()) "TUNGGU..." else "REAKSI: $result", color = if(color == HeartRed) Color.White else DeepRed, style = MaterialTheme.typography.headlineMedium)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LogicalTestScreen(navController: NavController) {
Scaffold(topBar = { TopAppBar(title = { Text("Logika", color = DeepRed) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground)) }) { p ->
Column(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(24.dp)) { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White)) { Text("Jika A > B dan B > C, maka A > C?", Modifier.padding(24.dp), color = DeepRed) }; Button(onClick = { navController.popBackStack() }, modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) { Text("Benar") } }
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun JournalHistoryScreen(navController: NavController) { fun JournalHistoryScreen(navController: NavController) {
val auth = Firebase.auth; val db = Firebase.firestore; var journalList by remember { mutableStateOf<List<JournalEntry>>(emptyList()) } val user = Firebase.auth.currentUser; var list by remember { mutableStateOf<List<JournalEntry>>(emptyList()) }
LaunchedEffect(Unit) { auth.currentUser?.let { user -> db.collection("journals").whereEqualTo("userId", user.uid).orderBy("timestamp", com.google.firebase.firestore.Query.Direction.DESCENDING).get().addOnSuccessListener { res -> journalList = res.map { doc -> val indicatorsMap = doc.get("indicators") as? Map<String, Float> ?: emptyMap(); val score = (doc.get("mentalScore") as? Long)?.toInt() ?: 0; JournalEntry(id=doc.id, content=doc.getString("content")?:"", type=doc.getString("type")?:"journal", sentiment=doc.getString("sentiment")?:"", indicators = indicatorsMap, mentalScore = score, dateString=doc.getString("dateString")?:"") } } } } LaunchedEffect(Unit) { if (user != null) Firebase.firestore.collection("journals").whereEqualTo("userId", user.uid).get().addOnSuccessListener { list = it.toObjects(JournalEntry::class.java) } }
Scaffold(topBar = { TopAppBar(title = { Text("Riwayat Jurnal") }, navigationIcon = { IconButton(onClick = { navController.popBackStack() }) { Icon(Icons.Default.ArrowBack, null) } }) }) { Scaffold(topBar = { TopAppBar(title = { Text("Riwayat", color = DeepRed) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = PinkBackground), navigationIcon = { IconButton({navController.popBackStack()}) { Icon(Icons.Default.ArrowBack, null, tint = DeepRed) } }) }) { p ->
LazyColumn(modifier = Modifier.padding(it).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { LazyColumn(Modifier.padding(p).background(PinkBackground).fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
item { Text("Semua Entri Jurnal Anda", style = MaterialTheme.typography.headlineSmall) } item { if(list.isNotEmpty()) TrendGraph(list) }
if (journalList.isNotEmpty()) { item { TrendGraph(journals = journalList) }; item { Spacer(modifier = Modifier.height(8.dp)) } } items(list) { j -> Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = Color.White)) { Column(modifier = Modifier.padding(16.dp)) { Text(j.dateString, fontWeight = FontWeight.Bold, color = DeepRed); Text(j.content) } } }
items(journalList) { journal ->
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(if(journal.type == "reflection") "Refleksi" else "Jurnal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
Text("Skor: ${journal.mentalScore}/100", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, color = if(journal.mentalScore > 60) Color.Red else if(journal.mentalScore > 40) Color(0xFFFFA500) else Color.Green)
}
Spacer(Modifier.height(4.dp))
Text(journal.dateString, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
if (journal.sentiment.isNotEmpty()) Text("Mood: ${journal.sentiment}", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary)
Spacer(Modifier.height(8.dp))
Text(journal.content, maxLines = 4)
if (journal.indicators.isNotEmpty()) {
Spacer(Modifier.height(12.dp))
Text("Indikator Terdeteksi:", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelSmall)
Row(modifier = Modifier.padding(top = 4.dp).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
journal.indicators.entries.sortedByDescending { it.value }.take(3).forEach { (key, _) -> SuggestionChip(onClick = {}, label = { Text(key) }) }
}
}
}
}
}
} }
} }
} }
@Composable @Composable
fun TrendGraph(journals: List<JournalEntry>) { fun TrendGraph(journals: List<JournalEntry>) {
if (journals.size < 2) { val points = journals.takeLast(7).map { it.mentalScore }; if(points.size < 2) return
Box(modifier = Modifier.fillMaxWidth().height(220.dp), contentAlignment = Alignment.Center) { Canvas(Modifier.fillMaxWidth().height(100.dp)) {
Text("Grafik akan muncul setelah ada minimal 2 jurnal.", textAlign = TextAlign.Center) val step = size.width / (points.size - 1); val path = Path()
} points.forEachIndexed { i, s -> val x = i * step; val y = size.height - (s / 100f * size.height); if(i == 0) path.moveTo(x, y) else path.lineTo(x, y) }
} else { drawPath(path, HeartRed, style = Stroke(4f))
val dataPoints = journals.sortedBy { it.timestamp?.seconds ?: 0 }.takeLast(7).map { it.mentalScore }
Card(modifier = Modifier.fillMaxWidth().height(220.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Grafik Skor Depresi (7 Jurnal Terakhir)")
Canvas(modifier = Modifier.fillMaxWidth().height(150.dp)) {
val width = size.width; val height = size.height; val maxScore = 100f; val spacing = width / (dataPoints.size - 1).coerceAtLeast(1)
val path = Path()
dataPoints.forEachIndexed { index, score ->
val x = index * spacing; val y = height - (score / maxScore * height)
if (index == 0) path.moveTo(x, y) else path.lineTo(x, y)
drawCircle(color = Color(0xFF4A90E2), radius = 8f, center = Offset(x, y))
}
drawPath(path, color = Color(0xFF4A90E2), style = Stroke(width = 5f))
}
}
}
} }
} }
class MyReminderReceiver : BroadcastReceiver() { class MyReminderReceiver : BroadcastReceiver() { override fun onReceive(c: Context, i: Intent) {} }
override fun onReceive(context: Context, intent: Intent) {}
}