diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8468bbf..329d86d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -3,6 +3,8 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
+ id("com.google.dagger.hilt.android")
+ kotlin("plugin.serialization").version("2.2.21")
}
android {
@@ -27,11 +29,12 @@ android {
"proguard-rules.pro"
)
- buildConfigField("String", "BASE_API_URL", "\"https://videochat-signaling-app.ue.r.appspot.com\"")
+ buildConfigField("String", "BASE_API_URL", "\"https://meechie.techkit.xyz\"")
buildConfigField("String", "BASE_WS_API_URL", "\"wss://videochat-signaling-app.ue.r.appspot.com\"")
- buildConfigField("int", "PORT", "443")
+ buildConfigField("int", "PORT", "444")
buildConfigField("boolean", "SECURE", "true") // yes use HTTPS
- buildConfigField("String", "API_POST_URL","\"https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post\"")
+ buildConfigField("String", "API_POST_URL","\"https://meechie.techkit.xyz:444/key=peerjs/post\"")
+ buildConfigField("String", "API_GET_ID_URL", "\"https://meechie.techkit.xyz:444/key=peerjs/id\"")
buildConfigField("String", "API_GET_PEERS_URL","\"https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/peers\"")
signingConfig = signingConfigs.getByName("debug")
}
@@ -43,11 +46,12 @@ android {
"proguard-rules.pro"
)
- buildConfigField("String", "BASE_API_URL", "\"https://videochat-signaling-app.ue.r.appspot.com\"")
+ buildConfigField("String", "BASE_API_URL", "\"https://meechie.techkit.xyz\"")
buildConfigField("String", "BASE_WS_API_URL", "\"wss://videochat-signaling-app.ue.r.appspot.com\"")
- buildConfigField("int", "PORT", "443")
+ buildConfigField("int", "PORT", "444")
buildConfigField("boolean", "SECURE", "true") // yes use HTTPS
- buildConfigField("String", "API_POST_URL","\"https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post\"")
+ buildConfigField("String", "API_POST_URL","\"https://meechie.techkit.xyz:444/key=peerjs/post\"")
+ buildConfigField("String", "API_GET_ID_URL", "\"https://meechie.techkit.xyz:444/key=peerjs/id\"")
buildConfigField("String", "API_GET_PEERS_URL","\"https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/peers\"")
signingConfig = signingConfigs.getByName("debug")
}
@@ -62,14 +66,18 @@ android {
applicationIdSuffix = ".local" // e.g., com.example.myapp.local
versionNameSuffix = "-local"
- // Override BASE_API_URL to point to your local development server
+// Override BASE_API_URL to point to your local development server
// 10.0.2.2 is the special IP for your host machine's loopback on Android Emulator
- buildConfigField("String", "BASE_API_URL", "\"localhost\"")
- buildConfigField("String", "BASE_WS_API_URL", "\"ws://localhost:9000\"")
- buildConfigField("int", "PORT", "9000")
- buildConfigField("boolean", "SECURE", "false") // no use HTTPS
- buildConfigField("String", "API_POST_URL","\"https://localhost:9000/key=peerjs/post\"")
- buildConfigField("String", "API_GET_PEERS_URL","\"http://localhost:9000/key=peerjs/peers\"")
+ buildConfigField("String", "BASE_API_URL", "\"https://10.0.2.2\"") // Use http for local unless you have SSL set up for this IP
+ buildConfigField("String", "BASE_WS_API_URL", "\"ws://10.0.2.2:3016\"")
+ buildConfigField("int", "PORT", "3016")
+ buildConfigField("boolean", "SECURE", "false") // It's common to use http (not https) for local IP access
+
+ // --- THIS IS THE FIX ---
+ // Replace 127.0.0.1 with 10.0.2.2
+ buildConfigField("String", "API_POST_URL", "\"http://10.0.2.2:3016/key=peerjs/post\"")
+ buildConfigField("String", "API_GET_PEERS_URL", "\"http://10.0.2.2:3016/key=peerjs/peers\"")
+ // ------------------------
// If using a physical device on your local network, replace with your actual local IP:
// buildConfigField("String", "BASE_API_URL", "\"http://192.168.1.XX:8080\"")
@@ -100,9 +108,15 @@ android {
}
dependencies {
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
+ implementation("com.google.dagger:hilt-android:2.57.1")
+ implementation(libs.androidx.runtime)
+ ksp("com.google.dagger:hilt-android-compiler:2.57.2")
+ implementation ("com.github.0-u-0:mediasoup-android-sdk:0.0.1")
+ implementation("com.github.0-u-0:dugon-webrtc-android:100.0.2")
+ implementation("io.socket:socket.io-client:2.1.1")
implementation ("com.github.franmontiel:PersistentCookieJar:v1.0.1")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")
- implementation("io.github.webrtc-sdk:android:125.6422.06.1")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.room:room-runtime:2.5.0")
implementation(libs.androidx.camera.core)
diff --git a/app/src/debug/res/AndroidManifest.xml b/app/src/debug/res/AndroidManifest.xml
new file mode 100644
index 0000000..6320ce2
--- /dev/null
+++ b/app/src/debug/res/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c925b25..a2873a7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,7 +15,9 @@
-
+
-
+
diff --git a/app/src/main/java/com/example/.DS_Store b/app/src/main/java/com/example/.DS_Store
index 5242d05..fb6c37c 100644
Binary files a/app/src/main/java/com/example/.DS_Store and b/app/src/main/java/com/example/.DS_Store differ
diff --git a/app/src/main/java/com/example/neurology_project_android/AppModule.kt b/app/src/main/java/com/example/neurology_project_android/AppModule.kt
new file mode 100644
index 0000000..bf3f52b
--- /dev/null
+++ b/app/src/main/java/com/example/neurology_project_android/AppModule.kt
@@ -0,0 +1,39 @@
+package com.example.neurology_project_android
+
+// In: app/src/main/java/com/example/neurology_project_android/di/AppModule.kt
+// (It's good practice to put modules in a 'di' package)
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.example.neurology_project_android.SessionManager
+import com.example.neurology_project_android.SignalingRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class) // These dependencies will live as long as the app
+object AppModule {
+
+ @Provides
+ @Singleton // Use @Singleton to ensure only one instance is ever created
+ fun provideOkHttpClient(@ApplicationContext context: Context): OkHttpClient {
+ // Hilt provides the application context for us
+ return SessionManager(context).client
+ }
+
+
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Provides // <-- ADD THIS: Tells Hilt this function PROVIDES a dependency.
+ @Singleton
+ fun provideSignalingClient(@ApplicationContext context: Context): SignalingClient {
+ return SignalingClient(context)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/neurology_project_android/CallScreenActivity.kt b/app/src/main/java/com/example/neurology_project_android/CallScreenActivity.kt
index be935f6..1d28dd2 100644
--- a/app/src/main/java/com/example/neurology_project_android/CallScreenActivity.kt
+++ b/app/src/main/java/com/example/neurology_project_android/CallScreenActivity.kt
@@ -1,9 +1,11 @@
package com.example.neurology_project_android
+import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
@@ -15,6 +17,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.sp
class CallScreenActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -28,7 +33,7 @@ class CallScreenActivity : ComponentActivity() {
@Composable
fun CallScreen() {
var isMuted by remember { mutableStateOf(false) }
-
+ val context = LocalContext.current
Box(
modifier = Modifier
.fillMaxSize()
@@ -49,46 +54,188 @@ fun CallScreen() {
.padding(16.dp)
.padding(bottom = 20.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
- verticalAlignment = Alignment.CenterVertically
+
) {
- IconButton(
- onClick = { /* Open Stroke Scale Form */ },
- modifier = Modifier
- .size(64.dp)
- .background(Color.Gray, shape = CircleShape)
+ Column(
+ // Center the items horizontally within the column
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f).background(Color.DarkGray).clickable(onClick = {
+ val intent = Intent(
+ context,
+ ListNIHFormActivity::class.java
+ )
+
+ context.startActivity(intent)
+ /* Open Stroke Scale Form */
+ })
+
) {
- Icon(
- painter = painterResource(id = android.R.drawable.ic_menu_edit),
- contentDescription = null,
- tint = Color.White
+ IconButton(
+ onClick = {
+ val intent = Intent(
+ context,
+ ListNIHFormActivity::class.java
+ )
+ // 3. Start the new Activity
+ context.startActivity(intent)
+ /* Open Stroke Scale Form */ },
+ modifier = Modifier
+ .size(64.dp)
+ .background(Color.DarkGray)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.assignment_add_24px),
+ contentDescription = null,
+ tint = Color.White
+ )
+ }
+ // Add the Text composable right below the IconButton
+ Spacer(modifier = Modifier.height(4.dp)) // Optional: Add a small vertical space
+ Text(
+ text = "New Form/View Form",
+ modifier = Modifier.fillMaxWidth(),
+
+
+ fontSize = 16.sp,
+
+
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
+
+
+ color = Color.White,
+
+ textAlign = TextAlign.Center,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis
)
}
- IconButton(
- onClick = { isMuted = !isMuted },
- modifier = Modifier
- .size(64.dp)
- .background(Color.DarkGray, shape = CircleShape)
+ Column(
+ // Center the items horizontally within the column
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f).background(Color.Gray).clickable(onClick = {
+
+ })
+
) {
- Icon(
- painter = painterResource(id = if (isMuted) android.R.drawable.ic_lock_silent_mode else android.R.drawable.ic_lock_silent_mode_off),
+ IconButton(
+ onClick = {
+ if(isMuted){
+ isMuted = false
+ }
+ else{
+ isMuted = true
+ }
+
+ },
+ modifier = Modifier
+ .size(64.dp)
+ .background(Color.Gray)
+ ) {
+ Icon(
+ painter = painterResource(id = if (isMuted) R.drawable.mic_off_24px else R.drawable.mic_24px),
contentDescription = null,
tint = Color.White
)
+ }
+ // Add the Text composable right below the IconButton
+ Spacer(modifier = Modifier.height(4.dp)) // Optional: Add a small vertical space
+ Text(
+ text = "Mute/Unmute",
+ modifier = Modifier.fillMaxWidth(),
+
+
+ fontSize = 16.sp,
+
+
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
+
+
+ color = Color.White,
+
+ textAlign = TextAlign.Center,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis
+ )
}
- IconButton(
- onClick = { /* End Call */ },
- modifier = Modifier
- .size(64.dp)
- .background(Color.Red, shape = CircleShape)
+ Column(
+ // Center the items horizontally within the column
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f).background(Color.Red).clickable(onClick = {
+ val intent = Intent(context, NewNIHFormActivity::class.java).apply {
+ putExtra(NewNIHFormActivity.EXTRA_IN_CALL, true)
+ }
+ context.startActivity(intent)
+
+ /* Open Stroke Scale Form */
+ })
+
) {
- Icon(
- painter = painterResource(id = android.R.drawable.ic_menu_close_clear_cancel),
+ IconButton(
+ onClick = {
+ val intent = Intent(context, NewNIHFormActivity::class.java).apply {
+ putExtra(NewNIHFormActivity.EXTRA_IN_CALL, true)
+ }
+ context.startActivity(intent)
+
+ /* Open Stroke Scale Form */ },
+ modifier = Modifier
+ .size(64.dp)
+ .background(Color.Red)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.call_end_24px),
contentDescription = null,
tint = Color.White
)
+ }
+ // Add the Text composable right below the IconButton
+ Spacer(modifier = Modifier.height(4.dp)) // Optional: Add a small vertical space
+ Text(
+ text = "End Call",
+ modifier = Modifier.fillMaxWidth(),
+
+
+ fontSize = 16.sp,
+
+
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
+
+
+ color = Color.White,
+
+ textAlign = TextAlign.Center,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis
+ )
}
+
+// IconButton(
+// onClick = { isMuted = !isMuted },
+// modifier = Modifier
+// .size(64.dp)
+// .background(Color.DarkGray, shape = CircleShape)
+// ) {
+// Icon(
+// painter = painterResource(id = if (isMuted) R.drawable.mic_off_24px else R.drawable.mic_24px),
+// contentDescription = null,
+// tint = Color.White
+// )
+// }
+//
+// IconButton(
+// onClick = { /* End Call */ },
+// modifier = Modifier
+// .size(64.dp)
+// .background(Color.Red, shape = CircleShape)
+// ) {
+// Icon(
+// painter = painterResource(R.drawable.call_end_24px),
+// contentDescription = null,
+// tint = Color.White
+// )
+// }
}
}
}
diff --git a/app/src/main/java/com/example/neurology_project_android/FormManager.kt b/app/src/main/java/com/example/neurology_project_android/FormManager.kt
index a392576..4d24e18 100644
--- a/app/src/main/java/com/example/neurology_project_android/FormManager.kt
+++ b/app/src/main/java/com/example/neurology_project_android/FormManager.kt
@@ -1,56 +1,98 @@
package com.example.neurology_project_android
import android.util.Log
-import kotlinx.coroutines.CoroutineScope
+import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
+import java.util.UUID
object FormManager {
+ private const val POST_URL = "https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post"
+ var TAG = "FormManager"
fun submitFormToServer(form: NIHForm, client: OkHttpClient, onResult: (Boolean) -> Unit) {
- val json = JSONObject().apply {
- put("patientName", form.patientName)
- put("patientDob", form.dob)
- put("formDate", form.date)
- put("results", form.formData)
- put("username", form.username)
- }
-
+ val jsonString = Gson().toJson(form)
+ Log.d(TAG, "values are " + form.toString())
val requestBody = RequestBody.create(
"application/json; charset=utf-8".toMediaTypeOrNull(),
- json.toString()
+ jsonString
)
val request = Request.Builder()
- .url("https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post")
+ .url(POST_URL)
.post(requestBody)
.addHeader("Content-Type", "application/json")
- .addHeader("Action", "submitStrokeScale")
+ .addHeader("Action", "start_new_nihss_form")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
+ Log.e(TAG, e.toString())
onResult(false)
}
override fun onResponse(call: Call, response: Response) {
+ Log.d(TAG, response.toString())
onResult(response.isSuccessful)
}
})
}
+ suspend fun loadForm(formId: String, client: OkHttpClient): NIHForm = withContext(Dispatchers.IO) {
+ Log.d(TAG, "formid is ${formId}")
+
+ val json = JSONObject().apply {
+ put("form_id", formId)
+ }
+
+ val requestBody = RequestBody.create(
+ "application/json; charset=utf-8".toMediaTypeOrNull(),
+ json.toString())
+ val request = Request.Builder()
+ .url(POST_URL)
+ .post(requestBody)
+ .addHeader("Content-Type", "application/json")
+ .addHeader("Action", "requestSingleForm")
+ .build()
+
+ try {
+ // 🔑 Fix 2: Keep the synchronous execute() call, but it's now safe
+ // because it's wrapped in withContext(Dispatchers.IO).
+ try {
+ val response = client.newCall(request).execute()
+ if (response.isSuccessful) {
+
+ val bodyString = response.body?.string().toString()
+ Log.d(TAG, "forms reponse ${bodyString}")
+ val formsList = decodeSingleForm(bodyString!!)
+ return@withContext formsList
+ } else {
+ Log.e("FORM_MANAGER", "Server error: ${response.code}")
+ }
+ } catch (e: Exception) {
+ Log.e("FORM_MANAGER", "Error opening form: ${e.message}")
+ }
+ } catch (e: Exception) {
+ Log.e("FORM_MANAGER", "Error opening form: ${e.message}")
+ }
+
+ // Return null instead of an empty NIHForm() on failure
+ return@withContext NIHForm()
+ }
+
suspend fun fetchFormsForUser(
username: String,
client: OkHttpClient
): List = withContext(Dispatchers.IO) {
val forms = mutableListOf()
-
+ Log.d(TAG, "FETCHING FORMS FOR USER")
val json = JSONObject().apply {
put("username", username)
}
@@ -61,7 +103,7 @@ object FormManager {
)
val request = Request.Builder()
- .url("https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post")
+ .url(POST_URL)
.post(requestBody)
.addHeader("Content-Type", "application/json")
.addHeader("Action", "getUsersForms")
@@ -70,33 +112,22 @@ object FormManager {
try {
val response = client.newCall(request).execute()
if (response.isSuccessful) {
+
val bodyString = response.body?.string()
- val jsonArray = JSONArray(bodyString)
-
- for (i in 0 until jsonArray.length()) {
- val item = jsonArray.getJSONObject(i)
- forms.add(
- NIHForm(
- id = item.getInt("id"),
- patientName = item.getString("patient_name"),
- dob = item.getString("patient_dob"),
- date = item.getString("form_date"),
- formData = item.getString("results"),
- username = item.getString("username")
- )
- )
- }
+ Log.d(TAG, "forms reponse ${bodyString}")
+ val formsList = decodeMultipleForms(bodyString!!)
+ return@withContext formsList
} else {
Log.e("FORM_MANAGER", "Server error: ${response.code}")
}
- } catch (e: IOException) {
- Log.e("FORM_MANAGER", "Network error: ${e.message}")
+ } catch (e: Exception) {
+ Log.e("FORM_MANAGER", "Error opening form: ${e.message}")
}
return@withContext forms
}
- fun deleteForm(formId: Int, username: String, client: OkHttpClient, callback: (Boolean) -> Unit) {
+ fun deleteForm(formId: UUID, username: String, client: OkHttpClient, callback: (Boolean) -> Unit) {
val json = JSONObject().apply {
put("id", formId)
put("username", username)
@@ -108,7 +139,7 @@ object FormManager {
)
val request = Request.Builder()
- .url("https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post")
+ .url(POST_URL)
.post(requestBody)
.addHeader("Content-Type", "application/json")
.addHeader("Action", "deleteForm")
@@ -129,12 +160,12 @@ object FormManager {
fun updateForm(form: NIHForm, client: OkHttpClient, onComplete: (Boolean) -> Unit) {
val json = JSONObject().apply {
- put("id", form.id)
- put("patientName", form.patientName)
- put("patientDob", form.dob)
- put("formDate", form.date)
- put("results", form.formData)
- put("username", form.username)
+// put("id", form.id)
+// put("patientName", form.patientName)
+// put("patientDob", form.dob)
+// put("formDate", form.date)
+// put("results", form.formData)
+// put("username", form.username)
}
val requestBody = RequestBody.create(
@@ -143,7 +174,7 @@ object FormManager {
)
val request = Request.Builder()
- .url("https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/post")
+ .url(POST_URL)
.post(requestBody)
.addHeader("Content-Type", "application/json")
.addHeader("Action", "updateForm")
@@ -168,4 +199,50 @@ object FormManager {
}
}
}
+
+ fun decodeMultipleForms(jsonString: String): List {
+ val jsonParser = Json {
+ // Essential for robustness: ignore fields present in JSON but not in your data class
+ ignoreUnknownKeys = true
+ // Handles cases where null is present for a property with a default value
+ coerceInputValues = true
+ }
+ Log.e(TAG, "Decoding forms")
+ return try {
+ // The key change: specify List as the target type
+ val formList: List = jsonParser.decodeFromString(jsonString)
+ formList
+ } catch (e: Exception) {
+ // Log the error and return an empty list or throw a custom exception
+ println("Error decoding list of forms: $e")
+ emptyList()
+ }
+ }
+
+ fun decodeSingleForm(jsonString: String): NIHForm {
+ val jsonParser = Json {
+ ignoreUnknownKeys = true
+ coerceInputValues = true
+ }
+
+ Log.e(TAG, "Attempting to decode single form from array.")
+
+ return try {
+ // 1. Decode as a List because the JSON starts with '['
+ val formList: List = jsonParser.decodeFromString(jsonString)
+
+ // 2. Safely return the first element of the list
+ if (formList.isNotEmpty()) {
+ formList.first()
+ } else {
+ Log.e(TAG, "Successfully decoded array, but the list was empty.")
+ NIHForm() // Return a default empty form if the list is empty
+ }
+
+ } catch (e: Exception) {
+ // This handles exceptions like malformed JSON or other parsing errors
+ Log.e(TAG, "Error decoding single form: ${e.message}")
+ NIHForm() // Return a default empty form on failure
+ }
+ }
}
diff --git a/app/src/main/java/com/example/neurology_project_android/ListNIHFormActivity.kt b/app/src/main/java/com/example/neurology_project_android/ListNIHFormActivity.kt
index 23bfa1d..aa7d4bd 100644
--- a/app/src/main/java/com/example/neurology_project_android/ListNIHFormActivity.kt
+++ b/app/src/main/java/com/example/neurology_project_android/ListNIHFormActivity.kt
@@ -87,16 +87,20 @@ fun ListNIHFormScreen(refreshTrigger: Int) {
) {
items(savedForms) { form ->
SavedFormItem(
- form = SavedForm(form.patientName, form.date),
+ form = SavedForm(
+ patientName = form.patientName?.takeIf { it.isNotBlank() } ?: "Unnamed Patient",
+ dateRecorded = form.formDate ?: "No Date" // Also handle null date for safety
+ ),
onClick = {
- val intent = Intent(context, SavedNIHFormActivity::class.java).apply {
- putExtra("formId", form.id)
- putExtra("patientName", form.patientName)
- putExtra("dob", form.dob)
- putExtra("date", form.date)
- putExtra("formData", form.formData)
- putExtra("username", form.username)
+ val intent = Intent(context, NewSavedNIHFormActivity::class.java).apply {
+ putExtra("form", form.form_id)
+// putExtra("patientName", form.patientName)
+// putExtra("dob", form.dob)
+// putExtra("date", form.date)
+// putExtra("formData", form.formData)
+// putExtra("username", form.username)
}
+
context.startActivity(intent)
}
)
diff --git a/app/src/main/java/com/example/neurology_project_android/LoginActivity.kt b/app/src/main/java/com/example/neurology_project_android/LoginActivity.kt
index 8997ad5..e6fc8d6 100644
--- a/app/src/main/java/com/example/neurology_project_android/LoginActivity.kt
+++ b/app/src/main/java/com/example/neurology_project_android/LoginActivity.kt
@@ -150,8 +150,11 @@ fun LoginScreen(sessionManager: SessionManager, onLoginSuccess: () -> Unit) {
override fun onResponse(call: Call, response: Response) {
val success = response.isSuccessful
val bodyString = response.body?.string()?.trim() ?: ""
- var authToken = response.headers.value(9)?.substringAfter("authorization=")?.substringBefore(";") ?: ""
+ var authToken = response.headers.get("Set-Cookie")
+ ?.substringBefore(";") ?: ""
+ Log.d("LOGINACTIVITY:", "RESPONSE HEADERS: " + response.headers.get("Set-Cookie"))
+ Log.d("LOGINACTIVITY:", "AUTH TOKEN: " + authToken)
if (success) {
sessionManager.saveAuthToken(authToken, username)
(context as ComponentActivity).runOnUiThread {
diff --git a/app/src/main/java/com/example/neurology_project_android/MainActivity.kt b/app/src/main/java/com/example/neurology_project_android/MainActivity.kt
index f6f8d2c..565061e 100644
--- a/app/src/main/java/com/example/neurology_project_android/MainActivity.kt
+++ b/app/src/main/java/com/example/neurology_project_android/MainActivity.kt
@@ -4,39 +4,49 @@ package com.example.neurology_project_android
import androidx.compose.ui.platform.LocalContext
import android.Manifest
+import android.R
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
+import com.example.neurology_project_android.BuildConfig.API_GET_ID_URL
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.camera.core.imagecapture.CameraRequest
+import androidx.compose.foundation.background
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.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -45,41 +55,40 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.media3.common.util.Log
import androidx.media3.common.util.UnstableApi
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import com.example.neurology_project_android.BuildConfig.BASE_WS_API_URL
-import com.example.neurology_project_android.BuildConfig.PORT
+import com.example.neurology_project_android.BuildConfig.API_GET_PEERS_URL
import com.example.neurology_project_android.ui.theme.NeurologyProjectAndroidTheme
-import okhttp3.Call
-import okhttp3.Callback
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
import okhttp3.OkHttpClient
import okhttp3.Request
-import okhttp3.Response
-import okio.IOException
-import org.webrtc.CapturerObserver
-import org.webrtc.VideoProcessor
-import org.webrtc.VideoSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+
+@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@SuppressLint("RestrictedApi")
private lateinit var cameraRequest: CameraRequest
- private lateinit var videoProcessor: VideoProcessor
- private lateinit var videoSource: VideoSource
- private lateinit var capturerObserver: CapturerObserver
+// private lateinit var videoProcessor: VideoProcessor
+// private lateinit var videoSource: VideoSource
+// private lateinit var capturerObserver: CapturerObserver
private var isInCall by mutableStateOf(false)
private var cameraInitialized by mutableStateOf(false)
private lateinit var signalingClient: SignalingClient
+ private lateinit var signalingRepository: SignalingRepository
+ private val viewModel: MainViewModel by viewModels()
+ @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -100,66 +109,37 @@ class MainActivity : ComponentActivity() {
), 1
)
-
+
enableEdgeToEdge()
setContent {
NeurologyProjectAndroidTheme {
- val userIdState = remember { mutableStateOf(null) }
- val peersState = remember { mutableStateOf>(emptyList()) }
-
- // Fetch user ID once
- LaunchedEffect(Unit) {
- val fetchedId = fetchUserId()
- userIdState.value = fetchedId
-
- // Now safe to start SignalingClient
- signalingClient = SignalingClient(
- "$BASE_WS_API_URL:$PORT/peerjs?id=$fetchedId&token=6789&key=peerjs",
- this@MainActivity,
- fetchedId,
- onCallRecieved = { isInCall = true },
- onCallEnded = { runOnUiThread { isInCall = false } }
- )
-
- // Fetch peers
- GetPeers { peers ->
- runOnUiThread {
- peersState.value = peers.filter { it != fetchedId }
+ viewModel.connectSignalingClient()
+ val uiState by viewModel.uiState.collectAsState()
+ // 3. OBSERVE state from the ViewModel
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ when (val state = uiState) {
+ is MainUiState.Loading -> {
+ Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator()
+ }
}
- }
- }
-
- val userId = userIdState.value
-
- if (userId == null) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- CircularProgressIndicator()
- }
- } else {
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- containerColor = Color.Transparent,
- content = { innerPadding ->
- HomeScreen(
- modifier = Modifier.padding(innerPadding),
- peerId = userId,
- peers = peersState.value
- )
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding),
- signalingClient = signalingClient,
- cameraInitialized = cameraInitialized,
- cameraRequest = { cameraRequest },
- isInCall = isInCall
+ is MainUiState.Error -> {
+ Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
+ Text("Error: ${state.message}")
+ }
+ }
+ is MainUiState.Success -> {
+ // Pass the state down to your UI
+ MyApp(
+ userId = state.userId,
+ peers = state.peers,
+ innerPadding = innerPadding,
+ onJoinRoom = { targetId -> viewModel.joinRoom(targetId) }
)
}
- )
+ }
}
}
}
@@ -167,7 +147,21 @@ class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
- fun OnlineNowSection(peers: List) {
+ fun OnlineNowSection(peers: List, onNavigateToOnlineScreen: () -> Unit) {
+
+
+// LaunchedEffect(isInCall) {
+// if (isInCall) {
+// navController.navigate("callScreen")
+// } else {
+// navController.navigate("home") // Navigate back when call ends
+// }
+// }
+
+
+
+
+
Text(
text = "Online Now:",
fontWeight = FontWeight.Bold,
@@ -179,7 +173,7 @@ class MainActivity : ComponentActivity() {
Text(text = "No peers online", modifier = Modifier.padding(16.dp))
} else {
peers.forEach { userId ->
- OnlineUserCard(userId)
+ OnlineUserCard(userId, onNavigateToOnlineScreen)
}
}
}
@@ -187,7 +181,8 @@ class MainActivity : ComponentActivity() {
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
- fun OnlineUserCard(userId: String) {
+ fun OnlineUserCard(userId: String, onNavigateToOnlineScreen: () -> Unit) {
+ var navController = rememberNavController()
Card(
modifier = Modifier
.fillMaxWidth()
@@ -209,7 +204,11 @@ class MainActivity : ComponentActivity() {
.padding(end = 16.dp)
)
Button(
- onClick = { signalingClient.startCall(userId) },
+ onClick = {
+
+ signalingClient.joinRoom(userId)
+ onNavigateToOnlineScreen()
+ }, //signalingClient.startCall(userId)
modifier = Modifier.wrapContentWidth()
) {
Text(text = "Call")
@@ -218,15 +217,47 @@ class MainActivity : ComponentActivity() {
}
}
+ // All your other @Composable functions should be updated to accept the data and lambdas they need
+ // For example, MyApp now takes the userId, peers, and onJoinRoom lambda
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Composable
+ fun MyApp(
+ userId: String,
+ peers: List,
+ innerPadding: PaddingValues,
+ onJoinRoom: (String) -> Unit // FIX #1: This is a simple function type, not @Composable
+ ) {
+ val navController = rememberNavController()
+ NavHost(navController, startDestination = "home") {
+ composable("home") {
+ HomeScreen(
+ modifier = Modifier.padding(innerPadding),
+ userId = userId, // Pass state down
+ peers = peers, // Pass state down
+ onJoinRoom = onJoinRoom, // Pass the event handler down
+ onNavigateToCallScreen = { navController.navigate("callScreen") }
+ )
+ }
+ composable("callScreen") {
+ // Assuming CallScreen is defined elsewhere
+ CallScreen()
+ }
+ }
+ }
+
+
+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
- fun HomeScreen(modifier: Modifier = Modifier, peerId: String, peers: List) {
+ fun HomeScreen(
+ modifier: Modifier = Modifier,
+ userId: String,
+ peers: List,
+ onJoinRoom: (String) -> Unit,
+ onNavigateToCallScreen: () -> Unit
+ ) {
val context = LocalContext.current
val sessionManager = remember { SessionManager(context) }
- // Refresh UI every 3 seconds
- LaunchedEffect(peers) {
- // This will trigger recomposition whenever peers update
- }
Column(
modifier = modifier
@@ -234,27 +265,24 @@ class MainActivity : ComponentActivity() {
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Row(
- modifier = Modifier
- .fillMaxWidth(),
- horizontalArrangement = Arrangement.End
- ) {
- TextButton(
- onClick = {
- sessionManager.logout()
- val intent = Intent(context, LoginActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
+ // Logout Button
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
+ TextButton(onClick = {
+ sessionManager.logout()
+ val intent = Intent(context, LoginActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
- ) {
+ context.startActivity(intent)
+ }) {
Text("Log Out", color = MaterialTheme.colorScheme.primary)
}
}
-
Spacer(modifier = Modifier.height(8.dp))
- PeerIdSection(peerId) // Displays the correct Peer ID
+ // Peer ID Section
+ PeerIdSection(peerId = userId)
+ // Online Now Section (Scrollable)
Column(
modifier = Modifier
.fillMaxWidth()
@@ -262,18 +290,25 @@ class MainActivity : ComponentActivity() {
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
- OnlineNowSection(peers) // No need for additional state
+ OnlineNowSection(
+ peers = peers,
+ onJoinRoom = onJoinRoom,
+ onNavigateToCallScreen = onNavigateToCallScreen
+ )
}
+ // NIH Forms Button
NIHFormsButton()
}
}
+
+
}
suspend fun fetchUserId(): String {
return withContext(Dispatchers.IO) {
try {
- val idUrl = "https://videochat-signaling-app.ue.r.appspot.com/key=peerjs/id"
+ val idUrl = API_GET_ID_URL
val client = OkHttpClient()
val request = Request.Builder().url(idUrl).build()
val response = client.newCall(request).execute()
@@ -290,38 +325,65 @@ suspend fun fetchUserId(): String {
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
-fun Greeting(
- name: String,
- modifier: Modifier = Modifier,
- signalingClient: SignalingClient,
- cameraInitialized: Boolean,
- @SuppressLint("RestrictedApi") cameraRequest: () -> CameraRequest,
- isInCall: Boolean
+fun OnlineNowSection(
+ peers: List,
+ onJoinRoom: (String) -> Unit,
+ onNavigateToCallScreen: () -> Unit
) {
- val navController = rememberNavController()
-
- LaunchedEffect(isInCall) {
- if (isInCall) {
- navController.navigate("callScreen")
- } else {
- navController.navigate("home") // Navigate back when call ends
+ Text(
+ text = "Online Now:",
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ if (peers.isEmpty()) {
+ Text(text = "No peers online", modifier = Modifier.padding(16.dp))
+ } else {
+ peers.forEach { peerId ->
+ OnlineUserCard(
+ userId = peerId,
+ onJoinClick = {
+ // FIX #2: Chain the events. Call ViewModel then navigate.
+ onJoinRoom(peerId)
+ onNavigateToCallScreen()
+ }
+ )
}
}
+}
-
- NavHost(navController, startDestination = "home") {
- composable("home") {
- // A simple loading/home screen
- //Greeting()
- }
- composable("callScreen") {
- CallScreen()
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@Composable
+fun OnlineUserCard(userId: String, onJoinClick: () -> Unit) { // FIX #3: Simplified parameter
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ .shadow(4.dp, RoundedCornerShape(8.dp)),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = userId, modifier = Modifier.weight(1f).padding(end = 16.dp))
+ Button(
+ onClick = onJoinClick, // Use the simplified lambda
+ modifier = Modifier.wrapContentWidth()
+ ) {
+ Text(text = "Call")
+ }
}
}
}
+
+
+
@Composable
fun PeerIdSection(peerId: String) {
Card(
diff --git a/app/src/main/java/com/example/neurology_project_android/MainViewModel.kt b/app/src/main/java/com/example/neurology_project_android/MainViewModel.kt
new file mode 100644
index 0000000..5d43d8b
--- /dev/null
+++ b/app/src/main/java/com/example/neurology_project_android/MainViewModel.kt
@@ -0,0 +1,102 @@
+// In: app/src/main/java/com/example/neurology_project_android/MainViewModel.kt
+
+package com.example.neurology_project_android
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.neurology_project_android.BuildConfig.API_GET_ID_URL
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import javax.inject.Inject
+
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val signalingClient: SignalingClient
+) : ViewModel() {
+
+ // --- State Management ---
+ private val _uiState = MutableStateFlow(MainUiState.Loading)
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ init {
+ // Start the process as soon as the ViewModel is created
+ initialize()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private fun initialize() {
+ viewModelScope.launch {
+ _uiState.value = MainUiState.Loading
+ val userId = fetchUserId()
+ if (userId == "unknown") {
+ _uiState.value = MainUiState.Error("Could not fetch user ID.")
+ return@launch
+ }
+
+
+ // 1. Connect the client
+// signalingClient.connectClient(userId)
+
+ // 2. Start collecting the peer list flow
+ signalingClient.peerListFlow
+ .catch { exception ->
+ // Handle any errors from the flow
+ _uiState.value = MainUiState.Error(exception.message ?: "An error occurred")
+ }
+ .collect { peers ->
+ Log.e("MainViewModel", "Received peer list: $peers")
+
+ // 3. Every time a new peer list arrives, update the UI state
+ // The current user is already filtered on the server in this example,
+ // but filtering here is a good safeguard.
+ val otherPeers = peers.filter { it != userId }
+ _uiState.value = MainUiState.Success(userId, otherPeers)
+ }
+ }
+ }
+ // Expose repository methods for the UI to call
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun joinRoom(targetUserId: String) {
+ signalingClient.joinRoom(targetUserId)
+ }
+
+ // This logic is now self-contained within the ViewModel
+ private suspend fun fetchUserId(): String {
+ return withContext(Dispatchers.IO) {
+ try {
+ val idUrl = API_GET_ID_URL
+ val client = OkHttpClient()
+ val request = Request.Builder().url(idUrl).build()
+ val response = client.newCall(request).execute()
+ response.body?.string() ?: "unknown"
+ } catch (e: Exception) {
+ "unknown"
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ fun connectSignalingClient() {
+ signalingClient.connectClient()
+ }
+}
+
+// Sealed interface to represent the different UI states
+sealed interface MainUiState {
+ object Loading : MainUiState
+ data class Success(val userId: String, val peers: List) : MainUiState
+ data class Error(val message: String) : MainUiState
+}
diff --git a/app/src/main/java/com/example/neurology_project_android/MyApplication.kt b/app/src/main/java/com/example/neurology_project_android/MyApplication.kt
new file mode 100644
index 0000000..e92cb82
--- /dev/null
+++ b/app/src/main/java/com/example/neurology_project_android/MyApplication.kt
@@ -0,0 +1,10 @@
+
+package com.example.neurology_project_android
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class MyApplication : Application() {
+ // The body can be empty. Hilt uses this annotation for setup.
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/neurology_project_android/NIHForm.kt b/app/src/main/java/com/example/neurology_project_android/NIHForm.kt
index f3c1dea..48b7510 100644
--- a/app/src/main/java/com/example/neurology_project_android/NIHForm.kt
+++ b/app/src/main/java/com/example/neurology_project_android/NIHForm.kt
@@ -2,13 +2,91 @@ package com.example.neurology_project_android
import androidx.room.Entity
import androidx.room.PrimaryKey
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.util.UUID
-@Entity(tableName = "nih_forms")
+
+
+
+@Serializable
data class NIHForm(
- @PrimaryKey(autoGenerate = true) val id: Int = 0,
- val patientName: String,
- val dob: String,
- val date: String,
- val formData: String,
- val username: String
-)
\ No newline at end of file
+
+ @SerialName(value = "form_id")
+ val form_id: String? = null,
+
+ // Other string/text fields (all nullable)
+ @SerialName("username")
+ val username: String? = null,
+
+ @SerialName("form_date")
+ val formDate: String? = null, // You can parse this into a Date object later
+
+ @SerialName("patient_name")
+ val patientName: String? = null,
+
+ @SerialName("patient_dob")
+ val patientDob: String? = null,
+
+ @SerialName("session_id")
+ val sessionId: String? = null,
+
+ // --- NIHSS Item Scores (all nullable) ---
+
+ @SerialName("item_1a_loc_level")
+ val item1aLocLevel: Int? = null,
+
+ @SerialName("item_1b_loc_commands")
+ val item1bLocCommands: Int? = null,
+
+ @SerialName("item_1c_loc_best_gaze")
+ val item1cLocBestGaze: Int? = null,
+
+ @SerialName("item_2_best_motor_gaze") // As per your schema
+ val item2BestMotorGaze: Int? = null,
+
+ @SerialName("item_3_visual")
+ val item3Visual: Int? = null,
+
+ @SerialName("item_4_facial_palsy")
+ val item4FacialPalsy: Int? = null,
+
+ @SerialName("item_5_left_arm_motor")
+ val item5LeftArmMotor: Int? = null,
+
+ @SerialName("item_6_right_arm_motor")
+ val item6RightArmMotor: Int? = null,
+
+ @SerialName("item_7_left_leg_motor")
+ val item7LeftLegMotor: Int? = null,
+
+ @SerialName("item_8_right_leg_motor")
+ val item8RightLegMotor: Int? = null,
+
+ @SerialName("item_9_ataxia")
+ val item9Ataxia: Int? = null,
+
+ @SerialName("item_10_sensory")
+ val item10Sensory: Int? = null,
+
+ @SerialName("item_11_language")
+ val item11Language: Int? = null,
+
+ @SerialName("item_12_dysarthria")
+ val item12Dysarthria: Int? = null,
+
+ @SerialName("item_13_extinction_inattention")
+ val item13ExtinctionInattention: Int? = null,
+
+ // --- Total Score ---
+
+ @SerialName("total_nihss_score")
+ val totalNihssScore: Int? = null
+
+
+)
+
+ public fun NIHForm.toJson(): String {
+ return Json.encodeToString(this)
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/example/neurology_project_android/NIHFormModel.kt b/app/src/main/java/com/example/neurology_project_android/NIHFormModel.kt
new file mode 100644
index 0000000..cbf84e8
--- /dev/null
+++ b/app/src/main/java/com/example/neurology_project_android/NIHFormModel.kt
@@ -0,0 +1,191 @@
+// In a new file: app/src/main/java/com/example/neurology_project_android/NIHFormModel.kt
+
+package com.example.neurology_project_android
+
+// Represents a single answer option for a question
+data class FormOption(
+ val displayText: String, // The text shown in the UI, e.g., "Both arms held for 10 seconds"
+ val score: Int // The value stored for this option, e.g., 0
+)
+
+// Represents a single question in the NIH Stroke Scale
+data class FormQuestion(
+ val id: Int, // A unique identifier, e.g., "1a"
+ val questionText: String, // The main question header, e.g., "1a. Level of Consciousness"
+ val instructionText: String, // The sub-header or instructions
+ val options: List // The list of possible answers for this question
+)
+
+// This object holds the complete definition of your form, replacing the old StrokeScaleQuestions
+object NIHFormModel {
+ val questions: List = listOf(
+ FormQuestion(
+ id = 1,
+ questionText = "1a. Level of Consciousness",
+ instructionText = "Assess the patient's response to stimulation.",
+ options = listOf(
+ FormOption("Alert; keenly responsive.", 0),
+ FormOption("Not alert, but arousable by minor stimulation.", 1),
+ FormOption("Not alert; requires repeated stimulation.", 2),
+ FormOption("Responds only with reflex motor or autonomic effects.", 3)
+ )
+ ),
+ FormQuestion(
+ id = 2,
+ questionText = "1b. LOC Questions",
+ instructionText = "Ask the patient: What is the month? What is your age?",
+ options = listOf(
+ FormOption("Answers both questions correctly.", 0),
+ FormOption("Answers one question correctly.", 1),
+ FormOption("Answers neither question correctly.", 2)
+ )
+ ),
+ FormQuestion(
+ id = 3,
+ questionText = "1c. LOC Commands",
+ instructionText = "Ask the patient to: Open and close your eyes. Grip and release your non-paretic hand.",
+ options = listOf(
+ FormOption("Performs both tasks correctly.", 0),
+ FormOption("Performs one task correctly.", 1),
+ FormOption("Performs neither task correctly.", 2)
+ )
+ ),
+ FormQuestion(
+ id = 4,
+ questionText = "2. Best Gaze",
+ instructionText = "Test horizontal eye movements.",
+ options = listOf(
+ FormOption("Normal.", 0),
+ FormOption("Partial gaze palsy; gaze is abnormal in one or both eyes.", 1),
+ FormOption("Forced deviation, or total gaze paresis.", 2)
+ )
+ ),
+ FormQuestion(
+ id = 5,
+ questionText = "3. Visual",
+ instructionText = "Test visual fields by confrontation (upper and lower quadrants).",
+ options = listOf(
+ FormOption("No visual loss.", 0),
+ FormOption("Partial hemianopia.", 1),
+ FormOption("Complete hemianopia.", 2),
+ FormOption("Bilateral hemianopia (blindness).", 3)
+ )
+ ),
+ FormQuestion(
+ id = 6,
+ questionText = "4. Facial Palsy",
+ instructionText = "Ask patient to show teeth or raise eyebrows and close eyes.",
+ options = listOf(
+ FormOption("Normal symmetrical movements.", 0),
+ FormOption("Minor paralysis (flattened nasolabial fold, asymmetry on smiling).", 1),
+ FormOption("Partial paralysis (total or near-total paralysis of lower face).", 2),
+ FormOption("Complete paralysis of one or both sides (absence of facial movement in the upper and lower face).", 3)
+ )
+ ),
+ FormQuestion(
+ id = 7,
+ questionText = "5. Motor Arm (Left)",
+ instructionText = "Extend the arm (palms down) 90 degrees (if sitting) or 45 degrees (if supine). Hold for 10 seconds.",
+ options = listOf(
+ FormOption("No drift; arm holds 90 (or 45) degrees for full 10 seconds.", 0),
+ FormOption("Drift; arm holds but drifts down before full 10 seconds.", 1),
+ FormOption("Some effort against gravity; arm cannot get to or maintain (if cued) 90 (or 45) degrees, drifts down to bed.", 2),
+ FormOption("No effort against gravity; arm falls.", 3),
+ FormOption("No movement.", 4),
+ FormOption("Amputation or joint fusion, explain:", 0)
+ )
+ ),
+ FormQuestion(
+ id = 8,
+ questionText = "6. Motor Arm (Right)",
+ instructionText = "Extend the arm (palms down) 90 degrees (if sitting) or 45 degrees (if supine). Hold for 10 seconds.",
+ options = listOf(
+ FormOption("No drift; arm holds 90 (or 45) degrees for full 10 seconds.", 0),
+ FormOption("Drift; arm holds but drifts down before full 10 seconds.", 1),
+ FormOption("Some effort against gravity; arm cannot get to or maintain (if cued) 90 (or 45) degrees, drifts down to bed.", 2),
+ FormOption("No effort against gravity; arm falls.", 3),
+ FormOption("No movement.", 4),
+ FormOption("Amputation or joint fusion, explain:", 0)
+ )
+ ),
+ FormQuestion(
+ id = 9,
+ questionText = "7. Motor Leg (Left)",
+ instructionText = "Raise the leg to 30 degrees (always supine). Hold for 5 seconds.",
+ options = listOf(
+ FormOption("No drift; leg holds 30-degree position for full 5 seconds.", 0),
+ FormOption("Drift; leg falls by end of the 5-second period but does not hit bed.", 1),
+ FormOption("Some effort against gravity; leg falls to bed by 5 seconds, but has some effort against gravity.", 2),
+ FormOption("No effort against gravity; leg falls to bed immediately.", 3),
+ FormOption("No movement.", 4),
+ FormOption("Amputation or joint fusion, explain:", 0)
+ )
+ ),
+ FormQuestion(
+ id = 10,
+ questionText = "8. Motor Leg (Right)",
+ instructionText = "Raise the leg to 30 degrees (always supine). Hold for 5 seconds.",
+ options = listOf(
+ FormOption("No drift; leg holds 30-degree position for full 5 seconds.", 0),
+ FormOption("Drift; leg falls by end of the 5-second period but does not hit bed.", 1),
+ FormOption("Some effort against gravity; leg falls to bed by 5 seconds, but has some effort against gravity.", 2),
+ FormOption("No effort against gravity; leg falls to bed immediately.", 3),
+ FormOption("No movement.", 4),
+ FormOption("Amputation or joint fusion, explain:", 0)
+ )
+ ),
+ FormQuestion(
+ id = 11,
+ questionText = "9. Limb Ataxia",
+ instructionText = "Test with finger-nose-finger and heel-shin tests on both sides.",
+ options = listOf(
+ FormOption("Absent.", 0),
+ FormOption("Present in one limb.", 1),
+ FormOption("Present in two limbs.", 2),
+ FormOption("Does not understand or is paralyzed.", 0)
+ )
+ ),
+ FormQuestion(
+ id = 12,
+ questionText = "10. Sensory",
+ instructionText = "Test sensation or grimace to pinprick.",
+ options = listOf(
+ FormOption("Normal; no sensory loss.", 0),
+ FormOption("Mild-to-moderate sensory loss; less sharp/dull on affected side.", 1),
+ FormOption("Severe to total sensory loss; not aware of being touched.", 2)
+ )
+ ),
+ FormQuestion(
+ id = 13,
+ questionText = "11. Best Language",
+ instructionText = "Ask patient to describe what is happening in the provided picture, name items, and read sentences.",
+ options = listOf(
+ FormOption("No aphasia; normal.", 0),
+ FormOption("Mild-to-moderate aphasia; some obvious loss of fluency or comprehension.", 1),
+ FormOption("Severe aphasia; all communication is fragmented, listener cannot understand.", 2),
+ FormOption("Mute, global aphasia; no usable speech or auditory comprehension.", 3)
+ )
+ ),
+ FormQuestion(
+ id = 14,
+ questionText = "12. Dysarthria",
+ instructionText = "Ask the patient to read or repeat words from an attached list.",
+ options = listOf(
+ FormOption("Normal.", 0),
+ FormOption("Mild-to-moderate dysarthria; slurring of words but can be understood.", 1),
+ FormOption("Severe dysarthria; so slurred it is unintelligible or worse.", 2),
+ FormOption("Intubated or other physical barrier.", 0)
+ )
+ ),
+ FormQuestion(
+ id = 15,
+ questionText = "13. Extinction and Inattention",
+ instructionText = "Sufficient information to identify neglect from prior testing is acceptable.",
+ options = listOf(
+ FormOption("No neglect.", 0),
+ FormOption("Visual, tactile, auditory, or personal inattention to one side.", 1),
+ FormOption("Profound hemi-inattention or extinction to more than one modality.", 2)
+ )
+ )
+ )
+}
diff --git a/app/src/main/java/com/example/neurology_project_android/NewNIHFormActivity.kt b/app/src/main/java/com/example/neurology_project_android/NewNIHFormActivity.kt
index 2b566a1..3566c72 100644
--- a/app/src/main/java/com/example/neurology_project_android/NewNIHFormActivity.kt
+++ b/app/src/main/java/com/example/neurology_project_android/NewNIHFormActivity.kt
@@ -1,490 +1,287 @@
package com.example.neurology_project_android
-import android.content.Context
-import android.content.Intent
+import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
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.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import kotlinx.coroutines.launch
-import okhttp3.OkHttpClient
-import java.text.SimpleDateFormat
-import java.util.*
-
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+// --- FIX 1: Import the new models ---
+import com.example.neurology_project_android.NIHFormModel
+import com.example.neurology_project_android.FormQuestion
+import com.example.neurology_project_android.NewNIHFormViewModel
+import com.example.neurology_project_android.SubmissionStatus
+import dagger.hilt.android.AndroidEntryPoint
+
+// --- You can remove the old StrokeScaleQuestions import if it exists ---
+ // This annotation tells Hilt to manage dependencies for this Activity
+@AndroidEntryPoint
class NewNIHFormActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val formId = intent.getIntExtra("formId", -1)
- val patientName = intent.getStringExtra("patientName")
- val dob = intent.getStringExtra("dob")
- val date = intent.getStringExtra("date")
- val formData = intent.getStringExtra("formData")
- val username = intent.getStringExtra("username")
- val sessionManager = SessionManager(this) // Create the real instance
- val existingForm = if (patientName != null && dob != null && date != null && formData != null && username != null) {
- NIHForm(formId, patientName, dob, date, formData, username)
- } else null
-
- setContent {
- NewNIHFormScreen(existingForm, sessionManager)
- }
- }
-}
-@Composable
-fun NewNIHFormScreen(existingForm: NIHForm? = null, sessionManager: ISessionManager) {
- val context = LocalContext.current
+ // 1. Get the ViewModel directly from Hilt.
+ // The `by viewModels()` delegate handles everything for you.
+ private val viewModel: NewNIHFormViewModel by viewModels()
- val client = sessionManager.client
- val coroutineScope = rememberCoroutineScope()
- var refreshTrigger by remember { mutableStateOf(0) }
-
- var patientName by remember { mutableStateOf(existingForm?.patientName ?: "") }
- var dob by remember { mutableStateOf(existingForm?.dob ?: "") }
- val questions = remember { StrokeScaleQuestions.questions }
- val selectedOptions = remember {
- mutableStateListOf().apply { repeat(questions.size) { add(null) } }
+ companion object {
+ const val EXTRA_IN_CALL = "extra_in_call"
}
- val keyboardController = LocalSoftwareKeyboardController.current
- val dobCalendar = remember { Calendar.getInstance() }
- val username = remember { sessionManager.fetchUsername() ?: "anonymous" }
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
- val date = remember {
- existingForm?.date ?: SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()).format(Date())
+ val inCall = intent.getBooleanExtra(EXTRA_IN_CALL, false)
+ // Hilt provides the ViewModel and its dependencies automatically.
+ // No manual setup needed.
+ setContent {
+ // 2. Simply pass the Hilt-provided ViewModel to your screen.
+ NewNIHFormScreen(viewModel = viewModel, inCall = inCall)
+ }
}
- // Populate selected options if editing
- LaunchedEffect(existingForm) {
- existingForm?.formData?.forEachIndexed { index, c ->
- val score = c.toString().toIntOrNull() ?: 9
- selectedOptions[index] = if (score != 9) score else null
+ // This is the Composable function your Activity is trying to call.
+ @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Composable
+ fun NewNIHFormScreen(viewModel: NewNIHFormViewModel, inCall: Boolean) {
+ // 1. Observe state directly from the ViewModel
+ val patientName by viewModel.patientName
+ val itemScores by viewModel.itemScores
+ val submissionStatus by viewModel.submissionStatus.collectAsState()
+ val context = LocalContext.current
+
+ val totalScore by remember(itemScores) {
+ derivedStateOf {
+ itemScores.mapNotNull { it }.sum()
+ }
}
- }
- val datePickerDialog = android.app.DatePickerDialog(
- context,
- { _, year, month, dayOfMonth ->
- dobCalendar.set(year, month, dayOfMonth)
- dob = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()).format(dobCalendar.time)
- },
- dobCalendar.get(Calendar.YEAR),
- dobCalendar.get(Calendar.MONTH),
- dobCalendar.get(Calendar.DAY_OF_MONTH)
- )
+ // 2. React to changes in submissionStatus (e.g., show a toast, navigate away)
+ LaunchedEffect(submissionStatus) {
+ when (val status = submissionStatus) {
+ is SubmissionStatus.Success -> {
+ Toast.makeText(context, "Form submitted successfully!", Toast.LENGTH_SHORT).show()
+ // You could finish the activity upon success
+ // (context as? android.app.Activity)?.finish()
+ viewModel.resetSubmissionStatus()
+ }
+ is SubmissionStatus.Error -> {
+ Toast.makeText(context, status.message, Toast.LENGTH_LONG).show()
+ viewModel.resetSubmissionStatus()
+ }
+ else -> { /* Do nothing for Idle or Loading states here */ }
+ }
+ }
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(Color(0xFFF3E5F5))
- .padding(16.dp)
- ) {
+ // 3. Define the UI structure
Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ .background(MaterialTheme.colorScheme.background)
) {
Text(
text = "New NIH Stroke Scale Form",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
- modifier = Modifier
- .padding(top = 16.dp, bottom = 8.dp)
+ modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedTextField(
value = patientName,
- onValueChange = { patientName = it },
- label = { Text("Enter Patient Name") },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 4.dp),
- keyboardOptions = KeyboardOptions(
- autoCorrect = false // Disables suggestions
- ),
- keyboardActions = KeyboardActions(
- onDone = { keyboardController?.hide() } // Closes keyboard on Done press
- ),
- placeholder = { Text("") }, // Prevents showing hints
+ // Send user input events up to the ViewModel
+ onValueChange = { viewModel.onPatientNameChange(it) },
+ label = { Text("Patient Name") },
+ modifier = Modifier.fillMaxWidth()
)
- OutlinedTextField(
- value = dob,
- onValueChange = {},
- readOnly = true,
- label = { Text("Date of Birth") },
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 4.dp)
- .clickable { datePickerDialog.show() },
- enabled = false, //Disables editing, but still clickable due to Modifier
- colors = OutlinedTextFieldDefaults.colors(
- disabledBorderColor = MaterialTheme.colorScheme.outline
- )
-
- )
-
-
- Text(
- text = "Date: $date",
- fontSize = 16.sp,
- color = Color.Gray,
- modifier = Modifier
- .padding(bottom = 16.dp)
- )
- }
-
- LazyColumn(
- modifier = Modifier.weight(1f)
- ) {
- items(questions) { question ->
- QuestionCard(question, selectedOptions)
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // A scrollable list for all the questions
+ LazyColumn(modifier = Modifier.weight(1f)) {
+ // --- FIX 2: Use the new NIHFormModel instead of StrokeScaleQuestions ---
+ itemsIndexed(NIHFormModel.questions) { index, question ->
+ QuestionCard(
+ questionIndex = index,
+ question = question,
+ // Get the currently selected score for this question from the ViewModel's state
+ selectedScore = itemScores.getOrNull(index),
+ inCall = inCall,
+ // When an option is clicked, notify the ViewModel with the question index and the option's score
+ onOptionClick = { score ->
+ viewModel.onScoreSelected(index, score)
+ },
+ onAutomateClick = {
+ viewModel.automateQuestion(index)
+ }
+ )
+ }
}
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 16.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
- ) {
- Button(
- onClick = {
- if (patientName.isBlank()) {
- Toast.makeText(context, "Please enter a patient name", Toast.LENGTH_SHORT).show()
- return@Button
- }
+ // --- NEW: ADD THE TOTAL SCORE DISPLAY ---
+ Spacer(modifier = Modifier.height(16.dp))
- coroutineScope.launch {
- val formData = selectedOptions.joinToString("") { (it ?: 9).toString() }
- val form = NIHForm(
- patientName = patientName,
- dob = dob,
- date = date,
- formData = formData,
- username = username
- )
- FormManager.submitFormToServer(form, client as OkHttpClient) { success ->
- (context as? ComponentActivity)?.runOnUiThread {
- if (success) {
- Toast.makeText(context, "Form saved successfully", Toast.LENGTH_SHORT).show()
- val intent = Intent(context, ListNIHFormActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- context.startActivity(intent)
- } else {
- Toast.makeText(context, "Failed to save form", Toast.LENGTH_SHORT).show()
- }
- }
- }
- }
- },
- colors = ButtonDefaults.buttonColors(containerColor = Color.Green),
- modifier = Modifier.weight(1f)
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- Text(text = "Save", color = Color.White)
+ Text(
+ text = "Total NIHSS Score:",
+ fontSize = 20.sp,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Text(
+ text = "$totalScore", // Display the calculated total score
+ fontSize = 22.sp,
+ fontWeight = FontWeight.ExtraBold,
+ color = MaterialTheme.colorScheme.primary
+ )
}
+ Spacer(modifier = Modifier.height(8.dp))
+
Button(
- onClick = {
- val intent = Intent(context, ListNIHFormActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- context.startActivity(intent)
- },
- colors = ButtonDefaults.buttonColors(containerColor = Color.Red),
- modifier = Modifier.weight(1f)
+ // When the button is clicked, call the submitForm function on the ViewModel
+ onClick = { viewModel.submitForm() },
+ // Disable the button while the form is submitting
+ enabled = submissionStatus != SubmissionStatus.Loading,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
) {
- Text(text = "Cancel", color = Color.White)
+ if (submissionStatus == SubmissionStatus.Loading) {
+ CircularProgressIndicator(modifier = Modifier.size(24.dp), color = Color.White)
+ } else {
+ Text("Save Form")
+ }
}
}
}
-}
-
-@Composable
-fun QuestionCard(question: StrokeScaleQuestion, selectedOptions: MutableList) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp)
- .background(Color.White)
- .padding(16.dp),
- horizontalAlignment = Alignment.Start
- ) {
- // Question Header
- Text(
- text = question.questionHeader,
- fontWeight = FontWeight.Bold,
- fontSize = 18.sp,
- modifier = Modifier.padding(bottom = 4.dp)
- )
-
- // Subheader
- if (!question.subHeader.isNullOrEmpty()) {
- Text(
- text = question.subHeader,
- fontSize = 14.sp,
- color = Color.Gray,
- modifier = Modifier.padding(bottom = 8.dp)
+ /**
+ * A reusable Composable for displaying a single question, its options, and highlighting the selection.
+ */
+ @Composable
+ fun QuestionCard(questionIndex: Int, question: FormQuestion, selectedScore: Int?, inCall: Boolean, onOptionClick: (Int) -> Unit, onAutomateClick: () -> Unit) { // --- FIX 3: Use FormQuestion class ---
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color.hsv(268.9F, saturation = .165F, value = 1.0F, alpha = 1.0F)
)
- }
-
- // Options
- question.options.forEachIndexed { index, option ->
- Row(
+ ) {
+ Column(
modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 4.dp)
- .background(
- if (selectedOptions[question.id] == index) Color(0xFFA5D6A7) else Color(0xFFF3E5F5)
- )
- .padding(8.dp)
- .clickable { selectedOptions[question.id] = index },
- horizontalArrangement = Arrangement.SpaceBetween
+ .padding(16.dp)
+
) {
- Text(text = option.title)
- Text(text = if (option.score > 0) "+${option.score}" else "${option.score}")
+ // --- FIX 4: Use properties from the new FormQuestion data class ---
+ Text(text = question.questionText, fontWeight = FontWeight.Bold, fontSize = 18.sp)
+ if (question.instructionText.isNotEmpty()) {
+ Text(
+ text = question.instructionText,
+ fontSize = 18.sp,
+ color = Color.Gray,
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+ }
+
+ // Show automate only for question index 6 AND only in a call
+ if (inCall && questionIndex == 6) {
+ Button(
+ onClick = onAutomateClick,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
+ ) {
+ Text("Automate Question")
+ }
+ }
+
+ // Create a clickable row for each answer option
+ question.options.forEach { option ->
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .border(
+ width = 2.dp,
+ color = Color.LightGray,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .fillMaxWidth()
+ .background(
+ // Highlight the row if its score matches the selected score
+ if (selectedScore == option.score) Color.hsv(270.0F, saturation = .477F, value = .987F, alpha = 1.0F) else Color.White
+ )
+
+ .clickable { onOptionClick(option.score) } // Pass the option's actual score up
+ .padding(8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // --- FIX 5: Use properties from the new FormOption data class ---
+ Text(text = option.displayText,fontSize = 18.sp, modifier = Modifier.weight(1f))
+ Text(text = "${option.score}",fontSize = 18.sp)
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ }
}
}
}
}
-data class StrokeScaleQuestion(
- val id: Int,
- val questionHeader: String,
- val subHeader: String?,
- val options: List