From 1118601925d577e6ea3594302f3deba49770b17b Mon Sep 17 00:00:00 2001 From: sandesh Date: Fri, 3 Apr 2026 00:31:31 +1100 Subject: [PATCH] Refine patient list and details UI with state handling --- .../guardian/adapter/PatientListAdapter.kt | 25 +- .../view/general/PatientDetailsActivity.kt | 144 ++++++---- .../view/general/PatientListActivity.kt | 69 +++-- .../res/layout/activity_patient_details.xml | 267 +++++++++++------- app/src/main/res/layout/item_patient.xml | 73 +++-- 5 files changed, 354 insertions(+), 224 deletions(-) diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt index 297137b4..a7ae6d2a 100644 --- a/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt +++ b/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt @@ -18,6 +18,7 @@ class PatientListAdapter( private val onAssignNurseClick: ((Patient) -> Unit)? = null, private val onDeleteClick: ((Patient) -> Unit)? = null, ) : RecyclerView.Adapter() { + inner class PatientViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val nameText: TextView = itemView.findViewById(R.id.tvName) val ageText: TextView = itemView.findViewById(R.id.tvAge) @@ -31,9 +32,8 @@ class PatientListAdapter( parent: ViewGroup, viewType: Int, ): PatientViewHolder { - val view = - LayoutInflater.from(parent.context) - .inflate(R.layout.item_patient, parent, false) + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_patient, parent, false) return PatientViewHolder(view) } @@ -42,18 +42,17 @@ class PatientListAdapter( position: Int, ) { val patient = patients[position] + holder.nameText.text = patient.fullname - holder.ageText.text = "Age: ${patient.age}" - holder.genderText.text = "Gender: ${ - patient.gender.replaceFirstChar { - if (it.isLowerCase()) it.titlecase() else it.toString() - } - }" + holder.ageText.text = "${patient.age} years" + holder.genderText.text = patient.gender.replaceFirstChar { + if (it.isLowerCase()) it.titlecase() else it.toString() + } - // Load image using Glide Glide.with(holder.itemView.context) .load(patient.photoUrl) .placeholder(R.drawable.profile) + .error(R.drawable.profile) .circleCrop() .into(holder.image) @@ -71,10 +70,12 @@ class PatientListAdapter( onAssignNurseClick?.invoke(patient) true } - R.id.action_delete -> { // Handle delete click + + R.id.action_delete -> { onDeleteClick?.invoke(patient) true } + else -> false } } @@ -89,4 +90,4 @@ class PatientListAdapter( patients = newPatients notifyDataSetChanged() } -} +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/PatientDetailsActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/PatientDetailsActivity.kt index 0e9f2ae4..291a64e2 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/PatientDetailsActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/PatientDetailsActivity.kt @@ -38,47 +38,24 @@ class PatientDetailsActivity : BaseActivity() { if (currentUser.role == Role.Nurse) { binding.toolbar.setBackgroundColor(getColor(R.color.TG_blue)) - binding.containerPatientInfo.setBackgroundColor(getColor(R.color.TG_blue)) + // binding.containerPatientInfo.setBackgroundColor(getColor(R.color.TG_blue)) } - val patient = intent.getSerializableExtra("patient") as Patient - - // Set patient info views - binding.tvName.text = patient.fullname - binding.tvAge.text = "Age: ${patient.age}" - binding.tvDob.text = "Date of Birth: ${patient.dateOfBirth?.substringBefore("T")}" - binding.tvGender.text = "Gender: ${ - patient.gender.replaceFirstChar { - if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() - } - }" - - if (patient.healthConditions.isNotEmpty()) { - val formattedConditions = - patient.healthConditions.joinToString(", ") { condition -> - condition.split(" ").joinToString(" ") { word -> - word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - } - } - binding.tvHealthConditions.text = "Health Conditions: $formattedConditions" - } else { - binding.tvHealthConditions.text = "Health Conditions: No conditions listed" + val patient = intent.getSerializableExtra("patient") as? Patient + if (patient == null) { + showMessage("Patient details not available") + finish() + return } - Glide.with(this) - .load(patient.photoUrl) - .placeholder(R.drawable.profile) - .circleCrop() - .into(binding.imagePatient) + bindPatientDetails(patient) - // Load the assigned nurses fragment dynamically and pass the nurses val nursesFragment = PatientAssignedNursesFragment() nursesFragment.setAssignedNurses(patient.assignedNurses ?: emptyList()) supportFragmentManager.beginTransaction() .replace(R.id.fragmentAssignedNursesContainer, nursesFragment) .commit() - // Setup RecyclerView for activity logs activitiesAdapter = PatientActivityAdapter(emptyList()) binding.recyclerViewActivities.layoutManager = LinearLayoutManager(this) binding.recyclerViewActivities.adapter = activitiesAdapter @@ -86,38 +63,99 @@ class PatientDetailsActivity : BaseActivity() { fetchPatientActivities(patient.id) } + @SuppressLint("SetTextI18n") + private fun bindPatientDetails(patient: Patient) { + val formattedGender = patient.gender.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + + val dob = patient.dateOfBirth?.substringBefore("T") ?: "Not available" + + val healthConditionsText = + if (!patient.healthConditions.isNullOrEmpty()) { + patient.healthConditions.joinToString(", ") { condition -> + condition.split(" ").joinToString(" ") { word -> + word.replaceFirstChar { + if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() + } + } + } + } else { + "No conditions listed" + } + + binding.tvName.text = patient.fullname + binding.tvAge.text = "${patient.age} years" + binding.tvDob.text = "Date of Birth: $dob" + binding.tvGender.text = formattedGender + binding.tvHealthConditions.text = "Health Conditions: $healthConditionsText" + + Glide.with(this) + .load(patient.photoUrl) + .placeholder(R.drawable.profile) + .error(R.drawable.profile) + .circleCrop() + .into(binding.imagePatient) + } + private fun fetchPatientActivities(patientId: String) { val token = "Bearer ${SessionManager.getToken()}" + CoroutineScope(Dispatchers.IO).launch { withContext(Dispatchers.Main) { binding.progressBar.visibility = View.VISIBLE + binding.tvEmptyMessage.visibility = View.GONE + binding.recyclerViewActivities.visibility = View.VISIBLE } - val response = - try { - deakin.gopher.guardian.services.api.ApiClient.apiService.getPatientActivities(token, patientId) - } catch (e: Exception) { - null - } - withContext(Dispatchers.Main) { - binding.progressBar.visibility = View.GONE - if (response?.isSuccessful == true) { - val activities = response.body() - if (!activities.isNullOrEmpty()) { - activitiesAdapter.updateData(activities) - binding.tvEmptyMessage.visibility = View.GONE + try { + val response = deakin.gopher.guardian.services.api.ApiClient + .apiService + .getPatientActivities(token, patientId) + + withContext(Dispatchers.Main) { + binding.progressBar.visibility = View.GONE + + if (response.isSuccessful) { + val activities = response.body() + + if (!activities.isNullOrEmpty()) { + activitiesAdapter.updateData(activities) + binding.recyclerViewActivities.visibility = View.VISIBLE + binding.tvEmptyMessage.visibility = View.GONE + } else { + binding.recyclerViewActivities.visibility = View.GONE + binding.tvEmptyMessage.visibility = View.VISIBLE + binding.tvEmptyMessage.text = "No patient activities found" + } } else { + val errorBody = response.errorBody()?.string() + + val errorResponse: ApiErrorResponse? = + if (!errorBody.isNullOrBlank()) { + try { + Gson().fromJson(errorBody, ApiErrorResponse::class.java) + } catch (e: Exception) { + null + } + } else { + null + } + + binding.recyclerViewActivities.visibility = View.GONE binding.tvEmptyMessage.visibility = View.VISIBLE + binding.tvEmptyMessage.text = "Unable to load patient activities" + + showMessage(errorResponse?.apiError ?: "Failed to load activities") } - } else { - val errorBody = response?.errorBody()?.string() - val errorResponse = - try { - Gson().fromJson(errorBody, ApiErrorResponse::class.java) - } catch (ex: Exception) { - null - } - showMessage(errorResponse?.apiError ?: "Failed to load activities") + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + binding.progressBar.visibility = View.GONE + binding.recyclerViewActivities.visibility = View.GONE + binding.tvEmptyMessage.visibility = View.VISIBLE + binding.tvEmptyMessage.text = "Network error occurred" + showMessage("Network error occurred") } } } @@ -126,4 +164,4 @@ class PatientDetailsActivity : BaseActivity() { private fun showMessage(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } -} +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt index c2bbfe07..f16dab3c 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt @@ -95,41 +95,68 @@ class PatientListActivity : BaseActivity() { private fun fetchPatients() { val token = "Bearer ${SessionManager.getToken()}" + CoroutineScope(Dispatchers.IO).launch { - if (patientListAdapter.itemCount <= 0) { - withContext(Dispatchers.Main) { + withContext(Dispatchers.Main) { + if (patientListAdapter.itemCount <= 0) { binding.progressBar.show() } + binding.tvEmptyMessage.visibility = View.GONE + binding.recyclerViewPatients.visibility = View.VISIBLE } - val response = ApiClient.apiService.getAssignedPatients(token) - withContext(Dispatchers.Main) { + + try { + val response = ApiClient.apiService.getAssignedPatients(token) + withContext(Dispatchers.Main) { binding.progressBar.hide() - } - if (response.isSuccessful) { - if (!response.body().isNullOrEmpty()) { - patientListAdapter.updateData(response.body()!!) - withContext(Dispatchers.Main) { + + if (response.isSuccessful) { + val patients = response.body() + + if (!patients.isNullOrEmpty()) { + patientListAdapter.updateData(patients) + binding.recyclerViewPatients.visibility = View.VISIBLE binding.tvEmptyMessage.visibility = View.GONE - } - } else { - withContext(Dispatchers.Main) { + } else { + patientListAdapter.updateData(emptyList()) + binding.recyclerViewPatients.visibility = View.GONE binding.tvEmptyMessage.visibility = View.VISIBLE + binding.tvEmptyMessage.text = "No patients found" } + } else { + patientListAdapter.updateData(emptyList()) + binding.recyclerViewPatients.visibility = View.GONE + binding.tvEmptyMessage.visibility = View.VISIBLE + binding.tvEmptyMessage.text = "Unable to load patients" + + val errorBody = response.errorBody()?.string() + val errorResponse = + if (!errorBody.isNullOrBlank()) { + try { + Gson().fromJson(errorBody, ApiErrorResponse::class.java) + } catch (e: Exception) { + null + } + } else { + null + } + + showMessage(errorResponse?.apiError ?: response.message()) } - } else { - // Handle error - val errorResponse = - Gson().fromJson( - response.errorBody()?.string(), - ApiErrorResponse::class.java, - ) - showMessage(errorResponse.apiError ?: response.message()) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + binding.progressBar.hide() + patientListAdapter.updateData(emptyList()) + binding.recyclerViewPatients.visibility = View.GONE + binding.tvEmptyMessage.visibility = View.VISIBLE + binding.tvEmptyMessage.text = "Network error occurred" + showMessage("Network error occurred") } } } } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { if (currentUser.organization != null) { return false diff --git a/app/src/main/res/layout/activity_patient_details.xml b/app/src/main/res/layout/activity_patient_details.xml index 64059d2d..62ed550b 100644 --- a/app/src/main/res/layout/activity_patient_details.xml +++ b/app/src/main/res/layout/activity_patient_details.xml @@ -1,193 +1,242 @@ - + android:fillViewport="true" + android:background="@android:color/white"> + android:layout_height="wrap_content" + android:paddingBottom="24dp"> - + app:layout_constraintTop_toTopOf="parent" /> - + + + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/profile" /> - + app:layout_constraintTop_toBottomOf="@id/toolbar"> - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="20dp" + android:paddingTop="64dp" + android:paddingEnd="20dp" + android:paddingBottom="20dp"> + + + tools:text="78 years" /> + tools:text="Male" /> + + - + - - - - - - - + app:layout_constraintTop_toBottomOf="@id/containerPatientInfo" /> - - + app:layout_constraintTop_toBottomOf="@id/tvAssignedNursesLabel"> + + + - - - - + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvActivitiesLabel"> + + + + + + + + + + - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_patient.xml b/app/src/main/res/layout/item_patient.xml index 12a9e6f7..bc6eb6ac 100644 --- a/app/src/main/res/layout/item_patient.xml +++ b/app/src/main/res/layout/item_patient.xml @@ -2,28 +2,31 @@ + android:foreground="?attr/selectableItemBackground" + app:cardCornerRadius="14dp" + app:cardElevation="4dp" + app:cardUseCompatPadding="true"> + android:minHeight="92dp" + android:padding="14dp"> @@ -45,33 +52,41 @@ android:id="@+id/tvAge" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/activity_quarter_margin" - android:textSize="@dimen/text_size_small" - app:layout_constraintStart_toStartOf="@id/tvName" + android:layout_marginStart="12dp" + android:layout_marginTop="6dp" + android:textColor="@color/default_text" + android:textSize="13sp" + app:layout_constraintStart_toEndOf="@id/imagePatient" app:layout_constraintTop_toBottomOf="@id/tvName" - tools:text="Age: 78" /> + tools:text="78 years" /> + tools:text="Male" /> + android:contentDescription="@string/more_options" + android:focusable="true" + android:padding="2dp" + android:src="@drawable/ic_more_vert" + app:layout_constraintBottom_toBottomOf="@id/imagePatient" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/imagePatient" + app:tint="@color/default_text" /> + \ No newline at end of file