From 54b84d4f3ce6a9607f0659e3344d11505f244536 Mon Sep 17 00:00:00 2001 From: munnokd Date: Sat, 9 May 2026 01:45:56 +1000 Subject: [PATCH] patient prescreption task has been done --- app/src/main/AndroidManifest.xml | 3 + .../gopher/guardian/model/Prescription.kt | 16 + .../guardian/services/api/ApiService.kt | 20 ++ .../guardian/view/caretaker/MainActivity.kt | 51 +++- .../guardian/view/general/Homepage4doctor.kt | 10 + .../patient/PatientPrescriptionsScreen.kt | 274 ++++++++++++++++++ .../view/patient/PatientReportScreen.kt | 146 ++++++++-- .../res/layout/activity_homepage4doctor.xml | 14 +- 8 files changed, 500 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/deakin/gopher/guardian/model/Prescription.kt create mode 100644 app/src/main/java/deakin/gopher/guardian/view/patient/PatientPrescriptionsScreen.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31d6a312a..e314d88a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -72,6 +72,9 @@ + diff --git a/app/src/main/java/deakin/gopher/guardian/model/Prescription.kt b/app/src/main/java/deakin/gopher/guardian/model/Prescription.kt new file mode 100644 index 000000000..b801e777c --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/model/Prescription.kt @@ -0,0 +1,16 @@ +package deakin.gopher.guardian.model + +import com.google.gson.annotations.SerializedName + +data class Prescription( + @SerializedName("_id") val id: String, + @SerializedName("medicineName") val medicineName: String? = null, + @SerializedName("dosage") val dosage: String? = null, + @SerializedName("frequency") val frequency: String? = null, + @SerializedName("status") val status: String? = null, + @SerializedName("startDate") val startDate: String? = null, + @SerializedName("endDate") val endDate: String? = null, + @SerializedName("createdAt") val createdAt: String? = null, + @SerializedName("updatedAt") val updatedAt: String? = null, + @SerializedName("instructions") val instructions: String? = null, +) diff --git a/app/src/main/java/deakin/gopher/guardian/services/api/ApiService.kt b/app/src/main/java/deakin/gopher/guardian/services/api/ApiService.kt index 0e9ea177e..02288bd24 100644 --- a/app/src/main/java/deakin/gopher/guardian/services/api/ApiService.kt +++ b/app/src/main/java/deakin/gopher/guardian/services/api/ApiService.kt @@ -5,10 +5,12 @@ import deakin.gopher.guardian.model.AddPatientResponse import deakin.gopher.guardian.model.BaseModel import deakin.gopher.guardian.model.Patient import deakin.gopher.guardian.model.PatientActivity +import deakin.gopher.guardian.model.Prescription import deakin.gopher.guardian.model.register.AuthResponse import deakin.gopher.guardian.model.register.RegisterRequest import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Response import retrofit2.http.Body @@ -60,6 +62,12 @@ interface ApiService { @Header("Authorization") token: String, ): Response> + @GET("doctors/{doctorId}/patients") + suspend fun getDoctorPatients( + @Header("Authorization") token: String, + @Path("doctorId") doctorId: String, + ): Response + @Multipart @POST("patients/add") suspend fun addPatient( @@ -91,4 +99,16 @@ interface ApiService { @Header("Authorization") token: String, @Path("id") patientId: String, ): Response + + @GET("patients/{patientId}/prescriptions") + suspend fun getPatientPrescriptions( + @Header("Authorization") token: String, + @Path("patientId") patientId: String, + ): Response> + + @GET("prescriptions/{id}") + suspend fun getPrescriptionById( + @Header("Authorization") token: String, + @Path("id") prescriptionId: String, + ): Response } diff --git a/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt index 5d7c949ba..46aaa13b4 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt @@ -1,10 +1,13 @@ package deakin.gopher.guardian +import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.navigation.NavType +import androidx.navigation.navArgument import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -17,7 +20,11 @@ class MainActivity : ComponentActivity() { GuardianTheme { Surface(color = MaterialTheme.colorScheme.background) { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "welcome") { + val startRoute = + intent?.getStringExtra("startRoute") + ?.takeIf { it.isNotBlank() } + ?: "welcome" + NavHost(navController = navController, startDestination = startRoute) { // 1. Welcome composable("welcome") { WelcomeScreen(navController) @@ -68,6 +75,48 @@ class MainActivity : ComponentActivity() { PrescriptionScreen(navController) } + composable( + route = "patient_prescriptions/{patientId}/{patientName}", + arguments = + listOf( + navArgument("patientId") { type = NavType.StringType }, + navArgument("patientName") { type = NavType.StringType }, + ), + ) { backStackEntry -> + val patientId = + backStackEntry.arguments?.getString("patientId").orEmpty() + val patientName = + Uri.decode( + backStackEntry.arguments?.getString("patientName").orEmpty(), + ) + PatientPrescriptionsScreen( + navController = navController, + patientId = patientId, + patientName = patientName, + ) + } + + composable( + route = "prescription_detail/{prescriptionId}/{patientName}", + arguments = + listOf( + navArgument("prescriptionId") { type = NavType.StringType }, + navArgument("patientName") { type = NavType.StringType }, + ), + ) { backStackEntry -> + val prescriptionId = + backStackEntry.arguments?.getString("prescriptionId").orEmpty() + val patientName = + Uri.decode( + backStackEntry.arguments?.getString("patientName").orEmpty(), + ) + PrescriptionDetailScreen( + navController = navController, + prescriptionId = prescriptionId, + patientName = patientName, + ) + } + // 11. Billing composable("billing") { BillingScreen(navController) diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt b/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt index fb2461467..10a210bc5 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4doctor.kt @@ -1,8 +1,10 @@ package deakin.gopher.guardian.view.general +import android.content.Intent import android.os.Bundle import android.widget.Button import androidx.appcompat.app.AppCompatActivity +import deakin.gopher.guardian.MainActivity as DoctorDashboardActivity import deakin.gopher.guardian.R import deakin.gopher.guardian.services.EmailPasswordAuthService @@ -11,7 +13,15 @@ class Homepage4doctor : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_homepage4doctor) + val openPatientsButton: Button = findViewById(R.id.openPatientsButton_doctor) val signOutButton: Button = findViewById(R.id.signOutButton_doctor) + + openPatientsButton.setOnClickListener { + val intent = Intent(this, DoctorDashboardActivity::class.java) + intent.putExtra("startRoute", "patient_report") + startActivity(intent) + } + signOutButton.setOnClickListener { EmailPasswordAuthService.signOut(this) finish() diff --git a/app/src/main/java/deakin/gopher/guardian/view/patient/PatientPrescriptionsScreen.kt b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientPrescriptionsScreen.kt new file mode 100644 index 000000000..57ba2508e --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientPrescriptionsScreen.kt @@ -0,0 +1,274 @@ +@file:Suppress("ktlint") + +package deakin.gopher.guardian + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavHostController +import deakin.gopher.guardian.model.Prescription +import deakin.gopher.guardian.model.login.SessionManager +import deakin.gopher.guardian.services.api.ApiClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun PatientPrescriptionsScreen( + navController: NavHostController, + patientId: String, + patientName: String, +) { + var isLoading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } + var prescriptions by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(patientId) { + isLoading = true + errorMessage = null + try { + val token = "Bearer ${SessionManager.getToken()}" + val response = + withContext(Dispatchers.IO) { + ApiClient.apiService.getPatientPrescriptions(token, patientId) + } + if (response.isSuccessful && response.body() != null) { + prescriptions = response.body() ?: emptyList() + } else { + errorMessage = "Unable to load prescriptions. Please try again." + } + } catch (e: Exception) { + errorMessage = "Network error while fetching prescriptions." + } finally { + isLoading = false + } + } + + Column( + modifier = + Modifier + .fillMaxSize() + .background(Color.White) + .padding(16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + modifier = + Modifier + .clickable { navController.popBackStack() }, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "$patientName Prescriptions", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF004D8C), + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + when { + isLoading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + errorMessage != null -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = errorMessage ?: "", color = Color.Red) + } + } + + prescriptions.isEmpty() -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "No prescriptions found for this patient.") + } + } + + else -> { + LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(prescriptions) { prescription -> + PrescriptionRow( + prescription = prescription, + onClick = { + navController.navigate( + "prescription_detail/${prescription.id}/${Uri.encode(patientName)}", + ) + }, + ) + } + } + } + } + } +} + +@Composable +private fun PrescriptionRow( + prescription: Prescription, + onClick: () -> Unit, +) { + Surface( + shape = MaterialTheme.shapes.medium, + tonalElevation = 2.dp, + modifier = + Modifier + .fillMaxWidth() + .clickable { onClick() }, + ) { + Column(modifier = Modifier.padding(14.dp)) { + Text( + text = prescription.medicineName ?: "Unknown medicine", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ) + Spacer(modifier = Modifier.height(6.dp)) + Text(text = "Dosage: ${prescription.dosage ?: "-"}") + Text(text = "Frequency: ${prescription.frequency ?: "-"}") + + val status = prescription.status?.lowercase() ?: "unknown" + val statusColor = + if (status == "discontinued") { + Color(0xFFC62828) + } else { + Color(0xFF2E7D32) + } + Text( + text = "Status: ${prescription.status ?: "Active"}", + color = statusColor, + fontWeight = FontWeight.SemiBold, + ) + + Text(text = "Start: ${prescription.startDate ?: "-"}") + Text(text = "End: ${prescription.endDate ?: "-"}") + } + } +} + +@Composable +fun PrescriptionDetailScreen( + navController: NavHostController, + prescriptionId: String, + patientName: String, +) { + var isLoading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } + var prescription by remember { mutableStateOf(null) } + + LaunchedEffect(prescriptionId) { + isLoading = true + errorMessage = null + try { + val token = "Bearer ${SessionManager.getToken()}" + val response = + withContext(Dispatchers.IO) { + ApiClient.apiService.getPrescriptionById(token, prescriptionId) + } + if (response.isSuccessful && response.body() != null) { + prescription = response.body() + } else { + errorMessage = "Unable to load prescription details." + } + } catch (e: Exception) { + errorMessage = "Network error while fetching prescription details." + } finally { + isLoading = false + } + } + + Column( + modifier = + Modifier + .fillMaxSize() + .background(Color.White) + .padding(16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + modifier = + Modifier + .clickable { navController.popBackStack() }, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "$patientName Prescription Detail", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF004D8C), + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + when { + isLoading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + errorMessage != null -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = errorMessage ?: "", color = Color.Red) + } + } + + prescription == null -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "Prescription not found.") + } + } + + else -> { + val item = prescription!! + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = "Medicine: ${item.medicineName ?: "-"}", fontWeight = FontWeight.Bold) + Text(text = "Dosage: ${item.dosage ?: "-"}") + Text(text = "Frequency: ${item.frequency ?: "-"}") + Text(text = "Status: ${item.status ?: "-"}") + Text(text = "Start date: ${item.startDate ?: "-"}") + Text(text = "End date: ${item.endDate ?: "-"}") + Text(text = "Created: ${item.createdAt ?: "-"}") + Text(text = "Updated: ${item.updatedAt ?: "-"}") + Text(text = "Instructions: ${item.instructions ?: "-"}") + } + } + } + } +} diff --git a/app/src/main/java/deakin/gopher/guardian/view/patient/PatientReportScreen.kt b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientReportScreen.kt index 5e37acb98..f9b0aa83e 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/patient/PatientReportScreen.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientReportScreen.kt @@ -2,17 +2,39 @@ package deakin.gopher.guardian +import android.net.Uri +import com.google.gson.Gson +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -21,9 +43,41 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import deakin.gopher.guardian.model.Patient +import deakin.gopher.guardian.model.login.SessionManager +import deakin.gopher.guardian.services.api.ApiClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Composable fun PatientReportScreen(navController: NavHostController) { + var isLoading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } + var patients by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(Unit) { + isLoading = true + errorMessage = null + try { + val token = "Bearer ${SessionManager.getToken()}" + val doctorId = SessionManager.getCurrentUser().id + val response = + withContext(Dispatchers.IO) { + ApiClient.apiService.getDoctorPatients(token, doctorId) + } + if (response.isSuccessful && response.body() != null) { + val rawJson = response.body()?.string().orEmpty() + patients = parsePatientsFromDoctorResponse(rawJson) + } else { + errorMessage = "Unable to load patients (HTTP ${response.code()})." + } + } catch (e: Exception) { + errorMessage = "Failed to load patients: ${e.message ?: "Unknown error"}" + } finally { + isLoading = false + } + } + Column( modifier = Modifier .fillMaxSize() @@ -49,40 +103,70 @@ fun PatientReportScreen(navController: NavHostController) { Spacer(modifier = Modifier.height(16.dp)) - // Patient List - PatientCard( - imageId = R.drawable.icon_william, - name = "Williams S", - age = 60, - status = "Need medication", - statusColor = Color.Red, - background = Color(0xFFFFEBEE), - onClick = { navController.navigate("medical_summary") } - ) + when { + isLoading -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } - Spacer(modifier = Modifier.height(12.dp)) + errorMessage != null -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = errorMessage ?: "", color = Color.Red) + } + } - PatientCard( - imageId = R.drawable.icon_norah, - name = "Norah P", - age = 56, - status = "No Issues", - statusColor = Color(0xFF4CAF50), - background = Color(0xFFE3F2FD), - onClick = { /* navController.navigate("medical_summary") */ } - ) + patients.isEmpty() -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "No patients found.") + } + } - Spacer(modifier = Modifier.height(12.dp)) + else -> { + LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(patients) { patient -> + val hasHealthIssues = patient.healthConditions.isNotEmpty() + val status = if (hasHealthIssues) "Need medication" else "No Issues" + val statusColor = if (hasHealthIssues) Color.Red else Color(0xFF4CAF50) + val background = if (hasHealthIssues) Color(0xFFFFEBEE) else Color(0xFFE3F2FD) + PatientCard( + imageId = R.drawable.icon_william, + name = patient.fullname, + age = patient.age, + status = status, + statusColor = statusColor, + background = background, + onClick = { + navController.navigate( + "patient_prescriptions/${patient.id}/${Uri.encode(patient.fullname)}", + ) + }, + ) + } + } + } + } + } +} - PatientCard( - imageId = R.drawable.icon_serah, - name = "Serah S", - age = 56, - status = "No Issues", - statusColor = Color(0xFF4CAF50), - background = Color(0xFFE3F2FD), - onClick = { /* navController.navigate("medical_summary") */ } - ) +private fun parsePatientsFromDoctorResponse(rawJson: String): List { + if (rawJson.isBlank()) return emptyList() + + val gson = Gson() + val listType = object : TypeToken>() {}.type + val root = JsonParser.parseString(rawJson) + + return when { + root.isJsonArray -> gson.fromJson(rawJson, listType) + root.isJsonObject -> { + val jsonObject = root.asJsonObject + when { + jsonObject.has("patients") -> gson.fromJson(jsonObject.get("patients"), listType) + jsonObject.has("data") -> gson.fromJson(jsonObject.get("data"), listType) + else -> emptyList() + } + } + else -> emptyList() } } diff --git a/app/src/main/res/layout/activity_homepage4doctor.xml b/app/src/main/res/layout/activity_homepage4doctor.xml index 6e7936f35..b8f07e891 100644 --- a/app/src/main/res/layout/activity_homepage4doctor.xml +++ b/app/src/main/res/layout/activity_homepage4doctor.xml @@ -16,14 +16,24 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> +