diff --git a/app/build.gradle b/app/build.gradle index 20d3f6d..1ad2ab9 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 44 - versionName "3.0.1" + versionCode 45 + versionName "3.0.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index f3a0380..c15c6db 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -214,7 +214,7 @@ interface APICommunity { @Path("username") username: String, ): Call - @GET("/api/v3/timetable/emptyClassRooms") + @GET("/api/v3/users/emptyClassRooms") fun getEmptyClassrooms( @Header("Authorization") authToken: String, @Query("slot") slot: String, diff --git a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt index 2c97b80..f82d7df 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/emptyclassrooms/EmptyClassroomsContent.kt @@ -1,5 +1,8 @@ package com.dscvit.vitty.ui.emptyclassrooms +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -7,12 +10,15 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -25,6 +31,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar @@ -33,6 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -45,6 +55,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -73,6 +84,8 @@ fun EmptyClassroomsContent( var isLoading by remember { mutableStateOf(true) } var selectedSlot by remember { mutableStateOf("A1") } var errorMessage by remember { mutableStateOf(null) } + var showReportDialog by remember { mutableStateOf(false) } + var selectedClassroomForReport by remember { mutableStateOf(null) } val regularSlots = listOf( @@ -153,6 +166,17 @@ fun EmptyClassroomsContent( } }, actions = { + Box { + IconButton(onClick = { + showReportDialog = true + }) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Report Incorrect Data", + tint = TextColor, + ) + } + } IconButton(onClick = { fetchEmptyClassrooms(selectedSlot) }) { Icon( imageVector = Icons.Default.Refresh, @@ -296,6 +320,20 @@ fun EmptyClassroomsContent( } } } + + // Report Dialog + if (showReportDialog) { + ReportIncorrectDataDialog( + availableClassrooms = emptyClassrooms, + selectedSlot = selectedSlot, + context = context, + prefs = prefs, + onDismiss = { + showReportDialog = false + selectedClassroomForReport = null + } + ) + } } @Composable @@ -377,3 +415,234 @@ private fun ClassroomCard(classroom: String) { } } } + +@Composable +private fun ReportIncorrectDataDialog( + availableClassrooms: List, + selectedSlot: String, + context: Context, + prefs: android.content.SharedPreferences, + onDismiss: () -> Unit +) { + val username = prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + val name = prefs.getString(Constants.COMMUNITY_NAME, "") ?: "" + val campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "Unknown" + + var selectedClassroom by remember { mutableStateOf(null) } + var showOverlay by remember { mutableStateOf(true) } + + val currentDate = remember { + java.text.SimpleDateFormat("MMM dd, yyyy 'at' hh:mm a", java.util.Locale.getDefault()) + .format(java.util.Date()) + } + + fun openEmailClient(classroom: String) { + val emailBody = """ +Dear VITTY Support Team, + +I would like to report incorrect classroom data: + +REPORT DETAILS: +- Reported Classroom: $classroom +- Time Slot: $selectedSlot +- Date & Time: $currentDate +- Campus: ${campus.capitalize()} + +USER INFORMATION: +- Username: $username +- Name: $name + +ISSUE DESCRIPTION: +The classroom "$classroom" is listed as empty for slot $selectedSlot, but it appears to be occupied or incorrectly marked. + +Please verify and update the classroom availability data. + +Thank you for your attention to this matter. + +Best regards, +$name +VITTY Android App + """.trimIndent() + + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_EMAIL, arrayOf("dscvit.vitty@gmail.com")) + putExtra(Intent.EXTRA_SUBJECT, "Report Incorrect Classroom Data - $classroom ($selectedSlot)") + putExtra(Intent.EXTRA_TEXT, emailBody) + } + + try { + context.startActivity(Intent.createChooser(emailIntent, "Send Email")) + onDismiss() + } catch (e: Exception) { + // Fallback if no email client available + val fallbackIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, emailBody) + } + context.startActivity(Intent.createChooser(fallbackIntent, "Share Report")) + onDismiss() + } + } + + if (showOverlay) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Background.copy(alpha = 0.5f)) + .clickable { + showOverlay = false + onDismiss() + }, + contentAlignment = Alignment.Center + ) { + AlertDialog( + onDismissRequest = { + showOverlay = false + onDismiss() + }, + icon = { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = Accent, + modifier = Modifier.size(32.dp) + ) + }, + title = { + Text( + text = "Report Incorrect Data", + style = MaterialTheme.typography.headlineSmall, + color = TextColor, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + }, + text = { + Column { + Text( + text = "Select the classroom that is incorrectly marked as empty for slot $selectedSlot", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Select Classroom:", + style = MaterialTheme.typography.titleSmall, + color = TextColor, + fontWeight = FontWeight.SemiBold + ) + + Spacer(modifier = Modifier.height(12.dp)) + + if (availableClassrooms.isEmpty()) { + Text( + text = "No classrooms available for slot $selectedSlot", + style = MaterialTheme.typography.bodyMedium, + color = Accent, + textAlign = TextAlign.Center + ) + } else { + LazyColumn( + modifier = Modifier.height(200.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(availableClassrooms) { classroom -> + ClassroomSelectionItem( + classroom = classroom, + isSelected = selectedClassroom == classroom, + onClick = { selectedClassroom = classroom } + ) + } + } + } + } + }, + confirmButton = { + Button( + onClick = { + selectedClassroom?.let { classroom -> + openEmailClient(classroom) + } + }, + enabled = selectedClassroom != null, + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background, + disabledContainerColor = Accent.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Send Report", + fontWeight = FontWeight.SemiBold + ) + } + }, + dismissButton = { + OutlinedButton( + onClick = onDismiss, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = TextColor + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Cancel", + fontWeight = FontWeight.Medium + ) + } + }, + containerColor = Secondary, + titleContentColor = TextColor, + textContentColor = Accent + ) + } + } +} + +@Composable +private fun ClassroomSelectionItem( + classroom: String, + isSelected: Boolean, + onClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() }, + colors = CardDefaults.cardColors( + containerColor = if (isSelected) Accent.copy(alpha = 0.2f) else Background + ), + shape = RoundedCornerShape(8.dp), + border = if (isSelected) BorderStroke(2.dp, Accent) else null + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .background( + if (isSelected) Accent else TextColor.copy(alpha = 0.3f), + CircleShape + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = classroom, + style = MaterialTheme.typography.bodyMedium, + color = TextColor, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal + ) + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 2593f15..a072f9f 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -3,6 +3,7 @@ package com.dscvit.vitty.ui.main import android.app.Activity import android.content.Intent import android.content.SharedPreferences +import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring @@ -30,6 +31,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card @@ -44,6 +46,10 @@ import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -62,6 +68,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -1132,6 +1139,7 @@ fun DrawerContent( val name = remember { prefs.getString(Constants.COMMUNITY_NAME, "") ?: "Name" } var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } + var showSupportDialog by remember { mutableStateOf(false) } DisposableEffect(Unit) { val listener = @@ -1216,15 +1224,36 @@ fun DrawerContent( ) }, label = { - Text( - modifier = Modifier.padding(start = 24.dp), - text = "Find Empty Classroom", - color = TextColor, - style = - MaterialTheme.typography.labelLarge.copy( - fontWeight = FontWeight.Normal, - ), - ) + Column( + modifier = Modifier.padding(start = 24.dp) + ) { + Text( + text = "Find Empty Classroom", + color = TextColor, + style = + MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Normal, + ), + ) + Spacer(modifier = Modifier.height(4.dp)) + Box( + modifier = Modifier + .background( + Accent, + RoundedCornerShape(6.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = "BETA", + color = Background, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.ExtraBold + ), + fontSize = 10.sp + ) + } + } }, selected = false, onClick = { @@ -1237,6 +1266,11 @@ fun DrawerContent( selectedContainerColor = Secondary, ), ) + } else { + Timber.d("DrawerContent: ❌ HIDING Empty Classroom option") + Timber.d("DrawerContent: Campus '$campus' is not 'vellore'") + Timber.d("DrawerContent: Campus is empty: ${campus.isEmpty()}") + Timber.d("DrawerContent: Campus is blank: ${campus.isBlank()}") } NavigationDrawerItem( @@ -1338,7 +1372,7 @@ fun DrawerContent( selected = false, onClick = { onCloseDrawer() - UtilFunctions.openLink(context, context.getString(R.string.telegram_link)) + showSupportDialog = true }, colors = NavigationDrawerItemDefaults.colors( @@ -1387,6 +1421,15 @@ fun DrawerContent( ) } } + + // Support Dialog + if (showSupportDialog) { + SupportDialog( + context = context, + prefs = prefs, + onDismiss = { showSupportDialog = false } + ) + } } private suspend fun loadCachedCourses(prefs: SharedPreferences): List = @@ -1444,3 +1487,212 @@ private fun extractCoursesFromTimetable(userResponse: UserResponse): List Unit +) { + val username = prefs.getString(Constants.COMMUNITY_USERNAME, "") ?: "" + val name = prefs.getString(Constants.COMMUNITY_NAME, "") ?: "" + val campus = prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "Unknown" + + val deviceInfo = remember { + """ + **Device Information:** + - Device: ${Build.MODEL} + - Manufacturer: ${Build.MANUFACTURER} + - OS: Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) + - App Version: ${context.packageManager.getPackageInfo(context.packageName, 0).versionName} + + **User Information:** + - Username: $username + - Name: $name + - Campus: $campus + """.trimIndent() + } + + fun openEmailSupport() { + val emailTemplate = """ +Dear VITTY Support Team, + +I am experiencing an issue with the VITTY Android app and would like to report it. + +$deviceInfo + +**Describe the bug** +[Please provide a clear and concise description of what the bug is] + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behaviour** +[Please provide a clear and concise description of what you expected to happen] + +**Screenshots** +[If applicable, please attach screenshots to help explain your problem] + +**Additional context** +[Add any other context about the problem here] + +Thank you for your time and assistance! + +Best regards, +$name +VITTY Android App User + """.trimIndent() + + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_EMAIL, arrayOf("dscvit.vitty@gmail.com")) + putExtra(Intent.EXTRA_SUBJECT, "Bug Report - VITTY Android v${context.packageManager.getPackageInfo(context.packageName, 0).versionName}") + putExtra(Intent.EXTRA_TEXT, emailTemplate) + } + + try { + context.startActivity(Intent.createChooser(emailIntent, "Send Bug Report")) + onDismiss() + } catch (e: Exception) { + // Fallback if no email client available + val fallbackIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, emailTemplate) + } + context.startActivity(Intent.createChooser(fallbackIntent, "Share Bug Report")) + onDismiss() + } + } + + fun openGitHub() { + UtilFunctions.openLink(context, "https://github.com/GDGVIT/vitty-app/issues") + onDismiss() + } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_support), + contentDescription = null, + tint = Accent, + modifier = Modifier.size(32.dp) + ) + }, + title = { + Text( + text = "Get Support", + style = MaterialTheme.typography.headlineSmall, + color = TextColor, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + }, + text = { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Troubleshooting Steps:", + style = MaterialTheme.typography.titleMedium, + color = TextColor, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(12.dp)) + + val troubleshootingSteps = listOf( + "1. Update the app from Play Store", + "2. Log out and log back in", + "3. Clear app cache (Settings > Apps > Vitty > Storage)" + ) + + troubleshootingSteps.forEach { step -> + Text( + text = step, + style = MaterialTheme.typography.bodyMedium, + color = TextColor.copy(alpha = 0.9f), + fontSize = 15.sp, + lineHeight = 20.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "Still need help?", + style = MaterialTheme.typography.titleSmall, + color = Accent, + fontWeight = FontWeight.SemiBold + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Email Support Button + Button( + onClick = { openEmailSupport() }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp), + elevation = ButtonDefaults.buttonElevation(defaultElevation = 2.dp) + ) { + Text( + text = "Email Support", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + // GitHub Issues Button + OutlinedButton( + onClick = { openGitHub() }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Accent + ), + border = BorderStroke(2.dp, Accent), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "GitHub Issues", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + } + }, + confirmButton = {}, + dismissButton = { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors( + containerColor = Secondary, + contentColor = TextColor + ), + shape = RoundedCornerShape(12.dp), + border = BorderStroke(1.dp, TextColor.copy(alpha = 0.2f)) + ) { + Text( + text = "Close", + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp + ) + } + }, + containerColor = Secondary, + titleContentColor = TextColor, + textContentColor = Accent + ) +}