From a53f66d626296785f52bbe26b838fb77517b2c10 Mon Sep 17 00:00:00 2001 From: Kaustubh Kale Date: Fri, 3 Apr 2026 15:33:30 +1100 Subject: [PATCH 1/4] SPrint 1 changes -Kaustubh --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 3 + .../guardian/services/api/ApiService.kt | 2 +- .../view/general/AddNewPatientActivity.kt | 259 ++++++++++++------ .../view/general/AssignNurseActivity.kt | 187 +++++++++++++ .../view/general/PatientDetailsActivity.kt | 43 ++- .../view/general/PatientListActivity.kt | 138 +++++----- .../layout/activity_add_new_patient_nurse.xml | 131 ++++----- .../main/res/layout/activity_assign_nurse.xml | 84 ++++++ app/src/main/res/values/strings.xml | 2 +- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 12 files changed, 610 insertions(+), 247 deletions(-) create mode 100644 app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt create mode 100644 app/src/main/res/layout/activity_assign_nurse.xml 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..80adfbf0d 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/services/api/ApiService.kt b/app/src/main/java/deakin/gopher/guardian/services/api/ApiService.kt index 0e9ea177e..2382612f7 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 @@ -91,4 +91,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..83cf22987 --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt @@ -0,0 +1,187 @@ +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.register.User +import deakin.gopher.guardian.services.api.ApiClient +//import deakin.gopher.guardian.services.api.AssignNurseRequest +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 + +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) +// } catch (e: Exception) { +// null +// } +// +// withContext(Dispatchers.Main) { +// binding.progressBar.hide() +// +// if (response?.isSuccessful == true) { +// val nurses = response.body().orEmpty() +// +// if (nurses.isNotEmpty()) { +// nurseListAdapter.updateData(nurses) +// binding.tvEmptyMessage.visibility = android.view.View.GONE +// } else { +// binding.tvEmptyMessage.visibility = android.view.View.VISIBLE +// } +// } else { +// val errorResponse = +// try { +// Gson().fromJson( +// response?.errorBody()?.string(), +// ApiErrorResponse::class.java, +// ) +// } catch (e: Exception) { +// null +// } +// +// showMessage(errorResponse?.apiError ?: "Failed to load nurses") +// } +// } +// } +// } + +// 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, +// patientId = patientId, +// request = AssignNurseRequest(nurse.id), +// ) +// } catch (e: Exception) { +// null +// } +// +// withContext(Dispatchers.Main) { +// binding.progressBar.hide() +// +// if (response?.isSuccessful == true) { +// showMessage("Nurse assigned successfully") +// setResult(RESULT_OK) +// finish() +// } else { +// val errorResponse = +// try { +// Gson().fromJson( +// response?.errorBody()?.string(), +// ApiErrorResponse::class.java, +// ) +// } catch (e: Exception) { +// null +// } +// +// showMessage(errorResponse?.apiError ?: "Failed to assign nurse") +// } +// } +// } +// } + + 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/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..a40399468 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,24 @@ 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 -> + 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) + }, + onDeleteClick = { patient -> + confirmDeletePatient(patient) + }, + ) private val currentUser = SessionManager.getCurrentUser() @@ -93,38 +68,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 +161,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/values/strings.xml b/app/src/main/res/values/strings.xml index 9fe4ecab0..f33e06461 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, 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 From 666c4946d88aab99bd0379ea7afaab2df9f31cb6 Mon Sep 17 00:00:00 2001 From: Kaustubh Kale Date: Fri, 3 Apr 2026 16:01:22 +1100 Subject: [PATCH 2/4] Edit patient forms -Kaustubh --- app/src/main/AndroidManifest.xml | 14 +- .../guardian/adapter/PatientListAdapter.kt | 5 + .../view/general/EditPatientActivity.kt | 380 ++++++++++++++++++ .../view/general/PatientListActivity.kt | 5 + .../main/res/layout/activity_edit_patient.xml | 179 +++++++++ app/src/main/res/menu/menu_patient_item.xml | 3 + 6 files changed, 581 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt create mode 100644 app/src/main/res/layout/activity_edit_patient.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 80adfbf0d..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"> @@ -93,6 +95,7 @@ + @@ -161,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/view/general/EditPatientActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt new file mode 100644 index 000000000..4c648785b --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt @@ -0,0 +1,380 @@ +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.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() +// val dob = binding.txtDob.text.toString().trim() +// val gender = binding.genderSpinner.selectedItem?.toString()?.lowercase() ?: "" +// +// 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 token = "Bearer ${SessionManager.getToken()}" +// +// CoroutineScope(Dispatchers.IO).launch { +// withContext(Dispatchers.Main) { +// binding.progressBar.show() +// binding.btnSave.hide() +// } +// +// val response = try { +// ApiClient.apiService.updatePatient( +// token = token, +// patientId = patient.id, +// name = namePart, +// dob = dobPart, +// gender = genderPart, +// photo = photoPart, +// ) +// } catch (e: Exception) { +// null +// } +// +// withContext(Dispatchers.Main) { +// binding.progressBar.hide() +// binding.btnSave.show() +// +// if (response?.isSuccessful == true) { +// showMessage(response.body()?.apiMessage ?: "Patient updated successfully") +// finish() +// } else { +// val errorBody = try { +// response?.errorBody()?.string() +// } catch (e: Exception) { +// null +// } +// +// val errorResponse = try { +// Gson().fromJson(errorBody, ApiErrorResponse::class.java) +// } catch (e: Exception) { +// null +// } +// +// showMessage(errorResponse?.apiError?.takeIf { it.isNotBlank() } +// ?: response?.message()?.takeIf { it.isNotBlank() } +// ?: "Failed to update patient") +// } +// } +// } +// } + + 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() + } +} \ 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 a40399468..ef6f013ee 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 @@ -38,6 +38,11 @@ class PatientListActivity : BaseActivity() { intent.putExtra(AssignNurseActivity.EXTRA_PATIENT_NAME, patient.fullname) startActivity(intent) }, + onEditClick = { patient -> + val intent = Intent(this, EditPatientActivity::class.java) + intent.putExtra(EditPatientActivity.EXTRA_PATIENT, patient) + startActivity(intent) + }, onDeleteClick = { patient -> confirmDeletePatient(patient) }, 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/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 @@ + From 58a6e24f30730208997bcff00e0eb28a6346afff Mon Sep 17 00:00:00 2001 From: Kaustubh Kale Date: Sat, 4 Apr 2026 17:00:17 +1100 Subject: [PATCH 3/4] fixed the code after using new backend url -Kaustubh --- .../deakin/gopher/guardian/model/Patient.kt | 11 + .../gopher/guardian/model/register/User.kt | 49 +++- .../gopher/guardian/services/api/ApiClient.kt | 4 +- .../guardian/services/api/ApiService.kt | 37 +++ .../view/general/AssignNurseActivity.kt | 216 ++++++++++-------- .../view/general/EditPatientActivity.kt | 171 ++++++++------ .../view/general/PatientListActivity.kt | 26 ++- .../res/layout/activity_patient_details.xml | 71 +++--- app/src/main/res/values/strings.xml | 2 +- 9 files changed, 365 insertions(+), 222 deletions(-) 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..6d2161b30 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,7 +7,9 @@ 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-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) 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 2382612f7..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( 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 index 83cf22987..49c0b7c38 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt @@ -11,14 +11,16 @@ 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.services.api.AssignNurseRequest 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() { @@ -52,7 +54,7 @@ class AssignNurseActivity : BaseActivity() { setupToolbar() setupRecyclerView() setupUi() -// fetchNurses() + fetchNurses() } private fun setupToolbar() { @@ -76,110 +78,124 @@ class AssignNurseActivity : BaseActivity() { private fun setupRecyclerView() { nurseListAdapter = NurseListAdapter(emptyList()) { nurse -> -// showAssignConfirmation(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) -// } catch (e: Exception) { -// null -// } -// -// withContext(Dispatchers.Main) { -// binding.progressBar.hide() -// -// if (response?.isSuccessful == true) { -// val nurses = response.body().orEmpty() -// -// if (nurses.isNotEmpty()) { -// nurseListAdapter.updateData(nurses) -// binding.tvEmptyMessage.visibility = android.view.View.GONE -// } else { -// binding.tvEmptyMessage.visibility = android.view.View.VISIBLE -// } -// } else { -// val errorResponse = -// try { -// Gson().fromJson( -// response?.errorBody()?.string(), -// ApiErrorResponse::class.java, -// ) -// } catch (e: Exception) { -// null -// } -// -// showMessage(errorResponse?.apiError ?: "Failed to load nurses") -// } -// } -// } -// } - -// 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, -// patientId = patientId, -// request = AssignNurseRequest(nurse.id), -// ) -// } catch (e: Exception) { -// null -// } -// -// withContext(Dispatchers.Main) { -// binding.progressBar.hide() -// -// if (response?.isSuccessful == true) { -// showMessage("Nurse assigned successfully") -// setResult(RESULT_OK) -// finish() -// } else { -// val errorResponse = -// try { -// Gson().fromJson( -// response?.errorBody()?.string(), -// ApiErrorResponse::class.java, -// ) -// } catch (e: Exception) { -// null -// } -// -// showMessage(errorResponse?.apiError ?: "Failed to assign nurse") -// } -// } -// } -// } + 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() 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 index 4c648785b..f8920f7f5 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt @@ -21,6 +21,7 @@ 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 @@ -96,7 +97,7 @@ class EditPatientActivity : BaseActivity() { binding.btnSelectFromGallery.setOnClickListener { openGallery() } binding.btnTakePhoto.setOnClickListener { checkCameraPermissionAndOpen() } -// binding.btnSave.setOnClickListener { updatePatientInfo() } + binding.btnSave.setOnClickListener { updatePatientInfo() } } private fun setupGenderSpinner() { @@ -164,8 +165,7 @@ class EditPatientActivity : BaseActivity() { private fun checkCameraPermissionAndOpen() { if (ContextCompat.checkSelfPermission( - this, - Manifest.permission.CAMERA + this, Manifest.permission.CAMERA ) == PackageManager.PERMISSION_GRANTED ) { openCamera() @@ -201,71 +201,93 @@ class EditPatientActivity : BaseActivity() { cameraLauncher.launch(null) } -// private fun updatePatientInfo() { -// if (!validateInputs()) return -// -// val fullname = binding.txtName.text.toString().trim() -// val dob = binding.txtDob.text.toString().trim() -// val gender = binding.genderSpinner.selectedItem?.toString()?.lowercase() ?: "" -// -// 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 token = "Bearer ${SessionManager.getToken()}" -// -// CoroutineScope(Dispatchers.IO).launch { -// withContext(Dispatchers.Main) { -// binding.progressBar.show() -// binding.btnSave.hide() -// } -// -// val response = try { -// ApiClient.apiService.updatePatient( -// token = token, -// patientId = patient.id, -// name = namePart, -// dob = dobPart, -// gender = genderPart, -// photo = photoPart, -// ) -// } catch (e: Exception) { -// null -// } -// -// withContext(Dispatchers.Main) { -// binding.progressBar.hide() -// binding.btnSave.show() -// -// if (response?.isSuccessful == true) { -// showMessage(response.body()?.apiMessage ?: "Patient updated successfully") -// finish() -// } else { -// val errorBody = try { -// response?.errorBody()?.string() -// } catch (e: Exception) { -// null -// } -// -// val errorResponse = try { -// Gson().fromJson(errorBody, ApiErrorResponse::class.java) -// } catch (e: Exception) { -// null -// } -// -// showMessage(errorResponse?.apiError?.takeIf { it.isNotBlank() } -// ?: response?.message()?.takeIf { it.isNotBlank() } -// ?: "Failed to update patient") -// } -// } -// } -// } + 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() @@ -377,4 +399,17 @@ class EditPatientActivity : BaseActivity() { 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/PatientListActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt index ef6f013ee..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 @@ -33,15 +33,27 @@ class PatientListActivity : BaseActivity() { startActivity(intent) }, onAssignNurseClick = { patient -> - 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) + 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 -> - val intent = Intent(this, EditPatientActivity::class.java) - intent.putExtra(EditPatientActivity.EXTRA_PATIENT, patient) - startActivity(intent) + 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) 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/values/strings.xml b/app/src/main/res/values/strings.xml index f33e06461..06ffe24c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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 From 650ee316a62ba7cde73789c7ab1e6f26d72b8e1f Mon Sep 17 00:00:00 2001 From: Kaustubh Kale Date: Tue, 7 Apr 2026 12:38:18 +1000 Subject: [PATCH 4/4] Changed the backend URL back to original -Kaustubh --- .../deakin/gopher/guardian/services/api/ApiClient.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 6d2161b30..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 @@ -8,19 +8,17 @@ 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 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() } }