From cf9dbf478bcf0c2745a9e8b96a7d61ab26f46cad Mon Sep 17 00:00:00 2001 From: iHateErrorsSoMuch Date: Sun, 17 May 2026 12:53:51 +1000 Subject: [PATCH] Implemented patient logs module for nurse dashboard --- app/src/main/AndroidManifest.xml | 1 + .../guardian/adapter/PatientLogAdapter.kt | 70 ++++++ .../guardian/model/CreatePatientLogRequest.kt | 7 + .../gopher/guardian/model/PatientLog.kt | 16 ++ .../guardian/services/NavigationService.kt | 10 + .../guardian/services/PatientLogService.kt | 4 + .../guardian/services/api/ApiService.kt | 21 ++ .../view/general/AddPatientLogActivity.kt | 152 ++++++------- .../guardian/view/general/Homepage4nurse.kt | 6 + .../view/patient/PatientLogsActivity.kt | 210 ++++++++++++++++++ app/src/main/res/drawable/ic_patientlogs.xml | 20 ++ .../res/layout/activity_add_patient_log.xml | 28 +++ .../activity_add_patient_log_screen.xml | 38 ++++ .../res/layout/activity_homepage4nurse.xml | 47 +++- .../main/res/layout/activity_patient_logs.xml | 62 ++++++ app/src/main/res/layout/item_patient_log.xml | 50 +++++ 16 files changed, 662 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/deakin/gopher/guardian/adapter/PatientLogAdapter.kt create mode 100644 app/src/main/java/deakin/gopher/guardian/model/CreatePatientLogRequest.kt create mode 100644 app/src/main/java/deakin/gopher/guardian/model/PatientLog.kt create mode 100644 app/src/main/java/deakin/gopher/guardian/services/PatientLogService.kt create mode 100644 app/src/main/java/deakin/gopher/guardian/view/patient/PatientLogsActivity.kt create mode 100644 app/src/main/res/drawable/ic_patientlogs.xml create mode 100644 app/src/main/res/layout/activity_add_patient_log.xml create mode 100644 app/src/main/res/layout/activity_add_patient_log_screen.xml create mode 100644 app/src/main/res/layout/activity_patient_logs.xml create mode 100644 app/src/main/res/layout/item_patient_log.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31d6a312a..adf1f38a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -187,6 +187,7 @@ + diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/PatientLogAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/PatientLogAdapter.kt new file mode 100644 index 000000000..4ecb2df08 --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/adapter/PatientLogAdapter.kt @@ -0,0 +1,70 @@ +package deakin.gopher.guardian.adapter + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import deakin.gopher.guardian.databinding.ItemPatientLogBinding +import deakin.gopher.guardian.model.PatientLog +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +class PatientLogAdapter( + private var logs: List, + private val onDeleteClick: (PatientLog) -> Unit +) : RecyclerView.Adapter() { + + inner class ViewHolder(val binding: ItemPatientLogBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemPatientLogBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ViewHolder(binding) + } + + override fun getItemCount() = logs.size + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val log = logs[position] + + holder.binding.title.text = log.title + holder.binding.description.text = log.description + holder.binding.meta.text = + "${log.createdBy.fullname} • ${formatDate(log.createdAt)}" + + holder.binding.deleteBtn.setOnClickListener { + onDeleteClick(log) + } + } + + fun updateData(newLogs: List) { + logs = newLogs + notifyDataSetChanged() + } + + private fun formatDate(dateString: String): String { + + return try { + + val inputFormat = + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) + + inputFormat.timeZone = TimeZone.getTimeZone("UTC") + + val outputFormat = + SimpleDateFormat("dd MMM yyyy, hh:mm a", Locale.getDefault()) + + val date = inputFormat.parse(dateString) + + outputFormat.format(date!!) + + } catch (e: Exception) { + + dateString + } + } +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/model/CreatePatientLogRequest.kt b/app/src/main/java/deakin/gopher/guardian/model/CreatePatientLogRequest.kt new file mode 100644 index 000000000..3acd2624e --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/model/CreatePatientLogRequest.kt @@ -0,0 +1,7 @@ +package deakin.gopher.guardian.model + +data class CreatePatientLogRequest( + val patient: String, + val title: String, + val description: String +) \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/model/PatientLog.kt b/app/src/main/java/deakin/gopher/guardian/model/PatientLog.kt new file mode 100644 index 000000000..dec07776b --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/model/PatientLog.kt @@ -0,0 +1,16 @@ +package deakin.gopher.guardian.model + +data class PatientLog( + val _id: String, + val patient: String, + val title: String, + val description: String, + val createdBy: CreatedBy, + val createdAt: String +) + +data class CreatedBy( + val _id: String, + val fullname: String, + val role: String +) \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt b/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt index c0b2775d5..eb601553d 100644 --- a/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt +++ b/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt @@ -14,6 +14,7 @@ import deakin.gopher.guardian.view.general.RegisterActivity import deakin.gopher.guardian.view.general.Setting import deakin.gopher.guardian.view.general.TaskAddActivity import deakin.gopher.guardian.view.general.TasksListActivity +import deakin.gopher.guardian.view.patient.PatientLogsActivity class NavigationService(val activity: Activity) { fun toHomeScreenForRole(role: Role) { @@ -74,6 +75,15 @@ class NavigationService(val activity: Activity) { ) } + fun onPatientLogs() { + activity.startActivity( + Intent( + activity.applicationContext, + PatientLogsActivity::class.java + ) + ) + } + fun onSignOut() { activity.startActivity( Intent( diff --git a/app/src/main/java/deakin/gopher/guardian/services/PatientLogService.kt b/app/src/main/java/deakin/gopher/guardian/services/PatientLogService.kt new file mode 100644 index 000000000..4faf0b24c --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/services/PatientLogService.kt @@ -0,0 +1,4 @@ +package deakin.gopher.guardian.services + +class PatientLogService { +} \ No newline at end of file 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..f53bffbbc 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 @@ -3,10 +3,12 @@ package deakin.gopher.guardian.services.api import deakin.gopher.guardian.model.AddPatientActivityResponse import deakin.gopher.guardian.model.AddPatientResponse import deakin.gopher.guardian.model.BaseModel +import deakin.gopher.guardian.model.CreatePatientLogRequest 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.PatientLog import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Call @@ -91,4 +93,23 @@ interface ApiService { @Header("Authorization") token: String, @Path("id") patientId: String, ): Response + + //For Patient Logs + @GET("patient-logs/{patientId}") + suspend fun getPatientLogs( + @Header("Authorization") token: String, + @Path("patientId") patientId: String, + ): Response> + + @POST("patient-logs") + suspend fun createPatientLog( + @Header("Authorization") token: String, + @Body log: CreatePatientLogRequest, + ): Response + + @DELETE("patient-logs/{id}") + suspend fun deletePatientLog( + @Header("Authorization") token: String, + @Path("id") id: String, + ): Response } diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/AddPatientLogActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/AddPatientLogActivity.kt index ac9838227..af1be0d82 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/AddPatientLogActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/AddPatientLogActivity.kt @@ -1,15 +1,11 @@ package deakin.gopher.guardian.view.general -import android.app.DatePickerDialog -import android.app.TimePickerDialog import android.os.Bundle import android.view.View import android.widget.ArrayAdapter import android.widget.Toast -import com.google.gson.Gson -import deakin.gopher.guardian.R import deakin.gopher.guardian.databinding.ActivityAddPatientActivityBinding -import deakin.gopher.guardian.model.ApiErrorResponse +import deakin.gopher.guardian.model.CreatePatientLogRequest import deakin.gopher.guardian.model.login.SessionManager import deakin.gopher.guardian.services.api.ApiClient import deakin.gopher.guardian.view.hide @@ -57,48 +53,39 @@ class AddPatientLogActivity : BaseActivity() { } // Set default date and time - val calendar = Calendar.getInstance() - val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) - - binding.txtDate.setText(dateFormat.format(calendar.time)) - binding.txtTime.setText(timeFormat.format(calendar.time)) - - // Date picker dialog - binding.txtDate.setOnClickListener { - val dpd = - DatePickerDialog( - this, - { _, year, month, dayOfMonth -> - calendar.set(year, month, dayOfMonth) - binding.txtDate.setText(dateFormat.format(calendar.time)) - }, - calendar.get(Calendar.YEAR), - calendar.get(Calendar.MONTH), - calendar.get(Calendar.DAY_OF_MONTH), - ) - dpd.datePicker.maxDate = System.currentTimeMillis() - dpd.show() - } + // Keep current date/time visible but prevent editing + binding.txtDate.isFocusable = false + binding.txtDate.isClickable = false + + binding.txtTime.isFocusable = false + binding.txtTime.isClickable = false + +// Live updating current date and time + val handler = android.os.Handler(mainLooper) + + val updateTimeRunnable = object : Runnable { + + override fun run() { + + val currentCalendar = Calendar.getInstance() + + val currentDate = + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + .format(currentCalendar.time) + + val currentTime = + SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + .format(currentCalendar.time) + + binding.txtDate.setText(currentDate) + binding.txtTime.setText(currentTime) - // Time picker dialog - binding.txtTime.setOnClickListener { - val tpd = - TimePickerDialog(this, { _, hour, minute -> - calendar.set(Calendar.HOUR_OF_DAY, hour) - calendar.set(Calendar.MINUTE, minute) - binding.txtTime.setText( - String.format( - Locale.getDefault(), - "%02d:%02d", - hour, - minute, - ), - ) - }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), true) - tpd.show() + handler.postDelayed(this, 1000) + } } + handler.post(updateTimeRunnable) + binding.txtReporter.setText(SessionManager.getCurrentUser().name) binding.btnSave.setOnClickListener { @@ -107,62 +94,75 @@ class AddPatientLogActivity : BaseActivity() { } private fun savePatientActivity() { - val activityType = + + val title = if (binding.txtActivityType.text.toString().trim() == "Other") { binding.txtOtherActivity.text.toString().trim() } else { binding.txtActivityType.text.toString().trim() } - if (activityType.trim().isEmpty()) { - showMessage(getString(R.string.validation_empty_activity_type)) + val description = binding.txtComment.text.toString().trim() + + if (title.isEmpty()) { + showMessage("Please enter title") return } - val date = binding.txtDate.text.toString() - val time = binding.txtTime.text.toString() - // Build ISO timestamp - val isoTimestamp = "${date}T$time:00Z" - - val comments = binding.txtComment.text.toString().trim() + if (description.isEmpty()) { + showMessage("Please enter description") + return + } val patientId = intent.getStringExtra("patientId").orEmpty() - val token = "Bearer ${SessionManager.getToken()}" + + val request = CreatePatientLogRequest( + patient = patientId, + title = title, + description = description + ) + CoroutineScope(Dispatchers.IO).launch { + withContext(Dispatchers.Main) { binding.progressBar.show() binding.btnSave.visibility = View.GONE } - val response = - ApiClient.apiService.logPatientActivity( - token, - patientId, - activityType, - isoTimestamp, - comments, + try { + + val response = ApiClient.apiService.createPatientLog( + "Bearer ${SessionManager.getToken()}", + request ) - withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { + binding.progressBar.hide() binding.btnSave.visibility = View.VISIBLE - } - if (response.isSuccessful) { - if (response.body() != null) { - showMessage(response.body()?.apiMessage ?: response.message()) - onBackPressedDispatcher.onBackPressed() + + if (response.isSuccessful) { + + showMessage("Log created successfully") + + finish() + } else { - showMessage(response.body()?.apiError ?: "Failed to add patient activity") - } - } else { - // Handle error - val errorResponse = - Gson().fromJson( - response.errorBody()?.string(), - ApiErrorResponse::class.java, + + showMessage( + response.errorBody()?.string() ?: "Failed to create log" ) - showMessage(errorResponse.apiError ?: response.message()) + } + } + + } catch (e: Exception) { + + withContext(Dispatchers.Main) { + + binding.progressBar.hide() + binding.btnSave.visibility = View.VISIBLE + + showMessage(e.message ?: "Unknown error") } } } diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4nurse.kt b/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4nurse.kt index 4b529e376..64d6d4c9a 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4nurse.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/Homepage4nurse.kt @@ -21,11 +21,17 @@ class Homepage4nurse : AppCompatActivity() { val patientsButton: Button = findViewById(R.id.patientsButton_nurse) val settingsButton: Button = findViewById(R.id.settingsButton_nurse) val signOutButton: Button = findViewById(R.id.sighOutButton_nurse) + val logsButton: Button = findViewById(R.id.logsButton_nurse) + patientsButton.setOnClickListener { NavigationService(this).onLaunchPatientList() } + logsButton.setOnClickListener { + NavigationService(this).onPatientLogs() + } + // settings button settingsButton.setOnClickListener { NavigationService(this).onSettings() diff --git a/app/src/main/java/deakin/gopher/guardian/view/patient/PatientLogsActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientLogsActivity.kt new file mode 100644 index 000000000..d34b2b97b --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientLogsActivity.kt @@ -0,0 +1,210 @@ +package deakin.gopher.guardian.view.patient + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import deakin.gopher.guardian.adapter.PatientLogAdapter +import deakin.gopher.guardian.databinding.ActivityPatientLogsBinding +import deakin.gopher.guardian.model.PatientLog +import deakin.gopher.guardian.services.api.ApiClient +import kotlinx.coroutines.launch +import deakin.gopher.guardian.model.login.SessionManager +import androidx.appcompat.app.AlertDialog +import android.widget.ArrayAdapter +import deakin.gopher.guardian.model.Patient +import deakin.gopher.guardian.view.general.AddPatientLogActivity + + +class PatientLogsActivity : AppCompatActivity() { + + private lateinit var binding: ActivityPatientLogsBinding + private lateinit var adapter: PatientLogAdapter + + private var patients: List = emptyList() + private var patientId: String = "" + private val api = ApiClient.apiService + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityPatientLogsBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + supportActionBar?.title = "Patient Logs" + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + adapter = PatientLogAdapter(emptyList()) { log -> + + AlertDialog.Builder(this) + .setTitle("Delete Log") + .setMessage("Are you sure you want to delete this log?") + .setPositiveButton("Yes") { _, _ -> + deleteLog(log) + } + .setNegativeButton("Cancel", null) + .show() + } + + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = adapter + + fetchAssignedPatients() + + binding.addLogBtn.setOnClickListener { + val intent = Intent(this, AddPatientLogActivity::class.java) + intent.putExtra("patientId", patientId) + startActivity(intent) + } + } + + private fun fetchAssignedPatients() { + + lifecycleScope.launch { + + try { + + val response = api.getAssignedPatients( + "Bearer ${SessionManager.getToken()}" + ) + + if (response.isSuccessful) { + + patients = response.body() ?: emptyList() + + val patientNames = patients.map { it.fullname } + + val spinnerAdapter = ArrayAdapter( + this@PatientLogsActivity, + android.R.layout.simple_spinner_dropdown_item, + patientNames + ) + + binding.patientSpinner.adapter = spinnerAdapter + + binding.patientSpinner.setOnItemSelectedListener( + object : android.widget.AdapterView.OnItemSelectedListener { + + override fun onItemSelected( + parent: android.widget.AdapterView<*>?, + view: android.view.View?, + position: Int, + id: Long + ) { + + patientId = patients[position].id + fetchLogs() + } + + override fun onNothingSelected(parent: android.widget.AdapterView<*>?) {} + } + ) + + } else { + + Toast.makeText( + this@PatientLogsActivity, + "Failed to load patients", + Toast.LENGTH_SHORT + ).show() + } + + } catch (e: Exception) { + + Toast.makeText( + this@PatientLogsActivity, + e.message, + Toast.LENGTH_LONG + ).show() + } + } + } + + override fun onResume() { + super.onResume() + + if (patientId.isNotEmpty()) { + fetchLogs() + } + } + + override fun onSupportNavigateUp(): Boolean { + finish() + return true + } + + private fun fetchLogs() { + + binding.progressBar.visibility = View.VISIBLE + binding.emptyText.visibility = View.GONE + + lifecycleScope.launch { + + try { + + val response = api.getPatientLogs( + "Bearer ${SessionManager.getToken()}", + patientId + ) + + binding.progressBar.visibility = View.GONE + + if (response.isSuccessful) { + + val logs = response.body() ?: emptyList() + + adapter.updateData(logs) + + if (logs.isEmpty()) { + binding.emptyText.visibility = View.VISIBLE + } else { + binding.emptyText.visibility = View.GONE + } + + } else { + + Toast.makeText( + this@PatientLogsActivity, + "Failed to load logs", + Toast.LENGTH_SHORT + ).show() + } + + } catch (e: Exception) { + + binding.progressBar.visibility = View.GONE + + Toast.makeText( + this@PatientLogsActivity, + "Something went wrong while loading logs", + Toast.LENGTH_LONG + ).show() + } + } + } + + private fun deleteLog(log: PatientLog) { + lifecycleScope.launch { + try { + val response = api.deletePatientLog( + "Bearer ${SessionManager.getToken()}", + log._id + ) + + if (response.isSuccessful) { + Toast.makeText(this@PatientLogsActivity, "Log deleted successfully", Toast.LENGTH_SHORT).show() + fetchLogs() + } else { + Toast.makeText(this@PatientLogsActivity, "Delete failed", Toast.LENGTH_SHORT).show() + } + + } catch (e: Exception) { + Toast.makeText(this@PatientLogsActivity, e.message, Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_patientlogs.xml b/app/src/main/res/drawable/ic_patientlogs.xml new file mode 100644 index 000000000..8030d9f45 --- /dev/null +++ b/app/src/main/res/drawable/ic_patientlogs.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_add_patient_log.xml b/app/src/main/res/layout/activity_add_patient_log.xml new file mode 100644 index 000000000..05d6d4de6 --- /dev/null +++ b/app/src/main/res/layout/activity_add_patient_log.xml @@ -0,0 +1,28 @@ + + + + + + +