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..c36e5ea7c
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/PatientLogAdapter.kt
@@ -0,0 +1,74 @@
+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
+ }
+ }
+}
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..a15295f57
--- /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,
+)
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..5a4956a4c
--- /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,
+)
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..b94c21f9e 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..d28b0f816
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/services/PatientLogService.kt
@@ -0,0 +1,3 @@
+package deakin.gopher.guardian.services
+
+class PatientLogService
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..1fe8fd83e 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,8 +3,10 @@ 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.PatientLog
import deakin.gopher.guardian.model.register.AuthResponse
import deakin.gopher.guardian.model.register.RegisterRequest
import okhttp3.MultipartBody
@@ -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..f5dbfc293 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,47 +53,37 @@ 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
- // 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()
- }
+ 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)
+
+ handler.postDelayed(this, 1000)
+ }
+ }
+
+ handler.post(updateTimeRunnable)
binding.txtReporter.setText(SessionManager.getCurrentUser().name)
@@ -107,62 +93,67 @@ 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..26aa8a24f 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,16 @@ 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..001377c46
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/view/patient/PatientLogsActivity.kt
@@ -0,0 +1,193 @@
+package deakin.gopher.guardian.view.patient
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+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.Patient
+import deakin.gopher.guardian.model.PatientLog
+import deakin.gopher.guardian.model.login.SessionManager
+import deakin.gopher.guardian.services.api.ApiClient
+import deakin.gopher.guardian.view.general.AddPatientLogActivity
+import kotlinx.coroutines.launch
+
+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()
+ }
+ }
+ }
+}
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 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_add_patient_log_screen.xml b/app/src/main/res/layout/activity_add_patient_log_screen.xml
new file mode 100644
index 000000000..3981bb719
--- /dev/null
+++ b/app/src/main/res/layout/activity_add_patient_log_screen.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_homepage4nurse.xml b/app/src/main/res/layout/activity_homepage4nurse.xml
index 2cd321f1e..384539347 100644
--- a/app/src/main/res/layout/activity_homepage4nurse.xml
+++ b/app/src/main/res/layout/activity_homepage4nurse.xml
@@ -77,11 +77,12 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/medicalDiaganosticsHeaderCardView" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_patient_log.xml b/app/src/main/res/layout/item_patient_log.xml
new file mode 100644
index 000000000..16de6efb2
--- /dev/null
+++ b/app/src/main/res/layout/item_patient_log.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file