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" />
+
+
+ app:layout_constraintTop_toBottomOf="@id/openPatientsButton_doctor" />