diff --git a/app/build.gradle b/app/build.gradle
index 18f94621d..9d68806af 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.5.1'
+ classpath 'com.android.tools.build:gradle:8.13.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:1.9.20"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 837d337b7..f4341a17f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,7 +18,9 @@
-
+
@@ -34,9 +36,9 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
+ tools:ignore="ExtraText"
tools:replace="android:allowBackup,android:label"
- tools:targetApi="31"
- tools:ignore="ExtraText">
+ tools:targetApi="31">
@@ -72,6 +74,9 @@
+
@@ -90,6 +95,7 @@
+
@@ -158,7 +164,8 @@
-
-
+
\ No newline at end of file
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 297137b43..a7c18a27a 100644
--- a/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt
@@ -16,6 +16,7 @@ class PatientListAdapter(
private var patients: List,
private val onPatientClick: ((Patient) -> Unit)? = null,
private val onAssignNurseClick: ((Patient) -> Unit)? = null,
+ private val onEditClick: ((Patient) -> Unit)? = null,
private val onDeleteClick: ((Patient) -> Unit)? = null,
) : RecyclerView.Adapter() {
inner class PatientViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -71,6 +72,10 @@ class PatientListAdapter(
onAssignNurseClick?.invoke(patient)
true
}
+ R.id.action_edit_patient -> {
+ onEditClick?.invoke(patient)
+ true
+ }
R.id.action_delete -> { // Handle delete click
onDeleteClick?.invoke(patient)
true
diff --git a/app/src/main/java/deakin/gopher/guardian/model/Patient.kt b/app/src/main/java/deakin/gopher/guardian/model/Patient.kt
index 968538510..954b00f8c 100644
--- a/app/src/main/java/deakin/gopher/guardian/model/Patient.kt
+++ b/app/src/main/java/deakin/gopher/guardian/model/Patient.kt
@@ -45,6 +45,11 @@ private fun calculateAge(
}
}
+data class AssignNurseRequest(
+ @SerializedName("nurseId") val nurseId: String,
+ @SerializedName("patientId") val patientId: String,
+)
+
data class AddPatientResponse(
@SerializedName("patient") val patient: Patient,
) : BaseModel()
@@ -59,3 +64,9 @@ data class PatientActivity(
data class AddPatientActivityResponse(
@SerializedName("activity") val activity: PatientActivity,
) : BaseModel()
+
+data class UpdatePatientRequest(
+ @SerializedName("fullName") val fullName: String,
+ @SerializedName("dateOfBirth") val dateOfBirth: String,
+ @SerializedName("gender") val gender: String,
+)
diff --git a/app/src/main/java/deakin/gopher/guardian/model/register/User.kt b/app/src/main/java/deakin/gopher/guardian/model/register/User.kt
index c906ee66e..930cc7620 100644
--- a/app/src/main/java/deakin/gopher/guardian/model/register/User.kt
+++ b/app/src/main/java/deakin/gopher/guardian/model/register/User.kt
@@ -6,19 +6,50 @@ import deakin.gopher.guardian.model.login.Role
import java.io.Serializable
data class User(
- @SerializedName("id") val id: String,
- @SerializedName("email") val email: String,
- @SerializedName("fullname") val name: String,
- @SerializedName("role") val roleName: String,
- @SerializedName("photoUrl") val photoUrl: String,
+ @SerializedName(value = "id", alternate = ["_id"]) val id: String = "",
+
+ @SerializedName(value = "fullname", alternate = ["fullName"]) val name: String = "",
+
+ @SerializedName("email") val email: String = "",
+
+ @SerializedName("role") val roleName: String? = null,
+
+ @SerializedName("photoUrl") val photoUrl: String? = null,
+
@SerializedName("organization") val organization: String? = null,
) : Serializable {
val role: Role
- get() {
- return Role.create(roleName)
- }
+ get() = Role.create(roleName ?: "")
}
+data class NurseListResponse(
+ @SerializedName("nurses") val nurses: List,
+)
+
+data class NurseListItem(
+ @SerializedName("_id") val id: String,
+ @SerializedName("fullname") val fullName: String?,
+ @SerializedName("email") val email: String?,
+ @SerializedName("photoUrl") val photoUrl: String? = null,
+ @SerializedName("role") val role: NurseRole?,
+) {
+ fun toUser(): User {
+ return User(
+ id = id,
+ email = email.orEmpty(),
+ name = fullName.orEmpty(),
+ roleName = role?.name.orEmpty(),
+ photoUrl = photoUrl.orEmpty(),
+ organization = null,
+ )
+ }
+}
+
+data class NurseRole(
+ @SerializedName("_id") val id: String,
+ @SerializedName("name") val name: String,
+)
+
data class RegisterRequest(
@SerializedName("email") val email: String,
@SerializedName("password") val password: String,
@@ -29,4 +60,4 @@ data class RegisterRequest(
data class AuthResponse(
@SerializedName("user") val user: User,
@SerializedName("token") val token: String,
-) : BaseModel()
+) : BaseModel()
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/services/api/ApiClient.kt b/app/src/main/java/deakin/gopher/guardian/services/api/ApiClient.kt
index b98828cc9..ec0c258a4 100644
--- a/app/src/main/java/deakin/gopher/guardian/services/api/ApiClient.kt
+++ b/app/src/main/java/deakin/gopher/guardian/services/api/ApiClient.kt
@@ -7,18 +7,18 @@ import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
// private const val BASE_URL = "http://10.0.2.2:3000/api/v1/"
+
private const val BASE_URL = "https://guardian-backend-ashen.vercel.app/api/v1/"
+// private const val BASE_URL = "https://guardian-backend-git-fix-cors-patelrudra2306-5873s-projects.vercel.app/api/v1/"
+
private val client = OkHttpClient()
private val interceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
private val clientBuilder = client.newBuilder().addInterceptor(interceptor)
val retrofit: Retrofit by lazy {
- Retrofit.Builder()
- .baseUrl(BASE_URL)
- .addConverterFactory(GsonConverterFactory.create())
- .client(clientBuilder.build())
- .build()
+ Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
+ .client(clientBuilder.build()).build()
}
}
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..0023d5fa6 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
@@ -1,12 +1,16 @@
package deakin.gopher.guardian.services.api
+
import deakin.gopher.guardian.model.AddPatientActivityResponse
import deakin.gopher.guardian.model.AddPatientResponse
+import deakin.gopher.guardian.model.AssignNurseRequest
+import deakin.gopher.guardian.model.UpdatePatientRequest
import deakin.gopher.guardian.model.BaseModel
import deakin.gopher.guardian.model.Patient
import deakin.gopher.guardian.model.PatientActivity
import deakin.gopher.guardian.model.register.AuthResponse
import deakin.gopher.guardian.model.register.RegisterRequest
+import deakin.gopher.guardian.model.register.NurseListResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
@@ -19,6 +23,7 @@ import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
+import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Path
import retrofit2.http.Query
@@ -70,6 +75,38 @@ interface ApiService {
@Part photo: MultipartBody.Part?,
): Response
+ @GET("nurse/all")
+ suspend fun getAllNurses(
+ @Header("Authorization") token: String,
+ @Query("q") query: String? = null,
+ @Query("page") page: Int = 1,
+ @Query("limit") limit: Int = 50,
+ ): Response
+
+ @PUT("patients/{patientId}")
+ suspend fun updatePatient(
+ @Header("Authorization") token: String,
+ @Path("patientId") patientId: String,
+ @Body request: UpdatePatientRequest,
+ ): Response
+
+ @Multipart
+ @PUT("patients/{patientId}")
+ suspend fun updatePatientWithPhoto(
+ @Header("Authorization") token: String,
+ @Path("patientId") patientId: String,
+ @Part("fullName") fullName: RequestBody,
+ @Part("dateOfBirth") dateOfBirth: RequestBody,
+ @Part("gender") gender: RequestBody,
+ @Part photo: MultipartBody.Part,
+ ): Response
+
+ @POST("patients/assign-nurse")
+ suspend fun assignNurseToPatient(
+ @Header("Authorization") token: String,
+ @Body request: AssignNurseRequest,
+ ): Response
+
@FormUrlEncoded
@POST("patients/entryreport")
suspend fun logPatientActivity(
@@ -91,4 +128,4 @@ interface ApiService {
@Header("Authorization") token: String,
@Path("id") patientId: String,
): Response
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/AddNewPatientActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/AddNewPatientActivity.kt
index df96346ac..df89ecf20 100644
--- a/app/src/main/java/deakin/gopher/guardian/view/general/AddNewPatientActivity.kt
+++ b/app/src/main/java/deakin/gopher/guardian/view/general/AddNewPatientActivity.kt
@@ -34,6 +34,8 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream
import java.util.Calendar
+import androidx.core.widget.doAfterTextChanged
+import com.google.android.material.textfield.TextInputLayout
import java.util.Locale
class AddNewPatientActivity : BaseActivity() {
@@ -44,6 +46,8 @@ class AddNewPatientActivity : BaseActivity() {
companion object {
private const val CAMERA_PERMISSION_CODE = 1001
+ private const val MAX_ALLOWED_AGE = 120
+ private val NAME_REGEX = Regex("^[A-Za-z][A-Za-z .'-]{1,49}$")
}
// Launcher for picking image from gallery
@@ -69,7 +73,9 @@ class AddNewPatientActivity : BaseActivity() {
val localBinding = binding
when (localBinding) {
is ActivityAddNewPatientBinding -> localBinding.imgPreview.setImageBitmap(bitmap)
- is ActivityAddNewPatientNurseBinding -> localBinding.imgPreview.setImageBitmap(bitmap)
+ is ActivityAddNewPatientNurseBinding -> localBinding.imgPreview.setImageBitmap(
+ bitmap
+ )
}
}
}
@@ -98,6 +104,7 @@ class AddNewPatientActivity : BaseActivity() {
}
setupUI(localBinding)
}
+
is ActivityAddNewPatientNurseBinding -> {
setSupportActionBar(localBinding.toolbar)
localBinding.toolbar.setNavigationOnClickListener {
@@ -114,13 +121,16 @@ class AddNewPatientActivity : BaseActivity() {
is ActivityAddNewPatientBinding -> {
setupGenderSpinner(localBinding.genderSpinner)
setupDOBPicker(localBinding.txtDob)
+ setupValidationListeners(localBinding.txtName, localBinding.txtDob)
localBinding.btnSelectFromGallery.setOnClickListener { openGallery() }
localBinding.btnTakePhoto.setOnClickListener { checkCameraPermissionAndOpen() }
localBinding.btnSave.setOnClickListener { savePatientInfo() }
}
+
is ActivityAddNewPatientNurseBinding -> {
setupGenderSpinner(localBinding.genderSpinner)
setupDOBPicker(localBinding.txtDob)
+ setupValidationListeners(localBinding.txtName, localBinding.txtDob)
localBinding.btnSelectFromGallery.setOnClickListener { openGallery() }
localBinding.btnTakePhoto.setOnClickListener { checkCameraPermissionAndOpen() }
localBinding.btnSave.setOnClickListener { savePatientInfo() }
@@ -142,53 +152,53 @@ class AddNewPatientActivity : BaseActivity() {
val month = calendar.get(Calendar.MONTH)
val day = calendar.get(Calendar.DAY_OF_MONTH)
- val datePickerDialog =
- DatePickerDialog(
- this,
- Theme_Holo_Light_Dialog,
- { _, selectedYear, selectedMonth, selectedDay ->
- val formattedDate =
- String.format(
- Locale.getDefault(),
- "%04d-%02d-%02d",
- selectedYear,
- selectedMonth + 1,
- selectedDay,
- )
- txtDob.setText(formattedDate)
- },
- year,
- month,
- day,
- )
+ val datePickerDialog = DatePickerDialog(
+ this,
+ Theme_Holo_Light_Dialog,
+ { _, selectedYear, selectedMonth, selectedDay ->
+ val formattedDate = String.format(
+ Locale.getDefault(),
+ "%04d-%02d-%02d",
+ selectedYear,
+ selectedMonth + 1,
+ selectedDay,
+ )
+ txtDob.setText(formattedDate)
+ updateAgeField(selectedYear, selectedMonth, selectedDay)
+ },
+ year,
+ month,
+ day,
+ )
// Force spinner mode (for API 21+)
try {
- val datePickerField = datePickerDialog.datePicker.javaClass.getDeclaredField("mDelegate")
+ val datePickerField =
+ datePickerDialog.datePicker.javaClass.getDeclaredField("mDelegate")
datePickerField.isAccessible = true
val delegate = datePickerField.get(datePickerDialog.datePicker)
val spinnerDelegateClass = Class.forName("android.widget.DatePickerSpinnerDelegate")
if (delegate.javaClass != spinnerDelegateClass) {
- datePickerField.set(datePickerDialog.datePicker, null) // Clear the current delegate
-
- val constructor =
- datePickerDialog.datePicker.javaClass.getDeclaredConstructor(
- Context::class.java,
- android.util.AttributeSet::class.java,
- Int::class.javaPrimitiveType,
- Int::class.javaPrimitiveType,
- )
+ datePickerField.set(
+ datePickerDialog.datePicker, null
+ ) // Clear the current delegate
+
+ val constructor = datePickerDialog.datePicker.javaClass.getDeclaredConstructor(
+ Context::class.java,
+ android.util.AttributeSet::class.java,
+ Int::class.javaPrimitiveType,
+ Int::class.javaPrimitiveType,
+ )
constructor.isAccessible = true
- val spinnerDelegate =
- constructor.newInstance(
- datePickerDialog.datePicker.context,
- null,
- android.R.attr.datePickerStyle,
- 0,
- )
+ val spinnerDelegate = constructor.newInstance(
+ datePickerDialog.datePicker.context,
+ null,
+ android.R.attr.datePickerStyle,
+ 0,
+ )
datePickerField.set(datePickerDialog.datePicker, spinnerDelegate)
// Re-initialize the date picker with current date
@@ -205,8 +215,10 @@ class AddNewPatientActivity : BaseActivity() {
// Permission check before opening camera
private fun checkCameraPermissionAndOpen() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
- == PackageManager.PERMISSION_GRANTED
+ if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_GRANTED
) {
openCamera()
} else {
@@ -246,37 +258,37 @@ class AddNewPatientActivity : BaseActivity() {
val localBinding = binding
- val fullname =
- when (localBinding) {
- is ActivityAddNewPatientBinding -> localBinding.txtName.text.toString().trim()
- is ActivityAddNewPatientNurseBinding -> localBinding.txtName.text.toString().trim()
- else -> ""
- }
+ val fullname = when (localBinding) {
+ is ActivityAddNewPatientBinding -> localBinding.txtName.text.toString().trim()
+ is ActivityAddNewPatientNurseBinding -> localBinding.txtName.text.toString().trim()
+ else -> ""
+ }
- val dob =
- when (localBinding) {
- is ActivityAddNewPatientBinding -> localBinding.txtDob.text.toString().trim()
- is ActivityAddNewPatientNurseBinding -> localBinding.txtDob.text.toString().trim()
- else -> ""
- }
+ val dob = when (localBinding) {
+ is ActivityAddNewPatientBinding -> localBinding.txtDob.text.toString().trim()
+ is ActivityAddNewPatientNurseBinding -> localBinding.txtDob.text.toString().trim()
+ else -> ""
+ }
- val gender =
- when (localBinding) {
- is ActivityAddNewPatientBinding -> localBinding.genderSpinner.selectedItem?.toString()?.lowercase() ?: ""
- is ActivityAddNewPatientNurseBinding -> localBinding.genderSpinner.selectedItem?.toString()?.lowercase() ?: ""
- else -> ""
- }
+ val gender = when (localBinding) {
+ is ActivityAddNewPatientBinding -> localBinding.genderSpinner.selectedItem?.toString()
+ ?.lowercase() ?: ""
+
+ is ActivityAddNewPatientNurseBinding -> localBinding.genderSpinner.selectedItem?.toString()
+ ?.lowercase() ?: ""
+
+ else -> ""
+ }
val namePart = fullname.toRequestBody("text/plain".toMediaTypeOrNull())
val dobPart = dob.toRequestBody("text/plain".toMediaTypeOrNull())
val genderPart = gender.toRequestBody("text/plain".toMediaTypeOrNull())
- val photoPart: MultipartBody.Part? =
- when {
- selectedPhotoUri != null -> prepareFilePart("photo", selectedPhotoUri!!, this)
- capturedPhotoBitmap != null -> prepareBitmapPart("photo", capturedPhotoBitmap!!)
- else -> null
- }
+ val photoPart: MultipartBody.Part? = when {
+ selectedPhotoUri != null -> prepareFilePart("photo", selectedPhotoUri!!, this)
+ capturedPhotoBitmap != null -> prepareBitmapPart("photo", capturedPhotoBitmap!!)
+ else -> null
+ }
val token = "Bearer ${SessionManager.getToken()}"
@@ -287,6 +299,7 @@ class AddNewPatientActivity : BaseActivity() {
localBinding.progressBar.show()
localBinding.btnSave.visibility = View.GONE
}
+
is ActivityAddNewPatientNurseBinding -> {
localBinding.progressBar.show()
localBinding.btnSave.visibility = View.GONE
@@ -294,7 +307,8 @@ class AddNewPatientActivity : BaseActivity() {
}
}
- val response = ApiClient.apiService.addPatient(token, namePart, dobPart, genderPart, photoPart)
+ val response =
+ ApiClient.apiService.addPatient(token, namePart, dobPart, genderPart, photoPart)
withContext(Dispatchers.Main) {
when (localBinding) {
@@ -302,6 +316,7 @@ class AddNewPatientActivity : BaseActivity() {
localBinding.progressBar.hide()
localBinding.btnSave.visibility = View.VISIBLE
}
+
is ActivityAddNewPatientNurseBinding -> {
localBinding.progressBar.hide()
localBinding.btnSave.visibility = View.VISIBLE
@@ -316,26 +331,55 @@ class AddNewPatientActivity : BaseActivity() {
showMessage(response.body()?.apiError ?: "Failed to add patient")
}
} else {
- val errorResponse =
- Gson().fromJson(
- response.errorBody()?.string(),
- ApiErrorResponse::class.java,
- )
- showMessage(errorResponse.apiError ?: response.message())
+ val errorBody = response.errorBody()?.string()
+ val errorResponse = try {
+ Gson().fromJson(errorBody, ApiErrorResponse::class.java)
+ } catch (e: Exception) {
+ null
+ }
+ showMessage(errorResponse?.apiError ?: response.message())
}
}
}
}
+ private fun setupValidationListeners(
+ nameEditText: android.widget.EditText,
+ dobEditText: android.widget.EditText,
+ ) {
+ nameEditText.doAfterTextChanged {
+ findViewById(R.id.nameInputLayout)?.error = null
+ }
+
+ dobEditText.doAfterTextChanged {
+ findViewById(R.id.dobInputLayout)?.error = null
+ }
+ }
+
private fun validateInputs(): Boolean {
+ clearErrors()
+
val localBinding = binding
return when (localBinding) {
is ActivityAddNewPatientBinding -> {
- if (localBinding.txtName.text.toString().trim().isEmpty()) {
- showMessage(getString(R.string.validation_empty_name))
+ val name =
+ localBinding.txtName.text.toString().trim().replace("\\s+".toRegex(), " ")
+ val dobText = localBinding.txtDob.text.toString().trim()
+
+ if (name.isEmpty()) {
+ setNameError(getString(R.string.validation_empty_name))
+ false
+ } else if (name.length < 2) {
+ setNameError("Name must be at least 2 characters")
false
- } else if (localBinding.txtDob.text.toString().trim().isEmpty()) {
- showMessage(getString(R.string.validation_empty_dob))
+ } else if (!NAME_REGEX.matches(name)) {
+ setNameError("Name can contain only letters, spaces, apostrophes, dots or hyphens")
+ false
+ } else if (dobText.isEmpty()) {
+ setDobError(getString(R.string.validation_empty_dob))
+ false
+ } else if (!isValidDob(dobText)) {
+ setDobError("Please select a valid date of birth")
false
} else if (localBinding.genderSpinner.selectedItemPosition == 0) {
showMessage(getString(R.string.validation_empty_gender))
@@ -344,12 +388,26 @@ class AddNewPatientActivity : BaseActivity() {
true
}
}
+
is ActivityAddNewPatientNurseBinding -> {
- if (localBinding.txtName.text.toString().trim().isEmpty()) {
- showMessage(getString(R.string.validation_empty_name))
+ val name =
+ localBinding.txtName.text.toString().trim().replace("\\s+".toRegex(), " ")
+ val dobText = localBinding.txtDob.text.toString().trim()
+
+ if (name.isEmpty()) {
+ setNameError(getString(R.string.validation_empty_name))
+ false
+ } else if (name.length < 2) {
+ setNameError("Name must be at least 2 characters")
+ false
+ } else if (!NAME_REGEX.matches(name)) {
+ setNameError("Name can contain only letters, spaces, apostrophes, dots or hyphens")
+ false
+ } else if (dobText.isEmpty()) {
+ setDobError(getString(R.string.validation_empty_dob))
false
- } else if (localBinding.txtDob.text.toString().trim().isEmpty()) {
- showMessage(getString(R.string.validation_empty_dob))
+ } else if (!isValidDob(dobText)) {
+ setDobError("Please select a valid date of birth")
false
} else if (localBinding.genderSpinner.selectedItemPosition == 0) {
showMessage(getString(R.string.validation_empty_gender))
@@ -358,10 +416,55 @@ class AddNewPatientActivity : BaseActivity() {
true
}
}
+
else -> false
}
}
+ private fun clearErrors() {
+ findViewById(R.id.nameInputLayout)?.error = null
+ findViewById(R.id.dobInputLayout)?.error = null
+ }
+
+ private fun setNameError(message: String) {
+ findViewById(R.id.nameInputLayout)?.error = message
+
+ when (val localBinding = binding) {
+ is ActivityAddNewPatientBinding -> localBinding.txtName.requestFocus()
+ is ActivityAddNewPatientNurseBinding -> localBinding.txtName.requestFocus()
+ }
+ }
+
+ private fun setDobError(message: String) {
+ findViewById(R.id.dobInputLayout)?.error = message
+
+ when (val localBinding = binding) {
+ is ActivityAddNewPatientBinding -> localBinding.txtDob.requestFocus()
+ is ActivityAddNewPatientNurseBinding -> localBinding.txtDob.requestFocus()
+ }
+ }
+
+ private fun isValidDob(dobText: String): Boolean {
+ val dobParts = dobText.split("-")
+ if (dobParts.size != 3) return false
+
+ val year = dobParts[0].toIntOrNull() ?: return false
+ val month = dobParts[1].toIntOrNull()?.minus(1) ?: return false
+ val day = dobParts[2].toIntOrNull() ?: return false
+
+ val age = calculateAge(year, month, day)
+ return age in 0..MAX_ALLOWED_AGE
+ }
+
+ private fun updateAgeField(year: Int, month: Int, day: Int) {
+ val age = calculateAge(year, month, day).coerceAtLeast(0)
+
+ when (val localBinding = binding) {
+ is ActivityAddNewPatientBinding -> localBinding.txtAge.setText(age.toString())
+ is ActivityAddNewPatientNurseBinding -> localBinding.txtAge.setText(age.toString())
+ }
+ }
+
private fun showMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt
new file mode 100644
index 000000000..49c0b7c38
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt
@@ -0,0 +1,203 @@
+package deakin.gopher.guardian.view.general
+
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.gson.Gson
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.adapter.NurseListAdapter
+import deakin.gopher.guardian.databinding.ActivityAssignNurseBinding
+import deakin.gopher.guardian.model.ApiErrorResponse
+import deakin.gopher.guardian.model.login.Role
+import deakin.gopher.guardian.model.login.SessionManager
+import deakin.gopher.guardian.model.AssignNurseRequest
+import deakin.gopher.guardian.model.register.User
+import deakin.gopher.guardian.services.api.ApiClient
+import deakin.gopher.guardian.view.hide
+import deakin.gopher.guardian.view.show
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import deakin.gopher.guardian.model.register.NurseListItem
+import deakin.gopher.guardian.model.register.NurseListResponse
+import kotlinx.coroutines.withContext
+
+class AssignNurseActivity : BaseActivity() {
+ private lateinit var binding: ActivityAssignNurseBinding
+ private lateinit var nurseListAdapter: NurseListAdapter
+
+ private var patientId: String = ""
+ private var patientName: String = ""
+
+ private val currentUser = SessionManager.getCurrentUser()
+
+ companion object {
+ const val EXTRA_PATIENT_ID = "patientId"
+ const val EXTRA_PATIENT_NAME = "patientName"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityAssignNurseBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ patientId = intent.getStringExtra(EXTRA_PATIENT_ID).orEmpty()
+ patientName = intent.getStringExtra(EXTRA_PATIENT_NAME).orEmpty()
+
+ if (patientId.isBlank()) {
+ showMessage("Patient information is missing")
+ finish()
+ return
+ }
+
+ setupToolbar()
+ setupRecyclerView()
+ setupUi()
+ fetchNurses()
+ }
+
+ private fun setupToolbar() {
+ setSupportActionBar(binding.toolbar)
+ binding.toolbar.setNavigationOnClickListener {
+ onBackPressedDispatcher.onBackPressed()
+ }
+
+ if (currentUser.role == Role.Nurse) {
+ binding.toolbar.setBackgroundColor(getColor(R.color.TG_blue))
+ }
+ }
+
+ private fun setupUi() {
+ binding.tvSelectedPatient.text = if (patientName.isNotBlank()) {
+ "Assign a nurse to $patientName"
+ } else {
+ "Assign a nurse"
+ }
+ }
+
+ private fun setupRecyclerView() {
+ nurseListAdapter = NurseListAdapter(emptyList()) { nurse ->
+ showAssignConfirmation(nurse)
+ }
+
+ binding.recyclerViewNurses.layoutManager = LinearLayoutManager(this)
+ binding.recyclerViewNurses.adapter = nurseListAdapter
+ }
+
+ private fun fetchNurses() {
+ val token = "Bearer ${SessionManager.getToken()}"
+
+ CoroutineScope(Dispatchers.IO).launch {
+ withContext(Dispatchers.Main) {
+ binding.progressBar.show()
+ }
+
+ val response = try {
+ ApiClient.apiService.getAllNurses(
+ token = token,
+ page = 1,
+ limit = 50,
+ )
+ } catch (e: Exception) {
+ null
+ }
+
+ withContext(Dispatchers.Main) {
+ binding.progressBar.hide()
+
+ if (response?.isSuccessful == true) {
+ val nurses = response.body()?.nurses.orEmpty().map { it.toUser() }
+ .filter { it.role == Role.Nurse }
+
+ if (nurses.isNotEmpty()) {
+ nurseListAdapter.updateData(nurses)
+ binding.tvEmptyMessage.visibility = android.view.View.GONE
+ } else {
+ binding.tvEmptyMessage.visibility = android.view.View.VISIBLE
+ }
+ } else {
+ val rawErrorBody = try {
+ response?.errorBody()?.string()
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorResponse = try {
+ Gson().fromJson(rawErrorBody, ApiErrorResponse::class.java)
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorMessage = errorResponse?.apiError?.takeIf { it.isNotBlank() }
+ ?: rawErrorBody?.takeIf { it.isNotBlank() } ?: response?.message()
+ ?.takeIf { it.isNotBlank() } ?: "Failed to load nurses"
+
+ showMessage(errorMessage)
+ }
+ }
+ }
+ }
+
+ private fun showAssignConfirmation(nurse: User) {
+ AlertDialog.Builder(this).setTitle("Assign Nurse")
+ .setMessage("Assign ${nurse.name} to ${patientName.ifBlank { "this patient" }}?")
+ .setPositiveButton("Assign") { _, _ ->
+ assignNurseToPatient(nurse)
+ }.setNegativeButton("Cancel", null).show()
+ }
+
+ private fun assignNurseToPatient(nurse: User) {
+ val token = "Bearer ${SessionManager.getToken()}"
+
+ CoroutineScope(Dispatchers.IO).launch {
+ withContext(Dispatchers.Main) {
+ binding.progressBar.show()
+ }
+
+ val response = try {
+ ApiClient.apiService.assignNurseToPatient(
+ token = token,
+ request = AssignNurseRequest(
+ nurseId = nurse.id,
+ patientId = patientId,
+ ),
+ )
+ } catch (e: Exception) {
+ null
+ }
+
+ withContext(Dispatchers.Main) {
+ binding.progressBar.hide()
+
+ if (response?.isSuccessful == true) {
+ showMessage("Nurse assigned successfully")
+ setResult(RESULT_OK)
+ finish()
+ } else {
+ val rawErrorBody = try {
+ response?.errorBody()?.string()
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorResponse = try {
+ Gson().fromJson(rawErrorBody, ApiErrorResponse::class.java)
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorMessage = errorResponse?.apiError?.takeIf { it.isNotBlank() }
+ ?: rawErrorBody?.takeIf { it.isNotBlank() } ?: response?.message()
+ ?.takeIf { it.isNotBlank() } ?: "Failed to assign nurse"
+
+ showMessage(errorMessage)
+ }
+ }
+ }
+ }
+
+ 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/EditPatientActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt
new file mode 100644
index 000000000..f8920f7f5
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt
@@ -0,0 +1,415 @@
+package deakin.gopher.guardian.view.general
+
+import android.Manifest
+import android.R.style.Theme_Holo_Light_Dialog
+import android.app.DatePickerDialog
+import android.content.Context
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.core.widget.doAfterTextChanged
+import com.bumptech.glide.Glide
+import com.google.android.material.textfield.TextInputLayout
+import com.google.gson.Gson
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.databinding.ActivityEditPatientBinding
+import deakin.gopher.guardian.model.ApiErrorResponse
+import deakin.gopher.guardian.model.Patient
+import deakin.gopher.guardian.model.UpdatePatientRequest
+import deakin.gopher.guardian.model.login.Role
+import deakin.gopher.guardian.model.login.SessionManager
+import deakin.gopher.guardian.services.api.ApiClient
+import deakin.gopher.guardian.view.hide
+import deakin.gopher.guardian.view.show
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.io.ByteArrayOutputStream
+import java.util.Calendar
+import java.util.Locale
+
+class EditPatientActivity : BaseActivity() {
+ private lateinit var binding: ActivityEditPatientBinding
+ private lateinit var patient: Patient
+
+ private var selectedPhotoUri: Uri? = null
+ private var capturedPhotoBitmap: Bitmap? = null
+
+ companion object {
+ const val EXTRA_PATIENT = "patient"
+ private const val CAMERA_PERMISSION_CODE = 1002
+ private const val MAX_ALLOWED_AGE = 120
+ private val NAME_REGEX = Regex("^[A-Za-z][A-Za-z .'-]{1,49}$")
+ }
+
+ private val galleryLauncher =
+ registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
+ if (uri != null) {
+ selectedPhotoUri = uri
+ capturedPhotoBitmap = null
+ binding.imgPreview.setImageURI(uri)
+ }
+ }
+
+ private val cameraLauncher =
+ registerForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap: Bitmap? ->
+ if (bitmap != null) {
+ capturedPhotoBitmap = bitmap
+ selectedPhotoUri = null
+ binding.imgPreview.setImageBitmap(bitmap)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityEditPatientBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ patient = intent.getSerializableExtra(EXTRA_PATIENT) as? Patient ?: run {
+ showMessage("Patient data missing")
+ finish()
+ return
+ }
+
+ setSupportActionBar(binding.toolbar)
+ binding.toolbar.setNavigationOnClickListener {
+ onBackPressedDispatcher.onBackPressed()
+ }
+
+ if (SessionManager.getCurrentUser().role == Role.Nurse) {
+ binding.toolbar.setBackgroundColor(getColor(R.color.TG_blue))
+ }
+
+ setupGenderSpinner()
+ setupDOBPicker()
+ setupValidationListeners()
+ populatePatientData()
+
+ binding.btnSelectFromGallery.setOnClickListener { openGallery() }
+ binding.btnTakePhoto.setOnClickListener { checkCameraPermissionAndOpen() }
+ binding.btnSave.setOnClickListener { updatePatientInfo() }
+ }
+
+ private fun setupGenderSpinner() {
+ val genderOptions = listOf("Select gender", "Male", "Female", "Other")
+ val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, genderOptions)
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ binding.genderSpinner.adapter = adapter
+ }
+
+ private fun populatePatientData() {
+ binding.txtName.setText(patient.fullname)
+ binding.txtDob.setText(patient.dateOfBirth.substringBefore("T"))
+ binding.txtAge.setText(patient.age.toString())
+
+ when (patient.gender.lowercase()) {
+ "male" -> binding.genderSpinner.setSelection(1)
+ "female" -> binding.genderSpinner.setSelection(2)
+ "other" -> binding.genderSpinner.setSelection(3)
+ else -> binding.genderSpinner.setSelection(0)
+ }
+
+ Glide.with(this).load(patient.photoUrl).placeholder(R.drawable.profile).circleCrop()
+ .into(binding.imgPreview)
+ }
+
+ private fun setupDOBPicker() {
+ binding.txtDob.setOnClickListener {
+ val calendar = Calendar.getInstance()
+ val year = calendar.get(Calendar.YEAR)
+ val month = calendar.get(Calendar.MONTH)
+ val day = calendar.get(Calendar.DAY_OF_MONTH)
+
+ val datePickerDialog = DatePickerDialog(
+ this,
+ Theme_Holo_Light_Dialog,
+ { _, selectedYear, selectedMonth, selectedDay ->
+ val formattedDate = String.format(
+ Locale.getDefault(),
+ "%04d-%02d-%02d",
+ selectedYear,
+ selectedMonth + 1,
+ selectedDay,
+ )
+ binding.txtDob.setText(formattedDate)
+ updateAgeField(selectedYear, selectedMonth, selectedDay)
+ },
+ year,
+ month,
+ day,
+ )
+
+ datePickerDialog.datePicker.maxDate = System.currentTimeMillis()
+ datePickerDialog.show()
+ }
+ }
+
+ private fun setupValidationListeners() {
+ binding.txtName.doAfterTextChanged {
+ binding.nameInputLayout.error = null
+ }
+ binding.txtDob.doAfterTextChanged {
+ binding.dobInputLayout.error = null
+ }
+ }
+
+ private fun checkCameraPermissionAndOpen() {
+ if (ContextCompat.checkSelfPermission(
+ this, Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ openCamera()
+ } else {
+ ActivityCompat.requestPermissions(
+ this,
+ arrayOf(Manifest.permission.CAMERA),
+ CAMERA_PERMISSION_CODE,
+ )
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray,
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == CAMERA_PERMISSION_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ openCamera()
+ } else {
+ showMessage("Camera permission is required to take photos")
+ }
+ }
+ }
+
+ private fun openGallery() {
+ galleryLauncher.launch("image/*")
+ }
+
+ private fun openCamera() {
+ cameraLauncher.launch(null)
+ }
+
+ private fun updatePatientInfo() {
+ if (!validateInputs()) return
+
+ val fullName = binding.txtName.text.toString().trim().replace("\\s+".toRegex(), " ")
+ val dob = binding.txtDob.text.toString().trim()
+ val gender = binding.genderSpinner.selectedItem?.toString()?.trim()?.lowercase() ?: ""
+
+ if (!hasChanges(fullName, dob, gender)) {
+ showMessage("No changes to update")
+ return
+ }
+
+ val token = "Bearer ${SessionManager.getToken()}"
+
+ CoroutineScope(Dispatchers.IO).launch {
+ withContext(Dispatchers.Main) {
+ binding.progressBar.show()
+ binding.btnSave.visibility = android.view.View.GONE
+ }
+
+ val response = try {
+ val photoPart = when {
+ selectedPhotoUri != null -> prepareFilePart(
+ "photo", selectedPhotoUri!!, this@EditPatientActivity
+ )
+
+ capturedPhotoBitmap != null -> prepareBitmapPart("photo", capturedPhotoBitmap!!)
+ else -> null
+ }
+
+ if (photoPart != null) {
+ val fullNamePart = fullName.toRequestBody("text/plain".toMediaTypeOrNull())
+ val dobPart = dob.toRequestBody("text/plain".toMediaTypeOrNull())
+ val genderPart = gender.toRequestBody("text/plain".toMediaTypeOrNull())
+
+ ApiClient.apiService.updatePatientWithPhoto(
+ token = token,
+ patientId = patient.id,
+ fullName = fullNamePart,
+ dateOfBirth = dobPart,
+ gender = genderPart,
+ photo = photoPart,
+ )
+ } else {
+ ApiClient.apiService.updatePatient(
+ token = token,
+ patientId = patient.id,
+ request = UpdatePatientRequest(
+ fullName = fullName,
+ dateOfBirth = dob,
+ gender = gender,
+ ),
+ )
+ }
+ } catch (e: Exception) {
+ null
+ }
+
+ withContext(Dispatchers.Main) {
+ binding.progressBar.hide()
+ binding.btnSave.visibility = android.view.View.VISIBLE
+
+ if (response?.isSuccessful == true) {
+ showMessage(response.body()?.apiMessage ?: "Patient updated successfully")
+ finish()
+ } else {
+ val rawErrorBody = try {
+ response?.errorBody()?.string()
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorResponse = try {
+ Gson().fromJson(rawErrorBody, ApiErrorResponse::class.java)
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorMessage = errorResponse?.apiError?.takeIf { it.isNotBlank() }
+ ?: rawErrorBody?.takeIf { it.isNotBlank() } ?: response?.message()
+ ?.takeIf { it.isNotBlank() } ?: "Failed to update patient"
+
+ showMessage(errorMessage)
+ }
+ }
+ }
+ }
+
+ private fun validateInputs(): Boolean {
+ clearErrors()
+
+ val name = binding.txtName.text.toString().trim().replace("\\s+".toRegex(), " ")
+ val dobText = binding.txtDob.text.toString().trim()
+
+ return when {
+ name.isEmpty() -> {
+ setNameError(getString(R.string.validation_empty_name))
+ false
+ }
+
+ name.length < 2 -> {
+ setNameError("Name must be at least 2 characters")
+ false
+ }
+
+ !NAME_REGEX.matches(name) -> {
+ setNameError("Name can contain only letters, spaces, apostrophes, dots or hyphens")
+ false
+ }
+
+ dobText.isEmpty() -> {
+ setDobError(getString(R.string.validation_empty_dob))
+ false
+ }
+
+ !isValidDob(dobText) -> {
+ setDobError("Please select a valid date of birth")
+ false
+ }
+
+ binding.genderSpinner.selectedItemPosition == 0 -> {
+ showMessage(getString(R.string.validation_empty_gender))
+ false
+ }
+
+ else -> true
+ }
+ }
+
+ private fun clearErrors() {
+ binding.nameInputLayout.error = null
+ binding.dobInputLayout.error = null
+ }
+
+ private fun setNameError(message: String) {
+ binding.nameInputLayout.error = message
+ binding.txtName.requestFocus()
+ }
+
+ private fun setDobError(message: String) {
+ binding.dobInputLayout.error = message
+ binding.txtDob.requestFocus()
+ }
+
+ private fun isValidDob(dobText: String): Boolean {
+ val dobParts = dobText.split("-")
+ if (dobParts.size != 3) return false
+
+ val year = dobParts[0].toIntOrNull() ?: return false
+ val month = dobParts[1].toIntOrNull()?.minus(1) ?: return false
+ val day = dobParts[2].toIntOrNull() ?: return false
+
+ val age = calculateAge(year, month, day)
+ return age in 0..MAX_ALLOWED_AGE
+ }
+
+ private fun updateAgeField(year: Int, month: Int, day: Int) {
+ val age = calculateAge(year, month, day).coerceAtLeast(0)
+ binding.txtAge.setText(age.toString())
+ }
+
+ private fun calculateAge(year: Int, month: Int, day: Int): Int {
+ val today = Calendar.getInstance()
+ val birthDate = Calendar.getInstance()
+ birthDate.set(year, month, day)
+ var age = today.get(Calendar.YEAR) - birthDate.get(Calendar.YEAR)
+
+ if (today.get(Calendar.DAY_OF_YEAR) < birthDate.get(Calendar.DAY_OF_YEAR)) {
+ age--
+ }
+ return age
+ }
+
+ private fun prepareFilePart(
+ partName: String,
+ fileUri: Uri,
+ context: Context,
+ ): MultipartBody.Part? {
+ val inputStream = context.contentResolver.openInputStream(fileUri)
+ val fileBytes = inputStream?.readBytes() ?: return null
+ val requestFile = fileBytes.toRequestBody("image/*".toMediaTypeOrNull())
+ return MultipartBody.Part.createFormData(partName, "profile.jpg", requestFile)
+ }
+
+ private fun prepareBitmapPart(
+ partName: String,
+ bitmap: Bitmap,
+ ): MultipartBody.Part {
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
+ val byteArray = stream.toByteArray()
+ val requestFile = byteArray.toRequestBody("image/jpeg".toMediaTypeOrNull())
+ return MultipartBody.Part.createFormData(partName, "profile.jpg", requestFile)
+ }
+
+ private fun showMessage(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+
+ private fun hasChanges(
+ fullName: String,
+ dob: String,
+ gender: String,
+ ): Boolean {
+ val currentName = patient.fullname.trim().replace("\\s+".toRegex(), " ")
+ val currentDob = patient.dateOfBirth.substringBefore("T")
+ val currentGender = patient.gender.trim().lowercase()
+
+ return fullName != currentName || dob != currentDob || gender != currentGender || selectedPhotoUri != null || capturedPhotoBitmap != null
+ }
+
+}
\ 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 0e9f2ae4d..ce09bb646 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
@@ -53,30 +53,25 @@ class PatientDetailsActivity : BaseActivity() {
}
}"
- if (patient.healthConditions.isNotEmpty()) {
- val formattedConditions =
- patient.healthConditions.joinToString(", ") { condition ->
- condition.split(" ").joinToString(" ") { word ->
- word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
- }
+ if (!patient.healthConditions.isNullOrEmpty()) {
+ 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"
}
- Glide.with(this)
- .load(patient.photoUrl)
- .placeholder(R.drawable.profile)
- .circleCrop()
+ Glide.with(this).load(patient.photoUrl).placeholder(R.drawable.profile).circleCrop()
.into(binding.imagePatient)
// 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()
+ .replace(R.id.fragmentAssignedNursesContainer, nursesFragment).commit()
// Setup RecyclerView for activity logs
activitiesAdapter = PatientActivityAdapter(emptyList())
@@ -92,12 +87,13 @@ class PatientDetailsActivity : BaseActivity() {
withContext(Dispatchers.Main) {
binding.progressBar.visibility = View.VISIBLE
}
- val response =
- try {
- deakin.gopher.guardian.services.api.ApiClient.apiService.getPatientActivities(token, patientId)
- } catch (e: Exception) {
- null
- }
+ 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
@@ -111,12 +107,11 @@ class PatientDetailsActivity : BaseActivity() {
}
} else {
val errorBody = response?.errorBody()?.string()
- val errorResponse =
- try {
- Gson().fromJson(errorBody, ApiErrorResponse::class.java)
- } catch (ex: Exception) {
- null
- }
+ val errorResponse = try {
+ Gson().fromJson(errorBody, ApiErrorResponse::class.java)
+ } catch (ex: Exception) {
+ null
+ }
showMessage(errorResponse?.apiError ?: "Failed to load activities")
}
}
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 c2bbfe079..410a6abf8 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
@@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
-import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.gson.Gson
@@ -25,48 +24,41 @@ import kotlinx.coroutines.withContext
class PatientListActivity : BaseActivity() {
private lateinit var binding: ActivityPatientListBinding
- private val patientListAdapter =
- PatientListAdapter(
- emptyList(),
- onPatientClick = { patient ->
- val intent = Intent(this, PatientDetailsActivity::class.java)
- intent.putExtra("patient", patient)
- startActivity(intent)
- },
- onAssignNurseClick = { patient ->
-// val intent = Intent(this, AssignNurseActivity::class.java)
-// intent.putExtra("patientId", patient.id)
-// startActivity(intent)
- },
- onDeleteClick = { patient ->
- confirmDeletePatient(patient)
- },
- )
- private fun confirmDeletePatient(patient: Patient) {
- // Optional: show a confirmation dialog before deleting
- deletePatient(patient)
- }
-
- private fun deletePatient(patient: Patient) {
- val token = "Bearer ${SessionManager.getToken()}"
- CoroutineScope(Dispatchers.IO).launch {
- val response =
- try {
- ApiClient.apiService.deletePatient(token, patient.id)
- } catch (e: Exception) {
- null
- }
- withContext(Dispatchers.Main) {
- if (response?.isSuccessful == true) {
- showMessage("Patient deleted")
- fetchPatients() // Refresh the patient list
- } else {
- showMessage("Failed to delete patient")
- }
+ private val patientListAdapter = PatientListAdapter(
+ emptyList(),
+ onPatientClick = { patient ->
+ val intent = Intent(this, PatientDetailsActivity::class.java)
+ intent.putExtra("patient", patient)
+ startActivity(intent)
+ },
+ onAssignNurseClick = { patient ->
+ if (currentUser.role == Role.Nurse) {
+ Toast.makeText(
+ this, "Only caretaker can assign nurse to the patient", Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ val intent = Intent(this, AssignNurseActivity::class.java)
+ intent.putExtra(AssignNurseActivity.EXTRA_PATIENT_ID, patient.id)
+ intent.putExtra(AssignNurseActivity.EXTRA_PATIENT_NAME, patient.fullname)
+ startActivity(intent)
}
- }
- }
+ },
+ onEditClick = { patient ->
+ if (currentUser.role == Role.Nurse) {
+ Toast.makeText(
+ this, "Only caretaker can edit patient info", Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ val intent = Intent(this, EditPatientActivity::class.java)
+ intent.putExtra(EditPatientActivity.EXTRA_PATIENT, patient)
+ startActivity(intent)
+ }
+ },
+ onDeleteClick = { patient ->
+ confirmDeletePatient(patient)
+ },
+ )
private val currentUser = SessionManager.getCurrentUser()
@@ -93,38 +85,75 @@ class PatientListActivity : BaseActivity() {
fetchPatients()
}
+ private fun confirmDeletePatient(patient: Patient) {
+ deletePatient(patient)
+ }
+
+ private fun deletePatient(patient: Patient) {
+ val token = "Bearer ${SessionManager.getToken()}"
+ CoroutineScope(Dispatchers.IO).launch {
+ val response = try {
+ ApiClient.apiService.deletePatient(token, patient.id)
+ } catch (e: Exception) {
+ null
+ }
+
+ withContext(Dispatchers.Main) {
+ if (response?.isSuccessful == true) {
+ showMessage("Patient deleted")
+ fetchPatients()
+ } else {
+ showMessage("Failed to delete patient")
+ }
+ }
+ }
+ }
+
private fun fetchPatients() {
val token = "Bearer ${SessionManager.getToken()}"
+
CoroutineScope(Dispatchers.IO).launch {
if (patientListAdapter.itemCount <= 0) {
withContext(Dispatchers.Main) {
binding.progressBar.show()
}
}
- val response = ApiClient.apiService.getAssignedPatients(token)
+
+ val response = try {
+ ApiClient.apiService.getAssignedPatients(token)
+ } catch (e: Exception) {
+ null
+ }
+
withContext(Dispatchers.Main) {
- withContext(Dispatchers.Main) {
- binding.progressBar.hide()
- }
- if (response.isSuccessful) {
- if (!response.body().isNullOrEmpty()) {
- patientListAdapter.updateData(response.body()!!)
- withContext(Dispatchers.Main) {
- binding.tvEmptyMessage.visibility = View.GONE
- }
+ binding.progressBar.hide()
+
+ if (response?.isSuccessful == true) {
+ val patients = response.body().orEmpty()
+ if (patients.isNotEmpty()) {
+ patientListAdapter.updateData(patients)
+ binding.tvEmptyMessage.visibility = android.view.View.GONE
} else {
- withContext(Dispatchers.Main) {
- binding.tvEmptyMessage.visibility = View.VISIBLE
- }
+ binding.tvEmptyMessage.visibility = android.view.View.VISIBLE
}
} else {
- // Handle error
- val errorResponse =
- Gson().fromJson(
- response.errorBody()?.string(),
- ApiErrorResponse::class.java,
- )
- showMessage(errorResponse.apiError ?: response.message())
+ val rawErrorBody = try {
+ response?.errorBody()?.string()
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorResponse = try {
+ Gson().fromJson(rawErrorBody, ApiErrorResponse::class.java)
+ } catch (e: Exception) {
+ null
+ }
+
+ val errorMessage = errorResponse?.apiError?.takeIf { it.isNotBlank() }
+ ?: rawErrorBody?.takeIf { it.isNotBlank() } ?: response?.message()
+ ?.takeIf { it.isNotBlank() } ?: "Failed to load patients"
+
+ showMessage(errorMessage)
}
}
}
@@ -149,4 +178,4 @@ class PatientListActivity : 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/res/layout/activity_add_new_patient_nurse.xml b/app/src/main/res/layout/activity_add_new_patient_nurse.xml
index 05368bbcc..91d12ddff 100644
--- a/app/src/main/res/layout/activity_add_new_patient_nurse.xml
+++ b/app/src/main/res/layout/activity_add_new_patient_nurse.xml
@@ -1,6 +1,5 @@
-
-
+ app:subtitleTextColor="@android:color/white"
+ app:title="@string/add_new_patient"
+ app:titleCentered="true"
+ app:titleTextColor="@android:color/white" />
-
+ app:layout_constraintTop_toBottomOf="@id/toolbar">
-
+ android:hint="@string/full_name">
-
+ android:hint="@string/date_of_birth">
-
+ android:hint="Age">
-
+
+
+
+
+ android:textStyle="bold" />
-
-
-
+ app:iconTint="@color/teal_600" />
-
+ app:iconTint="@color/teal_600" />
-
-
-
-
-
-
-
-
+ app:cornerRadius="8dp" />
-
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_assign_nurse.xml b/app/src/main/res/layout/activity_assign_nurse.xml
new file mode 100644
index 000000000..762f02e32
--- /dev/null
+++ b/app/src/main/res/layout/activity_assign_nurse.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_patient.xml b/app/src/main/res/layout/activity_edit_patient.xml
new file mode 100644
index 000000000..e17871ac4
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit_patient.xml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_patient_details.xml b/app/src/main/res/layout/activity_patient_details.xml
index 64059d2d1..84975b43b 100644
--- a/app/src/main/res/layout/activity_patient_details.xml
+++ b/app/src/main/res/layout/activity_patient_details.xml
@@ -1,6 +1,5 @@
-
+ app:layout_constraintTop_toTopOf="parent" />
+ app:titleTextColor="@android:color/white" />
+ app:layout_constraintTop_toBottomOf="@id/toolbar">
+ android:paddingEnd="16dp">
-
+ android:text="Activities"
+ android:textSize="16sp"
+ android:textStyle="bold"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/containerPatientInfo" />
+ app:layout_constraintTop_toBottomOf="@id/tvAssignedNursesLabel" />
+ app:layout_constraintTop_toBottomOf="@id/fragmentAssignedNursesContainer" />
+ app:layout_constraintTop_toBottomOf="@id/fragmentAssignedNursesContainer" />
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/fragmentAssignedNursesContainer" />
diff --git a/app/src/main/res/menu/menu_patient_item.xml b/app/src/main/res/menu/menu_patient_item.xml
index 1f11cf04f..14de00e8d 100644
--- a/app/src/main/res/menu/menu_patient_item.xml
+++ b/app/src/main/res/menu/menu_patient_item.xml
@@ -3,6 +3,9 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9fe4ecab0..06ffe24c8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -41,7 +41,7 @@
Verified
Welcome, Caretaker
Welcome, Administrator
- Welcome, Nurse
+ Welcome,
@@ -705,7 +705,7 @@
To Do
Mark as Complete
You have not added any Patients yet.\nTap + to add.
- You have not assigned any Nurses to this Patient yet.\nTap + to add.
+ You have not assigned any Nurses to this Patient yet.\
No activity has been recorded for this Patient yet.
Patient photo
More Options
diff --git a/build.gradle b/build.gradle
index 558092873..353446a9c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,8 +12,8 @@ buildscript {
}
plugins {
- id 'com.android.application' version '8.5.1' apply false
- id 'com.android.library' version '8.5.1' apply false
+ id 'com.android.application' version '8.13.2' apply false
+ id 'com.android.library' version '8.13.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 404bb61a8..2c628d5ce 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Wed Aug 31 11:56:40 AEST 2022
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME