diff --git a/.editorconfig b/.editorconfig index 93eb1e0e..069caad9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,5 +2,4 @@ # This configuration is used by ktlint when spotless invokes it [*.{kt,kts}] -ktlint_function_naming_ignore_when_annotated_with=Composable, Test -ktlint_disabled_rules=no-wildcard-imports \ No newline at end of file + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 837d337b..0694920f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,25 +2,17 @@ - + + + + - + - - - - - - - - - - - + + - - - - - + + + - - - - - - - - - - - - - - - - - - + android:grantUriPermissions="true"> - - - - - - - - - - - - - - - - - - - - - + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - - \ No newline at end of file + diff --git a/app/src/main/java/deakin/gopher/guardian/ActivityLogScreen.kt b/app/src/main/java/deakin/gopher/guardian/ActivityLogScreen.kt index 09496595..82d230a9 100644 --- a/app/src/main/java/deakin/gopher/guardian/ActivityLogScreen.kt +++ b/app/src/main/java/deakin/gopher/guardian/ActivityLogScreen.kt @@ -34,7 +34,7 @@ import androidx.navigation.NavController @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ActivityLogScreen(navController: NavController) { +fun activityLogScreen(navController: NavController) { Scaffold( topBar = { TopAppBar( diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt index 297137b4..a50fe7ef 100644 --- a/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt +++ b/app/src/main/java/deakin/gopher/guardian/adapter/PatientListAdapter.kt @@ -14,9 +14,10 @@ import deakin.gopher.guardian.model.Patient class PatientListAdapter( private var patients: List, - private val onPatientClick: ((Patient) -> Unit)? = null, - private val onAssignNurseClick: ((Patient) -> Unit)? = null, - private val onDeleteClick: ((Patient) -> Unit)? = null, + private val onPatientClick: (Patient) -> Unit, + private val onAssignNurseClick: (Patient) -> Unit, + private val onEditClick: (Patient) -> Unit, + private val onDeleteClick: (Patient) -> Unit, ) : RecyclerView.Adapter() { inner class PatientViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val nameText: TextView = itemView.findViewById(R.id.tvName) @@ -68,11 +69,15 @@ class PatientListAdapter( popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.assign_nurse -> { - onAssignNurseClick?.invoke(patient) + onAssignNurseClick(patient) true } - R.id.action_delete -> { // Handle delete click - onDeleteClick?.invoke(patient) + R.id.action_edit -> { + onEditClick(patient) // NEW + true + } + R.id.action_delete -> { + onDeleteClick(patient) true } else -> false diff --git a/app/src/main/java/deakin/gopher/guardian/model/Nurse.kt b/app/src/main/java/deakin/gopher/guardian/model/Nurse.kt new file mode 100644 index 00000000..bbef10f3 --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/model/Nurse.kt @@ -0,0 +1,18 @@ +package deakin.gopher.guardian.model.user + +data class Nurse( + val _id: String, + val fullname: String, + val email: String, + val assignedPatients: List?, + val failedLoginAttempts: Int, + val lastPasswordChange: String?, + val created_at: String, + val updated_at: String, + val role: RoleDto, +) + +data class RoleDto( + val _id: String, + val name: String, +) diff --git a/app/src/main/java/deakin/gopher/guardian/model/NursesResponse.kt b/app/src/main/java/deakin/gopher/guardian/model/NursesResponse.kt new file mode 100644 index 00000000..6d58e23c --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/model/NursesResponse.kt @@ -0,0 +1,5 @@ +package deakin.gopher.guardian.model.user + +data class NursesResponse( + val nurses: List, +) 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 96853851..2d31812e 100644 --- a/app/src/main/java/deakin/gopher/guardian/model/Patient.kt +++ b/app/src/main/java/deakin/gopher/guardian/model/Patient.kt @@ -15,6 +15,7 @@ data class Patient( @SerializedName("dateOfBirth") val dateOfBirth: String, @SerializedName("age") val _age: Int, @SerializedName("gender") val gender: String, + @SerializedName("isDeleted") val isDeleted: Boolean, @SerializedName("healthConditions") val healthConditions: List, @SerializedName("caretaker") val caretaker: User, @SerializedName("assignedNurses") val assignedNurses: List, 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 c906ee66..1bead93f 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 @@ -30,3 +30,11 @@ data class AuthResponse( @SerializedName("user") val user: User, @SerializedName("token") val token: String, ) : BaseModel() + +data class NotificationItem( + val userId: String, + val title: String, + val message: String, + val isRead: Boolean, + val createdAt: String, +) 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 48eff44c..0a8ca63c 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 @@ -6,8 +6,9 @@ import retrofit2.Retrofit 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-opal.vercel.app/api/v1/" + // private const val BASE_URL = "http://10.0.2.2:3000/api/v1/" +// private const val BASE_URL = "https://guardian-backend-opal.vercel.app/api/v1/" + private const val BASE_URL = "https://guardian-backend-x5v6.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 0e9ea177..3a930c12 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 @@ -6,7 +6,10 @@ 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.NotificationItem import deakin.gopher.guardian.model.register.RegisterRequest +import deakin.gopher.guardian.model.user.NursesResponse +import deakin.gopher.guardian.view.general.UpdatePatientRequest import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Call @@ -19,6 +22,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 @@ -91,4 +95,35 @@ interface ApiService { @Header("Authorization") token: String, @Path("id") patientId: String, ): Response + + @GET("/api/v1/nurse/all") + suspend fun getAllNurses( + @Header("Authorization") token: String, + @Query("page") page: Int = 1, + @Query("limit") limit: Int = 100, + ): Response + + @POST("patients/assign-nurse") + suspend fun assignNurse( + @Header("Authorization") token: String, + @Body body: Map, + ): Response + + @PUT("/api/v1/patients/{patientId}") + suspend fun updatePatient( + @Header("Authorization") token: String, + @Path("patientId") patientId: String, + @Body request: UpdatePatientRequest, + ): Response + + @POST("notifications") + suspend fun createNotification( + @Header("Authorization") token: String, + @Body body: Map, + ): Response + + @GET("notifications") + suspend fun getMyNotifications( + @Header("Authorization") token: String, + ): Response> } diff --git a/app/src/main/java/deakin/gopher/guardian/view/caretaker/AssignNurseScreen.kt b/app/src/main/java/deakin/gopher/guardian/view/caretaker/AssignNurseScreen.kt index 8de11deb..69d65e14 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/caretaker/AssignNurseScreen.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/caretaker/AssignNurseScreen.kt @@ -34,7 +34,7 @@ import androidx.navigation.NavController @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AssignNurseScreen(navController: NavController) { +fun assignNurseScreen(navController: NavController) { Scaffold( topBar = { TopAppBar( diff --git a/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt index 5d7c949b..4fdb3fba 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/caretaker/MainActivity.kt @@ -50,12 +50,12 @@ class MainActivity : ComponentActivity() { // 7. Assign nurse composable("assign_nurse") { - AssignNurseScreen(navController) + assignNurseScreen(navController) } // 8. Activity log composable("activity_log") { - ActivityLogScreen(navController) + activityLogScreen(navController) } // 9. Appointment 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 df96346a..2a8f618d 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 @@ -1,20 +1,24 @@ +@file:Suppress("ktlint") + package deakin.gopher.guardian.view.general import android.Manifest -import android.R.style.Theme_Holo_Light_Dialog +import android.app.Activity import android.app.DatePickerDialog -import android.content.Context +import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Bitmap import android.net.Uri +import java.text.SimpleDateFormat +import java.util.Locale import android.os.Bundle +import android.provider.MediaStore import android.view.View -import android.widget.ArrayAdapter -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts +import android.widget.* +import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.viewbinding.ViewBinding +import androidx.core.content.FileProvider +import com.google.android.material.button.MaterialButton import com.google.gson.Gson import deakin.gopher.guardian.R import deakin.gopher.guardian.databinding.ActivityAddNewPatientBinding @@ -32,378 +36,248 @@ 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 AddNewPatientActivity : BaseActivity() { - private lateinit var binding: ViewBinding - - private var selectedPhotoUri: Uri? = null - private var capturedPhotoBitmap: Bitmap? = null +import java.io.File +import java.util.* - companion object { - private const val CAMERA_PERMISSION_CODE = 1001 - } +class AddNewPatientActivity : AppCompatActivity() { - // Launcher for picking image from gallery - private val galleryLauncher = - registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> - if (uri != null) { - selectedPhotoUri = uri - capturedPhotoBitmap = null - val localBinding = binding - when (localBinding) { - is ActivityAddNewPatientBinding -> localBinding.imgPreview.setImageURI(uri) - is ActivityAddNewPatientNurseBinding -> localBinding.imgPreview.setImageURI(uri) - } - } - } + private lateinit var txtName: EditText + private lateinit var txtDob: EditText + private lateinit var txtAge: EditText + private lateinit var genderSpinner: Spinner + private lateinit var btnSave: MaterialButton + private lateinit var btnSelectFromGallery: MaterialButton + private lateinit var btnTakePhoto: MaterialButton + private lateinit var imgPreview: ImageView + private lateinit var progressBar: ProgressBar - // Launcher for taking photo with camera (returns Bitmap) - private val cameraLauncher = - registerForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap: Bitmap? -> - if (bitmap != null) { - capturedPhotoBitmap = bitmap - selectedPhotoUri = null - val localBinding = binding - when (localBinding) { - is ActivityAddNewPatientBinding -> localBinding.imgPreview.setImageBitmap(bitmap) - is ActivityAddNewPatientNurseBinding -> localBinding.imgPreview.setImageBitmap(bitmap) - } - } - } + private var selectedPhotoUri: Uri? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContentView(R.layout.activity_add_new_patient) val currentUser = SessionManager.getCurrentUser() if (currentUser.role == Role.Nurse) { val nurseBinding = ActivityAddNewPatientNurseBinding.inflate(layoutInflater) - binding = nurseBinding + var binding = nurseBinding setContentView(nurseBinding.root) } else { val caretakerBinding = ActivityAddNewPatientBinding.inflate(layoutInflater) - binding = caretakerBinding + var binding = caretakerBinding setContentView(caretakerBinding.root) } - val localBinding = binding - when (localBinding) { - is ActivityAddNewPatientBinding -> { - setSupportActionBar(localBinding.toolbar) - localBinding.toolbar.setNavigationOnClickListener { - onBackPressedDispatcher.onBackPressed() - } - setupUI(localBinding) - } - is ActivityAddNewPatientNurseBinding -> { - setSupportActionBar(localBinding.toolbar) - localBinding.toolbar.setNavigationOnClickListener { - onBackPressedDispatcher.onBackPressed() - } - setupUI(localBinding) - } - } - } - // Common UI setup for both bindings - private fun setupUI(localBinding: ViewBinding) { - when (localBinding) { - is ActivityAddNewPatientBinding -> { - setupGenderSpinner(localBinding.genderSpinner) - setupDOBPicker(localBinding.txtDob) - localBinding.btnSelectFromGallery.setOnClickListener { openGallery() } - localBinding.btnTakePhoto.setOnClickListener { checkCameraPermissionAndOpen() } - localBinding.btnSave.setOnClickListener { savePatientInfo() } - } - is ActivityAddNewPatientNurseBinding -> { - setupGenderSpinner(localBinding.genderSpinner) - setupDOBPicker(localBinding.txtDob) - localBinding.btnSelectFromGallery.setOnClickListener { openGallery() } - localBinding.btnTakePhoto.setOnClickListener { checkCameraPermissionAndOpen() } - localBinding.btnSave.setOnClickListener { savePatientInfo() } - } + txtName = findViewById(R.id.txtName) + txtDob = findViewById(R.id.txtDob) + txtAge = findViewById(R.id.txtAge) + genderSpinner = findViewById(R.id.genderSpinner) + btnSave = findViewById(R.id.btnSave) + btnSelectFromGallery = findViewById(R.id.btnSelectFromGallery) + btnTakePhoto = findViewById(R.id.btnTakePhoto) + imgPreview = findViewById(R.id.imgPreview) + progressBar = findViewById(R.id.progressBar) + + val toolbar: androidx.appcompat.widget.Toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { onBackPressed() } + + + btnSelectFromGallery.setOnClickListener { + if (checkPermissions()) openGallery() + } + btnTakePhoto.setOnClickListener { + if (checkPermissions()) openCamera() } + + + setupGenderSpinner() + setupDOBPicker() + + btnSave.setOnClickListener { savePatientInfo() } + btnSelectFromGallery.setOnClickListener { openGallery() } + btnTakePhoto.setOnClickListener { openCamera() } } - private fun setupGenderSpinner(genderSpinner: android.widget.Spinner) { - val genderOptions = listOf("Select gender", "Male", "Female", "Other") - val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, genderOptions) + private fun setupGenderSpinner() { + val options = listOf("Select gender", "Male", "Female", "Other") + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, options) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) genderSpinner.adapter = adapter } - private fun setupDOBPicker(txtDob: android.widget.EditText) { + private fun calculateAge(year: Int, month: Int, day: Int): Int { + val today = Calendar.getInstance() + var age = today.get(Calendar.YEAR) - year + + // Adjust if birthday hasn't occurred yet this year + if (today.get(Calendar.MONTH) < month || + (today.get(Calendar.MONTH) == month && today.get(Calendar.DAY_OF_MONTH) < day)) { + age-- + } + return age + } + + + private fun setupDOBPicker() { + val calendar = Calendar.getInstance() + 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, - ) - txtDob.setText(formattedDate) - }, - year, - month, - day, - ) - - // Force spinner mode (for API 21+) - try { - 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, - ) - constructor.isAccessible = true - - 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 - datePickerDialog.datePicker.updateDate(year, month, day) - } - } catch (e: Exception) { - e.printStackTrace() // Fallback gracefully if reflection fails - } - - datePickerDialog.datePicker.maxDate = System.currentTimeMillis() - datePickerDialog.show() - } - } - - // Permission check before opening camera - private fun checkCameraPermissionAndOpen() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) - == PackageManager.PERMISSION_GRANTED - ) { - openCamera() - } else { - ActivityCompat.requestPermissions( + val dpd = DatePickerDialog( this, - arrayOf(Manifest.permission.CAMERA), - CAMERA_PERMISSION_CODE, + android.R.style.Theme_Holo_Light_Dialog, + { _, selectedYear, selectedMonth, selectedDay -> + txtDob.setText("${selectedDay}/${selectedMonth + 1}/${selectedYear}") + txtAge.setText(calculateAge(selectedYear, selectedMonth, selectedDay).toString()) + }, + year, + month, + day ) - } - } - 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") - } + dpd.datePicker.maxDate = System.currentTimeMillis() + dpd.show() } } - private fun openGallery() { - galleryLauncher.launch("image/*") - } - private fun openCamera() { - cameraLauncher.launch(null) + private fun validateInputs(): Boolean { + if (txtName.text.toString().trim().isEmpty()) { + showMessage(getString(R.string.validation_empty_name)) + return false + } + if (txtDob.text.toString().trim().isEmpty()) { + showMessage(getString(R.string.validation_empty_dob)) + return false + } + if (genderSpinner.selectedItemPosition == 0) { + showMessage(getString(R.string.validation_empty_gender)) + return false + } + return true } private fun savePatientInfo() { if (!validateInputs()) return - val localBinding = binding - - 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 gender = - when (localBinding) { - is ActivityAddNewPatientBinding -> localBinding.genderSpinner.selectedItem?.toString()?.lowercase() ?: "" - is ActivityAddNewPatientNurseBinding -> localBinding.genderSpinner.selectedItem?.toString()?.lowercase() ?: "" - else -> "" - } + val fullname = txtName.text.toString().trim() + val dobInput = txtDob.text.toString().trim() + val gender = genderSpinner.selectedItem.toString().lowercase() + + // Convert dob to ISO 8601 format + val sdfInput = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val sdfOutput = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dobFormatted = try { + val date = sdfInput.parse(dobInput) + sdfOutput.format(date!!) + } catch (e: Exception) { + showMessage("Invalid date format") + return + } val namePart = fullname.toRequestBody("text/plain".toMediaTypeOrNull()) - val dobPart = dob.toRequestBody("text/plain".toMediaTypeOrNull()) + val dobPart = dobFormatted.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? = selectedPhotoUri?.let { prepareFilePart("photo", it) } val token = "Bearer ${SessionManager.getToken()}" CoroutineScope(Dispatchers.IO).launch { withContext(Dispatchers.Main) { - when (localBinding) { - is ActivityAddNewPatientBinding -> { - localBinding.progressBar.show() - localBinding.btnSave.visibility = View.GONE - } - is ActivityAddNewPatientNurseBinding -> { - localBinding.progressBar.show() - localBinding.btnSave.visibility = View.GONE - } - } + progressBar.show() + btnSave.visibility = View.GONE } val response = ApiClient.apiService.addPatient(token, namePart, dobPart, genderPart, photoPart) withContext(Dispatchers.Main) { - when (localBinding) { - is ActivityAddNewPatientBinding -> { - localBinding.progressBar.hide() - localBinding.btnSave.visibility = View.VISIBLE - } - is ActivityAddNewPatientNurseBinding -> { - localBinding.progressBar.hide() - localBinding.btnSave.visibility = View.VISIBLE - } - } + progressBar.hide() + btnSave.visibility = View.VISIBLE if (response.isSuccessful) { if (response.body()?.patient != null) { - showMessage(response.message()) - onBackPressedDispatcher.onBackPressed() + showMessage("Patient added successfully") + onBackPressed() } else { showMessage(response.body()?.apiError ?: "Failed to add patient") } } else { - val errorResponse = - Gson().fromJson( - response.errorBody()?.string(), - ApiErrorResponse::class.java, - ) + val errorResponse = Gson().fromJson(response.errorBody()?.string(), ApiErrorResponse::class.java) showMessage(errorResponse.apiError ?: response.message()) } } } } - private fun validateInputs(): Boolean { - val localBinding = binding - return when (localBinding) { - is ActivityAddNewPatientBinding -> { - if (localBinding.txtName.text.toString().trim().isEmpty()) { - showMessage(getString(R.string.validation_empty_name)) - false - } else if (localBinding.txtDob.text.toString().trim().isEmpty()) { - showMessage(getString(R.string.validation_empty_dob)) - false - } else if (localBinding.genderSpinner.selectedItemPosition == 0) { - showMessage(getString(R.string.validation_empty_gender)) - false - } else { - true + + private val GALLERY_REQUEST_CODE = 100 + + private fun openGallery() { + val intent = Intent(Intent.ACTION_PICK) + intent.type = "image/*" + startActivityForResult(intent, GALLERY_REQUEST_CODE) + } + + + private val CAMERA_REQUEST_CODE = 101 + private var photoUri: Uri? = null + + private fun openCamera() { + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + // Prepare a temporary file for the photo + val photoFile = File(externalCacheDir, "temp_photo.jpg") + photoUri = FileProvider.getUriForFile(this, "${packageName}.provider", photoFile) + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri) + startActivityForResult(intent, CAMERA_REQUEST_CODE) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + GALLERY_REQUEST_CODE -> { + val selectedImage = data?.data + selectedImage?.let { imgPreview.setImageURI(it) } } - } - is ActivityAddNewPatientNurseBinding -> { - if (localBinding.txtName.text.toString().trim().isEmpty()) { - showMessage(getString(R.string.validation_empty_name)) - false - } else if (localBinding.txtDob.text.toString().trim().isEmpty()) { - showMessage(getString(R.string.validation_empty_dob)) - false - } else if (localBinding.genderSpinner.selectedItemPosition == 0) { - showMessage(getString(R.string.validation_empty_gender)) - false - } else { - true + CAMERA_REQUEST_CODE -> { + photoUri?.let { imgPreview.setImageURI(it) } } } - else -> false } } - private fun showMessage(message: String) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show() - } - - 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 birthday hasn't happened yet this year, subtract one - if (today.get(Calendar.DAY_OF_YEAR) < birthDate.get(Calendar.DAY_OF_YEAR)) { - age-- + private fun checkPermissions(): Boolean { + val cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + val storagePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + return if (cameraPermission == PackageManager.PERMISSION_GRANTED && + storagePermission == PackageManager.PERMISSION_GRANTED) { + true + } else { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE), + 102 + ) + false } - return age } - private fun prepareFilePart( - partName: String, - fileUri: Uri, - context: Context, - ): MultipartBody.Part? { - val contentResolver = context.contentResolver + private fun prepareFilePart(partName: String, fileUri: Uri): MultipartBody.Part { val inputStream = contentResolver.openInputStream(fileUri) - val fileBytes = inputStream?.readBytes() - if (fileBytes == null) return null - val requestFile = fileBytes.toRequestBody("image/*".toMediaTypeOrNull()) - return MultipartBody.Part.createFormData(partName, "profile.jpg", requestFile) + val bytes = inputStream?.readBytes() + val requestFile = bytes?.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(msg: String) { + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() } -} +} \ No newline at end of file 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 00000000..49933d0f --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/general/AssignNurseActivity.kt @@ -0,0 +1,158 @@ +@file:Suppress("ktlint") + +package deakin.gopher.guardian.view.general + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import deakin.gopher.guardian.R +import deakin.gopher.guardian.model.user.Nurse +import deakin.gopher.guardian.model.login.SessionManager +import deakin.gopher.guardian.services.api.ApiClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class AssignNurseActivity : AppCompatActivity() { + + private lateinit var spinnerNurses: Spinner + private lateinit var btnAssign: Button + private lateinit var progressBar: ProgressBar + + + private var nursesList: MutableList = mutableListOf() + private var assignedNurseIds: MutableSet = mutableSetOf() + private var selectedNurse: Nurse? = null + private var patientId: String = "" + private lateinit var patientName: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_assign_nurse) + + spinnerNurses = findViewById(R.id.spinnerNurses) + btnAssign = findViewById(R.id.btnAssign) + progressBar = findViewById(R.id.progressBar) + progressBar.visibility = View.GONE + + // Get patientId from intent extras + patientId = intent.getStringExtra("patientId") ?: "" + + fetchNurses() + + spinnerNurses.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + selectedNurse = if (position > 0) nursesList[position - 1] else null + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + + btnAssign.setOnClickListener { + if (selectedNurse != null) { + assignNurse(selectedNurse!!) + } else { + Toast.makeText(this, "Please select a nurse", Toast.LENGTH_SHORT).show() + } + } + } + + private fun fetchNurses() { + val token = "Bearer ${SessionManager.getToken()}" + progressBar.visibility = View.VISIBLE + CoroutineScope(Dispatchers.IO).launch { + try { + val response = ApiClient.apiService.getAllNurses(token) + if (response.isSuccessful && response.body() != null) { + val allNurses = response.body()!!.nurses + // Filter out nurses already assigned to this patient + nursesList.clear() + nursesList.addAll(allNurses.filter { it._id !in assignedNurseIds }) + + withContext(Dispatchers.Main) { + populateSpinner() + progressBar.visibility = View.GONE + } + } else { + withContext(Dispatchers.Main) { + progressBar.visibility = View.GONE + Toast.makeText(this@AssignNurseActivity, "Failed to fetch nurses", Toast.LENGTH_SHORT).show() + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + progressBar.visibility = View.GONE + Toast.makeText(this@AssignNurseActivity, "Error: ${e.localizedMessage}", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun populateSpinner() { + val names = mutableListOf("Select a nurse") + names.addAll(nursesList.map { it.fullname }) + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, names) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinnerNurses.adapter = adapter + } + + private fun assignNurse(nurse: Nurse) { + val token = "Bearer ${SessionManager.getToken()}" + val body = mapOf( + "nurseId" to nurse._id, + "patientId" to patientId + ) + + progressBar.visibility = View.VISIBLE + CoroutineScope(Dispatchers.IO).launch { + try { + val response = ApiClient.apiService.assignNurse(token, body) + withContext(Dispatchers.Main) { + progressBar.visibility = View.GONE + if (response.isSuccessful) { + Toast.makeText( + this@AssignNurseActivity, + "Assigned nurse: ${nurse.fullname}", + Toast.LENGTH_SHORT + ).show() + + sendAssignmentNotification(nurse, patientName = patientName ) + nursesList.remove(nurse) + populateSpinner() + } else { + Toast.makeText(this@AssignNurseActivity, "Failed to assign nurse", Toast.LENGTH_SHORT).show() + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + progressBar.visibility = View.GONE + Toast.makeText(this@AssignNurseActivity, "Error: ${e.localizedMessage}", Toast.LENGTH_SHORT).show() + } + } + } + } + + private fun sendAssignmentNotification(nurse: Nurse, patientName: String) { + val token = "Bearer ${SessionManager.getToken()}" + val body = mapOf( + "userId" to nurse._id, + "title" to "New Patient Assigned", + "message" to "Patient has been assigned to you" + ) + + CoroutineScope(Dispatchers.IO).launch { + try { + val resp = ApiClient.apiService.createNotification(token, body) + if (!resp.isSuccessful) { + Log.e("AssignNurse", "Notification API failed: ${resp.code()}") + } + } catch (e: Exception) { + Log.e("AssignNurse", "Notification API error", e) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt new file mode 100644 index 00000000..436caa0d --- /dev/null +++ b/app/src/main/java/deakin/gopher/guardian/view/general/EditPatientActivity.kt @@ -0,0 +1,83 @@ +@file:Suppress("ktlint") + +package deakin.gopher.guardian.view.general + +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import deakin.gopher.guardian.databinding.ActivityEditPatientBinding +import deakin.gopher.guardian.model.Patient +import deakin.gopher.guardian.model.login.SessionManager +import deakin.gopher.guardian.services.api.ApiClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.Response + +// DTO for update request (matches API spec) +data class UpdatePatientRequest( + val fullname: String, + val dateOfBirth: String, + val gender: String +) + +class EditPatientActivity : AppCompatActivity() { + + private lateinit var binding: ActivityEditPatientBinding + private lateinit var patient: Patient + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityEditPatientBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Get patient from Intent + patient = intent.getSerializableExtra("patient") as Patient + + // Pre-fill fields with current data + binding.etFullname.setText(patient.fullname) + binding.etDateOfBirth.setText(patient.dateOfBirth) + binding.etGender.setText(patient.gender) + + // Save button + binding.btnSave.setOnClickListener { + updatePatient() + } + } + + private fun updatePatient() { + val updatedPatient = UpdatePatientRequest( + fullname = binding.etFullname.text.toString().trim(), + dateOfBirth = binding.etDateOfBirth.text.toString().trim(), + gender = binding.etGender.text.toString().trim() + ) + + val token = "Bearer ${SessionManager.getToken()}" + + CoroutineScope(Dispatchers.IO).launch { + val response = try { + ApiClient.apiService.updatePatient( + token = token, + patientId = patient.id, // 👈 ensure this is the DB ObjectId + request = updatedPatient + ) + } catch (e: Exception) { + null + } + + withContext(Dispatchers.Main) { + if (response?.isSuccessful == true) { + Toast.makeText(this@EditPatientActivity, "Patient updated successfully", Toast.LENGTH_SHORT).show() + finish() + } else { + Toast.makeText( + this@EditPatientActivity, + "Update failed: ${response?.errorBody()?.string() ?: response?.message() ?: "Unknown"}", + Toast.LENGTH_LONG + ).show() + } + } + } + } +} \ No newline at end of file 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 4b529e37..3e55414a 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 @@ -1,23 +1,40 @@ package deakin.gopher.guardian.view.general import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View import android.widget.Button +import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.coordinatorlayout.widget.CoordinatorLayout import deakin.gopher.guardian.R import deakin.gopher.guardian.model.login.SessionManager +import deakin.gopher.guardian.model.register.NotificationItem import deakin.gopher.guardian.services.EmailPasswordAuthService import deakin.gopher.guardian.services.NavigationService +import deakin.gopher.guardian.services.api.ApiClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class Homepage4nurse : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_homepage4nurse) + // Get current nurse user val user = SessionManager.getCurrentUser() + val userId = user.id // nurse ID + + // Update welcome title val titleText: TextView = findViewById(R.id.medicalDiagnosticsTitleTextView) titleText.append(" ${user.name.split(" ").getOrNull(0) ?: ""}") + // Initialize buttons val patientsButton: Button = findViewById(R.id.patientsButton_nurse) val settingsButton: Button = findViewById(R.id.settingsButton_nurse) val signOutButton: Button = findViewById(R.id.sighOutButton_nurse) @@ -26,15 +43,99 @@ class Homepage4nurse : AppCompatActivity() { NavigationService(this).onLaunchPatientList() } - // settings button settingsButton.setOnClickListener { NavigationService(this).onSettings() } - // sign out button signOutButton.setOnClickListener { EmailPasswordAuthService.signOut(this) finish() } + + // Fetch notifications from backend + fetchNotifications() + + val coordinatorLayout: CoordinatorLayout = findViewById(R.id.homepageCoordinator) + + showDropdownNotification( + coordinatorLayout, + "New Patient Assigned", + "A new Patient has been assigned to you", + ) + } + + private fun fetchNotifications() { + val token = "Bearer ${SessionManager.getToken()}" + + CoroutineScope(Dispatchers.IO).launch { + try { + val response = ApiClient.apiService.getMyNotifications(token) + if (response.isSuccessful) { + val notifications: List = response.body() ?: emptyList() + + // Display notifications (toast for now) + withContext(Dispatchers.Main) { + notifications.forEach { notification -> + Toast.makeText( + this@Homepage4nurse, + notification.message, + Toast.LENGTH_LONG, + ).show() + } + } + } else { + Log.e("Homepage4nurse", "Failed to fetch notifications: ${response.code()}") + } + } catch (e: Exception) { + Log.e("Homepage4nurse", "Error fetching notifications", e) + } + } + } + + fun showDropdownNotification( + coordinatorLayout: CoordinatorLayout, + title: String, + message: String, + ) { + val inflater = LayoutInflater.from(coordinatorLayout.context) + val notificationView = + inflater.inflate(R.layout.dropdown_notification, coordinatorLayout, false) + + val titleText: TextView = notificationView.findViewById(R.id.notificationTitle) + val messageText: TextView = notificationView.findViewById(R.id.notificationMessage) + val dismissBtn: ImageView = notificationView.findViewById(R.id.notificationDismiss) + + titleText.text = title + messageText.text = message + + // Add view to CoordinatorLayout + coordinatorLayout.addView(notificationView) + + // Slide-down animation + notificationView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val height = notificationView.measuredHeight.toFloat() + + notificationView.translationY = -height + notificationView.animate() + .translationY(0f) + .setDuration(300) + .start() + + // Auto-dismiss after 2s + val dismissRunnable = + Runnable { + notificationView.animate() + .translationY(-height) + .setDuration(300) + .withEndAction { coordinatorLayout.removeView(notificationView) } + .start() + } + notificationView.postDelayed(dismissRunnable, 2000) + + // Manual dismiss + dismissBtn.setOnClickListener { + notificationView.removeCallbacks(dismissRunnable) + dismissRunnable.run() + } } } diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt index c03d645d..0093652f 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/LoginActivity.kt @@ -87,7 +87,7 @@ class LoginActivity : BaseActivity() { val user = response.body()!!.user val token = response.body()!!.token SessionManager.createLoginSession(user, token) - NavigationService(this@LoginActivity).toPinCodeActivity(user.role) + NavigationService(this@LoginActivity).toHomeScreenForRole(user.role) } else { // Handle error val errorResponse = diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/MedicalSummaryScreen.kt b/app/src/main/java/deakin/gopher/guardian/view/general/MedicalSummaryScreen.kt index cce54bbd..37484864 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/general/MedicalSummaryScreen.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/general/MedicalSummaryScreen.kt @@ -1,3 +1,6 @@ + +@file:Suppress("ktlint") + package deakin.gopher.guardian import androidx.compose.foundation.Image @@ -165,4 +168,4 @@ fun MedicalSummaryScreen( ) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/PatientDetailsActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/PatientDetailsActivity.kt index 0e9f2ae4..facd4de5 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,18 +53,6 @@ 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() } - } - } - 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) diff --git a/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/general/PatientListActivity.kt index c2bbfe07..ab3b20fd 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 @@ -34,9 +34,15 @@ class PatientListActivity : BaseActivity() { startActivity(intent) }, onAssignNurseClick = { patient -> -// val intent = Intent(this, AssignNurseActivity::class.java) -// intent.putExtra("patientId", patient.id) -// startActivity(intent) + val intent = Intent(this, AssignNurseActivity::class.java) + intent.putExtra("patientId", patient.id) + intent.putExtra("patientName", patient.fullname) + startActivity(intent) + }, + onEditClick = { patient -> + val intent = Intent(this, EditPatientActivity::class.java) + intent.putExtra("patient", patient) + startActivity(intent) }, onDeleteClick = { patient -> confirmDeletePatient(patient) @@ -44,8 +50,37 @@ class PatientListActivity : BaseActivity() { ) private fun confirmDeletePatient(patient: Patient) { - // Optional: show a confirmation dialog before deleting - deletePatient(patient) + val builder = androidx.appcompat.app.AlertDialog.Builder(this) + builder.setTitle("Delete Patient") + builder.setMessage("Are you sure you want to delete?") + + builder.setPositiveButton("Delete") { dialog, _ -> + deletePatient(patient) + dialog.dismiss() + } + + builder.setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + + val dialog = builder.create() + dialog.show() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_add_patient -> { + val intent = Intent(this, AddNewPatientActivity::class.java) + startActivity(intent) + true + } + R.id.assign_nurse -> { + val intent = Intent(this, AssignNurseActivity::class.java) + startActivity(intent) + true + } + else -> super.onOptionsItemSelected(item) + } } private fun deletePatient(patient: Patient) { @@ -60,7 +95,7 @@ class PatientListActivity : BaseActivity() { withContext(Dispatchers.Main) { if (response?.isSuccessful == true) { showMessage("Patient deleted") - fetchPatients() // Refresh the patient list + fetchPatients() } else { showMessage("Failed to delete patient") } @@ -101,24 +136,25 @@ class PatientListActivity : BaseActivity() { binding.progressBar.show() } } + val response = ApiClient.apiService.getAssignedPatients(token) + withContext(Dispatchers.Main) { - withContext(Dispatchers.Main) { - binding.progressBar.hide() - } + binding.progressBar.hide() + if (response.isSuccessful) { - if (!response.body().isNullOrEmpty()) { - patientListAdapter.updateData(response.body()!!) - withContext(Dispatchers.Main) { - binding.tvEmptyMessage.visibility = View.GONE + val patients = + response.body()?.filter { patient -> + patient.isDeleted != true } + + if (!patients.isNullOrEmpty()) { + patientListAdapter.updateData(patients) + binding.tvEmptyMessage.visibility = View.GONE } else { - withContext(Dispatchers.Main) { - binding.tvEmptyMessage.visibility = View.VISIBLE - } + binding.tvEmptyMessage.visibility = View.VISIBLE } } else { - // Handle error val errorResponse = Gson().fromJson( response.errorBody()?.string(), @@ -131,21 +167,10 @@ class PatientListActivity : BaseActivity() { } override fun onCreateOptionsMenu(menu: Menu?): Boolean { - if (currentUser.organization != null) { - return false - } - menuInflater.inflate(R.menu.menu_patient_list, menu) + menuInflater.inflate(R.menu.menu_add_patient, menu) return true } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == R.id.action_add_patient) { - startActivity(Intent(this, AddNewPatientActivity::class.java)) - return true - } - return super.onOptionsItemSelected(item) - } - private fun showMessage(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/deakin/gopher/guardian/view/patient/DoctorHomeScreen.kt b/app/src/main/java/deakin/gopher/guardian/view/patient/DoctorHomeScreen.kt index 83c86b44..90b8ed27 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/patient/DoctorHomeScreen.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/patient/DoctorHomeScreen.kt @@ -1,3 +1,6 @@ +@file:Suppress("ktlint") + + package deakin.gopher.guardian import androidx.compose.foundation.Image @@ -139,4 +142,4 @@ fun HomeCard( Text(text = label, fontSize = 13.sp, color = Color.White) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/components/HeartRateInput.kt b/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/components/HeartRateInput.kt index 40068b5c..0f5a9caa 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/components/HeartRateInput.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/components/HeartRateInput.kt @@ -1,3 +1,6 @@ +@file:Suppress("ktlint") + + package deakin.gopher.guardian.view.patient.patientdata.heartrate.ui.components import androidx.compose.foundation.layout.Arrangement @@ -81,4 +84,4 @@ fun HeartRateInputPreview() { GuardianTheme { HeartRateInput { _, _ -> } } -} +} \ No newline at end of file diff --git a/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/screens/AddHeartRateScreen.kt b/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/screens/AddHeartRateScreen.kt index a50a19d2..3205077b 100644 --- a/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/screens/AddHeartRateScreen.kt +++ b/app/src/main/java/deakin/gopher/guardian/view/patient/patientdata/heartrate/ui/screens/AddHeartRateScreen.kt @@ -1,3 +1,6 @@ +@file:Suppress("ktlint") + + package deakin.gopher.guardian.view.patient.patientdata.heartrate.ui.screens import android.widget.Toast @@ -75,4 +78,4 @@ fun AddHeartRateScreen(navController: NavController) { }) } } -} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 00000000..7bd1f481 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/notification_background.xml b/app/src/main/res/drawable/notification_background.xml new file mode 100644 index 00000000..a4edcefa --- /dev/null +++ b/app/src/main/res/drawable/notification_background.xml @@ -0,0 +1,5 @@ + + + + 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 00000000..f7cb19a0 --- /dev/null +++ b/app/src/main/res/layout/activity_assign_nurse.xml @@ -0,0 +1,38 @@ + + + + + + + + +