diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 837d337b..93ce9811 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -36,7 +36,8 @@
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup,android:label"
tools:targetApi="31"
- tools:ignore="ExtraText">
+ tools:ignore="ExtraText"
+ android:networkSecurityConfig="@xml/network_security_config">
@@ -185,6 +186,7 @@
android:name=".view.general.TasksListActivity"
android:exported="false" />
+
@@ -207,6 +209,21 @@
+
+
+
+
+
+
,
+ private val timeSlots: List,
+ private val onSlotClick: (Date, String) -> Unit
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarSlotViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_calendar_slot, parent, false)
+ return CalendarSlotViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: CalendarSlotViewHolder, position: Int) {
+ val dayIndex = position % 7
+ val timeIndex = position / 7
+
+ if (dayIndex < weekDays.size && timeIndex < timeSlots.size) {
+ val date = weekDays[dayIndex]
+ val time = timeSlots[timeIndex]
+ holder.bind(date, time, onSlotClick)
+ }
+ }
+
+ override fun getItemCount() = weekDays.size * timeSlots.size
+
+ fun updateWeekDays(newWeekDays: List) {
+ weekDays = newWeekDays
+ notifyDataSetChanged()
+ }
+
+ class CalendarSlotViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ fun bind(date: Date, time: String, onSlotClick: (Date, String) -> Unit) {
+ itemView.setOnClickListener {
+ onSlotClick(date, time)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/LogbookAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/LogbookAdapter.kt
new file mode 100644
index 00000000..e4005636
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/LogbookAdapter.kt
@@ -0,0 +1,86 @@
+package deakin.gopher.guardian.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.recyclerview.widget.RecyclerView
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.model.logbook.LogEntry
+import java.text.SimpleDateFormat
+import java.util.*
+
+class LogbookAdapter(
+ private val onLogClick: (LogEntry) -> Unit,
+ private val onDeleteClick: (LogEntry) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var logs = mutableListOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LogEntryViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_log_entry, parent, false)
+ return LogEntryViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: LogEntryViewHolder, position: Int) {
+ holder.bind(logs[position], onLogClick, onDeleteClick)
+ }
+
+ override fun getItemCount() = logs.size
+
+ fun updateLogs(newLogs: List) {
+ logs.clear()
+ logs.addAll(newLogs)
+ notifyDataSetChanged()
+ }
+
+ class LogEntryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val titleText: TextView = itemView.findViewById(R.id.log_title)
+ private val descriptionText: TextView = itemView.findViewById(R.id.log_description)
+ private val createdByText: TextView = itemView.findViewById(R.id.created_by_text)
+ private val createdAtText: TextView = itemView.findViewById(R.id.created_at_text)
+ private val deleteButton: ImageButton = itemView.findViewById(R.id.delete_button)
+ private val roleBadge: TextView = itemView.findViewById(R.id.role_badge)
+
+ fun bind(
+ log: LogEntry,
+ onLogClick: (LogEntry) -> Unit,
+ onDeleteClick: (LogEntry) -> Unit
+ ) {
+ titleText.text = log.title
+ descriptionText.text = log.description
+ createdByText.text = log.createdBy.fullname
+
+ // Format the creation date
+ val formattedDate = formatCreatedAt(log.createdAt)
+ createdAtText.text = formattedDate
+
+ // Set role badge
+ roleBadge.text = log.createdBy.role.uppercase()
+ val roleBadgeColor = when (log.createdBy.role.lowercase()) {
+ "nurse" -> itemView.context.getColor(R.color.nurse_role_color)
+ "admin" -> itemView.context.getColor(R.color.admin_role_color)
+ "doctor" -> itemView.context.getColor(R.color.doctor_role_color)
+ else -> itemView.context.getColor(R.color.default_role_color)
+ }
+ roleBadge.setBackgroundColor(roleBadgeColor)
+
+ // Click listeners
+ itemView.setOnClickListener { onLogClick(log) }
+ deleteButton.setOnClickListener { onDeleteClick(log) }
+ }
+
+ private fun formatCreatedAt(createdAt: String): String {
+ return try {
+ val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ val outputFormat = SimpleDateFormat("MMM dd, yyyy 'at' h:mm a", Locale.getDefault())
+ val date = inputFormat.parse(createdAt)
+ date?.let { outputFormat.format(it) } ?: createdAt
+ } catch (e: Exception) {
+ // Fallback to original string if parsing fails
+ createdAt
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/TaskEventsAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/TaskEventsAdapter.kt
new file mode 100644
index 00000000..82062f63
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/TaskEventsAdapter.kt
@@ -0,0 +1,215 @@
+package deakin.gopher.guardian.adapter
+
+import deakin.gopher.guardian.util.CalendarUtils
+
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.recyclerview.widget.RecyclerView
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.model.calendar.TaskResponse
+
+import java.text.SimpleDateFormat
+import java.util.*
+
+class TaskEventsAdapter(
+ private val onTaskClick: (TaskResponse) -> Unit,
+ private val onTaskComplete: (String) -> Unit,
+ private val onTaskEdit: (TaskResponse) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var displayItems = mutableListOf()
+
+ companion object {
+ const val TYPE_DATE_HEADER = 0
+ const val TYPE_TASK_ITEM = 1
+ const val TYPE_NO_EVENTS = 2
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return when (displayItems[position]) {
+ is DisplayItem.DateHeader -> TYPE_DATE_HEADER
+ is DisplayItem.TaskItem -> TYPE_TASK_ITEM
+ is DisplayItem.NoEvents -> TYPE_NO_EVENTS
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return when (viewType) {
+ TYPE_DATE_HEADER -> {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_date_header, parent, false)
+ DateHeaderViewHolder(view)
+ }
+ TYPE_TASK_ITEM -> {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_task_event, parent, false)
+ TaskEventViewHolder(view)
+ }
+ TYPE_NO_EVENTS -> {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_no_events, parent, false)
+ NoEventsViewHolder(view)
+ }
+ else -> throw IllegalArgumentException("Unknown view type: $viewType")
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ when (val item = displayItems[position]) {
+ is DisplayItem.DateHeader -> {
+ (holder as DateHeaderViewHolder).bind(item.date)
+ }
+ is DisplayItem.TaskItem -> {
+ (holder as TaskEventViewHolder).bind(
+ item.task,
+ onTaskClick,
+ onTaskComplete,
+ onTaskEdit
+ )
+ }
+ is DisplayItem.NoEvents -> {
+ // No binding needed
+ }
+ }
+ }
+
+ override fun getItemCount() = displayItems.size
+
+ fun updateTasks(tasks: List) {
+ displayItems.clear()
+
+ if (tasks.isEmpty()) {
+ displayItems.add(DisplayItem.NoEvents)
+ notifyDataSetChanged()
+ return
+ }
+
+ // Group tasks by date and sort
+ val tasksByDate = tasks.groupBy { task ->
+ task.dueDate?.let {
+ val date = CalendarUtils.parseDateString(it)
+ SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
+ } ?: "unknown"
+ }.toSortedMap()
+
+ tasksByDate.forEach { (dateString, tasksForDate) ->
+ if (dateString != "unknown") {
+ val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(dateString) ?: Date()
+ displayItems.add(DisplayItem.DateHeader(date))
+
+ // Sort tasks by time for the day
+ val sortedTasks = tasksForDate.sortedBy { task ->
+ task.dueDate?.let { CalendarUtils.parseDateString(it) } ?: Date(0)
+ }
+
+ sortedTasks.forEach { task ->
+ displayItems.add(DisplayItem.TaskItem(task))
+ }
+ }
+ }
+
+ notifyDataSetChanged()
+ }
+
+ sealed class DisplayItem {
+ data class DateHeader(val date: Date) : DisplayItem()
+ data class TaskItem(val task: TaskResponse) : DisplayItem()
+ object NoEvents : DisplayItem()
+ }
+
+ class DateHeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val dateText: TextView = itemView.findViewById(R.id.date_text)
+
+ fun bind(date: Date) {
+ dateText.text = CalendarUtils.formatDateHeader(date)
+ }
+ }
+
+ class TaskEventViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val timeText: TextView = itemView.findViewById(R.id.time_text)
+ private val durationText: TextView = itemView.findViewById(R.id.duration_text)
+ private val titleText: TextView = itemView.findViewById(R.id.title_text)
+ private val descriptionText: TextView = itemView.findViewById(R.id.description_text)
+ private val priorityIndicator: View = itemView.findViewById(R.id.priority_indicator)
+ private val statusIcon: ImageView = itemView.findViewById(R.id.status_icon)
+ private val patientBadge: TextView = itemView.findViewById(R.id.patient_badge)
+ private val completeButton: ImageButton = itemView.findViewById(R.id.complete_button)
+ private val editButton: ImageButton = itemView.findViewById(R.id.edit_button)
+
+ fun bind(
+ task: TaskResponse,
+ onTaskClick: (TaskResponse) -> Unit,
+ onTaskComplete: (String) -> Unit,
+ onTaskEdit: (TaskResponse) -> Unit
+ ) {
+ // Time formatting
+ val time = task.dueDate?.let {
+ val date = CalendarUtils.parseDateString(it)
+ SimpleDateFormat("h:mm a", Locale.getDefault()).format(date)
+ } ?: "No time"
+
+ timeText.text = time
+ durationText.text = "1h" // You can calculate or store duration
+ titleText.text = task.title
+
+ // Show description if available and different from title
+ if (task.description.isNotBlank() && task.description != task.title) {
+ descriptionText.text = task.description
+ descriptionText.visibility = View.VISIBLE
+ } else {
+ descriptionText.visibility = View.GONE
+ }
+
+ // Priority indicator
+ val priorityColor = when (task.priority.lowercase()) {
+ "urgent", "high" -> itemView.context.getColor(R.color.priority_high)
+ "medium" -> itemView.context.getColor(R.color.priority_medium)
+ "low" -> itemView.context.getColor(R.color.priority_low)
+ else -> itemView.context.getColor(R.color.priority_medium)
+ }
+ priorityIndicator.setBackgroundColor(priorityColor)
+
+ // Status icon and styling
+ val isCompleted = task.status.lowercase() == "completed"
+ when (task.status.lowercase()) {
+ "completed" -> {
+ statusIcon.setImageResource(R.drawable.ic_check_circle)
+ statusIcon.setColorFilter(itemView.context.getColor(R.color.success_color))
+ itemView.alpha = 0.7f
+ }
+ "in_progress" -> {
+ statusIcon.setImageResource(R.drawable.ic_in_progress)
+ statusIcon.setColorFilter(itemView.context.getColor(R.color.warning_color))
+ itemView.alpha = 1.0f
+ }
+ else -> {
+ statusIcon.setImageResource(R.drawable.ic_pending)
+ statusIcon.setColorFilter(itemView.context.getColor(R.color.text_secondary))
+ itemView.alpha = 1.0f
+ }
+ }
+
+ // Patient badge
+ if (task.patientId != null) {
+ patientBadge.visibility = View.VISIBLE
+ patientBadge.text = "Patient"
+ } else {
+ patientBadge.visibility = View.GONE
+ }
+
+ // Action buttons
+ completeButton.visibility = if (isCompleted) View.GONE else View.VISIBLE
+ completeButton.setOnClickListener { onTaskComplete(task.id) }
+
+ editButton.setOnClickListener { onTaskEdit(task) }
+
+ // Click listeners
+ itemView.setOnClickListener { onTaskClick(task) }
+ }
+ }
+
+ class NoEventsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/TimeSlotAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/TimeSlotAdapter.kt
new file mode 100644
index 00000000..f073f6f8
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/TimeSlotAdapter.kt
@@ -0,0 +1,33 @@
+package deakin.gopher.guardian.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import deakin.gopher.guardian.R
+
+class TimeSlotAdapter(
+ private val timeSlots: List
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimeSlotViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_time_slot, parent, false)
+ return TimeSlotViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: TimeSlotViewHolder, position: Int) {
+ holder.bind(timeSlots[position])
+ }
+
+ override fun getItemCount() = timeSlots.size
+
+ class TimeSlotViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val timeText: TextView = itemView.findViewById(R.id.time_text)
+
+ fun bind(time: String) {
+ timeText.text = time
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/WeekCalendarAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/WeekCalendarAdapter.kt
new file mode 100644
index 00000000..0ceaaaf5
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/WeekCalendarAdapter.kt
@@ -0,0 +1,51 @@
+package deakin.gopher.guardian.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import deakin.gopher.guardian.R
+import java.text.SimpleDateFormat
+import java.util.*
+
+class WeekCalendarAdapter(
+ private var weekDays: List,
+ private val onDateClick: (Date) -> Unit
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeekDayViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_week_day, parent, false)
+ return WeekDayViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: WeekDayViewHolder, position: Int) {
+ holder.bind(weekDays[position], onDateClick)
+ }
+
+ override fun getItemCount() = weekDays.size
+
+ fun updateDays(newDays: List) {
+ weekDays = newDays
+ notifyDataSetChanged()
+ }
+
+ class WeekDayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val dayText: TextView = itemView.findViewById(R.id.day_text)
+ private val dateText: TextView = itemView.findViewById(R.id.date_text)
+
+ fun bind(date: Date, onDateClick: (Date) -> Unit) {
+ val calendar = Calendar.getInstance().apply { time = date }
+
+ // Set day name
+ val dayFormat = SimpleDateFormat("EEE", Locale.getDefault())
+ dayText.text = dayFormat.format(date).take(2)
+
+ // Set date number
+ dateText.text = calendar.get(Calendar.DAY_OF_MONTH).toString()
+
+ itemView.setOnClickListener { onDateClick(date) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/adapter/WeekDaysAdapter.kt b/app/src/main/java/deakin/gopher/guardian/adapter/WeekDaysAdapter.kt
new file mode 100644
index 00000000..e36738e2
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/adapter/WeekDaysAdapter.kt
@@ -0,0 +1,100 @@
+package deakin.gopher.guardian.adapter
+
+
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.util.CalendarUtils
+import java.text.SimpleDateFormat
+import java.util.*
+
+class WeekDaysAdapter(
+ private var weekDays: List,
+ private val onDateClick: (Date) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var selectedPosition = -1
+
+ init {
+ // Find today's position if it's in the current week
+ weekDays.forEachIndexed { index, date ->
+ if (CalendarUtils.isToday(date)) {
+ selectedPosition = index
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeekDayViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_week_day, parent, false)
+ return WeekDayViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: WeekDayViewHolder, position: Int) {
+ holder.bind(weekDays[position], position == selectedPosition) {
+ val oldPosition = selectedPosition
+ selectedPosition = position
+ notifyItemChanged(oldPosition)
+ notifyItemChanged(selectedPosition)
+ onDateClick(weekDays[position])
+ }
+ }
+
+ override fun getItemCount() = weekDays.size
+
+ fun updateDays(newDays: List) {
+ weekDays = newDays
+ // Reset selection to today if present
+ selectedPosition = -1
+ newDays.forEachIndexed { index, date ->
+ if (CalendarUtils.isToday(date)) {
+ selectedPosition = index
+ }
+ }
+ notifyDataSetChanged()
+ }
+
+ class WeekDayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val dayText: TextView = itemView.findViewById(R.id.day_text)
+ private val dateText: TextView = itemView.findViewById(R.id.date_text)
+ private val container: View = itemView.findViewById(R.id.day_container)
+
+ fun bind(date: Date, isSelected: Boolean, onClick: () -> Unit) {
+ val calendar = Calendar.getInstance().apply { time = date }
+
+ // Set day name (M, Tu, W, etc.)
+ val dayFormat = SimpleDateFormat("EEE", Locale.getDefault())
+ val dayName = dayFormat.format(date)
+ dayText.text = when (dayName) {
+ "Mon" -> "M"
+ "Tue" -> "Tu"
+ "Wed" -> "W"
+ "Thu" -> "Th"
+ "Fri" -> "F"
+ "Sat" -> "Sa"
+ "Sun" -> "Su"
+ else -> dayName.take(2)
+ }
+
+ // Set date number
+ dateText.text = calendar.get(Calendar.DAY_OF_MONTH).toString()
+
+ // Handle selection state
+ if (isSelected) {
+ container.setBackgroundResource(R.drawable.selected_day_background)
+ dayText.setTextColor(itemView.context.getColor(android.R.color.white))
+ dateText.setTextColor(itemView.context.getColor(android.R.color.white))
+ } else {
+ container.setBackgroundResource(R.drawable.unselected_day_background)
+ dayText.setTextColor(itemView.context.getColor(R.color.text_secondary))
+ dateText.setTextColor(itemView.context.getColor(R.color.text_primary))
+ }
+
+ container.setOnClickListener { onClick() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/model/calendar/TaskModels.kt b/app/src/main/java/deakin/gopher/guardian/model/calendar/TaskModels.kt
new file mode 100644
index 00000000..b1454ab3
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/model/calendar/TaskModels.kt
@@ -0,0 +1,99 @@
+// Fixed TaskModels.kt without JVM signature clash
+package deakin.gopher.guardian.model.calendar
+
+import com.google.gson.annotations.SerializedName
+import deakin.gopher.guardian.model.BaseModel
+
+// Request Models (these don't need to extend BaseModel)
+data class CreateTaskRequest(
+ val title: String,
+ val description: String,
+ val priority: String, // "low", "medium", "high", "urgent"
+ val status: String = "pending",
+ val dueDate: String, // ISO 8601 format
+ val caretaker: String, // Caretaker user ID
+ val patientId: String? = null
+)
+
+data class UpdateTaskRequest(
+ val title: String? = null,
+ val description: String? = null,
+ val priority: String? = null,
+ val status: String? = null,
+ val dueDate: String? = null,
+ val patientId: String? = null
+)
+
+data class UpdateTaskStatusRequest(
+ val status: String // "pending", "in_progress", "completed", "cancelled"
+)
+
+// Simple Task data class (without BaseModel inheritance for cleaner structure)
+data class TaskResponse(
+ @SerializedName("_id")
+ val id: String,
+ val title: String,
+ val description: String,
+ val priority: String,
+ val status: String,
+ @SerializedName("due_date")
+ val dueDate: String?,
+ @SerializedName("created_at")
+ val createdAt: String,
+ @SerializedName("updated_at")
+ val updatedAt: String,
+ val caretaker: String,
+ val patientId: String? = null
+)
+
+// Wrapper response for single task operations - FIXED VERSION
+data class TaskApiResponse(
+ val task: TaskResponse? = null,
+ val data: TaskResponse? = null // Some APIs return data instead of task
+) : BaseModel() {
+ // Use a computed property instead of a function to avoid JVM signature clash
+ val actualTask: TaskResponse?
+ get() = task ?: data
+}
+
+// Wrapper response for task list operations - FIXED VERSION
+data class TaskListApiResponse(
+ val tasks: List? = null,
+ val data: List? = null, // Some APIs return data instead of tasks
+ val totalCount: Int = 0,
+ val currentPage: Int = 1,
+ val totalPages: Int = 1,
+ val hasNextPage: Boolean = false,
+ val hasPrevPage: Boolean = false
+) : BaseModel() {
+ // Use a computed property instead of a function
+ val actualTasks: List
+ get() = tasks ?: data ?: emptyList()
+}
+
+// For operations that just return success/failure
+class TaskOperationResponse : BaseModel()
+
+// Extension function for TaskResponse copy
+fun TaskResponse.copy(
+ id: String = this.id,
+ title: String = this.title,
+ description: String = this.description,
+ priority: String = this.priority,
+ status: String = this.status,
+ dueDate: String? = this.dueDate,
+ createdAt: String = this.createdAt,
+ updatedAt: String = this.updatedAt,
+ caretaker: String = this.caretaker,
+ patientId: String? = this.patientId
+) = TaskResponse(id, title, description, priority, status, dueDate, createdAt, updatedAt, caretaker, patientId)
+
+// Extension function to convert CreateTaskRequest to UpdateTaskRequest
+fun CreateTaskRequest.toUpdateRequest() = UpdateTaskRequest(
+ title = this.title,
+ description = this.description,
+ priority = this.priority,
+ status = this.status,
+ dueDate = this.dueDate,
+ patientId = this.patientId
+)
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/model/logbook/LogbookModels.kt b/app/src/main/java/deakin/gopher/guardian/model/logbook/LogbookModels.kt
new file mode 100644
index 00000000..d7e5284f
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/model/logbook/LogbookModels.kt
@@ -0,0 +1,46 @@
+package deakin.gopher.guardian.model.logbook
+
+import com.google.gson.annotations.SerializedName
+import deakin.gopher.guardian.model.BaseModel
+
+// Request Models
+data class CreateLogRequest(
+ val title: String,
+ val description: String,
+ val patient: String // Patient ID
+)
+
+// Response Models
+data class LogEntry(
+ @SerializedName("_id")
+ val id: String,
+ val title: String,
+ val description: String,
+ val patient: String,
+ val createdBy: LogCreator,
+ @SerializedName("createdAt")
+ val createdAt: String
+)
+
+data class LogCreator(
+ val fullname: String,
+ val role: String
+)
+
+// API Response Wrappers
+data class CreateLogResponse(
+ val message: String,
+ val log: LogEntry
+) : BaseModel()
+
+data class LogListResponse(
+ val logs: List? = null,
+ val data: List? = null
+) : BaseModel() {
+ val actualLogs: List
+ get() = logs ?: data ?: emptyList()
+}
+
+data class DeleteLogResponse(
+ val message: String = "Log deleted successfully"
+) : BaseModel()
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/repository/LogbookRepository.kt b/app/src/main/java/deakin/gopher/guardian/repository/LogbookRepository.kt
new file mode 100644
index 00000000..439be251
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/repository/LogbookRepository.kt
@@ -0,0 +1,105 @@
+package deakin.gopher.guardian.repository
+
+
+import deakin.gopher.guardian.services.api.ApiClient
+import deakin.gopher.guardian.model.logbook.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class LogbookRepository {
+
+ private val apiService = ApiClient.apiService
+
+ suspend fun createLog(
+ token: String,
+ request: CreateLogRequest
+ ): ApiResult {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.createPatientLog("Bearer $token", request)
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ ApiResult.Success(apiResponse.log)
+ }
+ } ?: ApiResult.Error("Empty response body")
+ } else {
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+
+ suspend fun getLogs(
+ token: String,
+ patientId: String? = null,
+ page: Int? = null,
+ limit: Int? = null
+ ): ApiResult> {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.getPatientLogs("Bearer $token", patientId, page, limit)
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ ApiResult.Success(apiResponse.actualLogs)
+ }
+ } ?: ApiResult.Error("Empty response body")
+ } else {
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+
+ suspend fun deleteLog(
+ token: String,
+ logId: String
+ ): ApiResult {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.deletePatientLog("Bearer $token", logId)
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ ApiResult.Success(apiResponse.message)
+ }
+ } ?: ApiResult.Success("Log deleted successfully")
+ } else {
+ val errorMessage = try {
+ when (response.code()) {
+ 403 -> "You don't have permission to delete this log"
+ 404 -> "Log not found"
+ else -> response.errorBody()?.string() ?: "Unknown error"
+ }
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/deakin/gopher/guardian/repository/TaskRepository.kt b/app/src/main/java/deakin/gopher/guardian/repository/TaskRepository.kt
new file mode 100644
index 00000000..38344202
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/repository/TaskRepository.kt
@@ -0,0 +1,202 @@
+// Updated TaskRepository.kt with fixed property names
+package deakin.gopher.guardian.repository
+
+import deakin.gopher.guardian.services.api.ApiClient
+import deakin.gopher.guardian.model.calendar.*
+import deakin.gopher.guardian.model.BaseModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class TaskRepository {
+
+ private val apiService = ApiClient.apiService
+
+ suspend fun createTask(
+ token: String,
+ request: CreateTaskRequest
+ ): ApiResult {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.createTask("Bearer $token", request)
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ // Check for API errors using your BaseModel structure
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ val task = apiResponse.actualTask // Using the new property name
+ if (task != null) {
+ ApiResult.Success(task)
+ } else {
+ ApiResult.Error("No task data in response")
+ }
+ }
+ } ?: ApiResult.Error("Empty response body")
+ } else {
+ // Parse error from response body
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+
+ suspend fun getTasks(
+ token: String,
+ caretakerId: String,
+ filter: String? = null,
+ status: String? = null,
+ dueDate: String? = null,
+ page: Int = 1,
+ limit: Int = 50,
+ sort: String? = "due_date"
+ ): ApiResult> {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.getTasks(
+ "Bearer $token", caretakerId, filter, status, dueDate, page, limit, sort
+ )
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ // Check for API errors using your BaseModel structure
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ val tasks = apiResponse.actualTasks // Using the new property name
+ ApiResult.Success(tasks)
+ }
+ } ?: ApiResult.Error("Empty response body")
+ } else {
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+
+ suspend fun updateTaskStatus(
+ token: String,
+ taskId: String,
+ status: String
+ ): ApiResult {
+ return withContext(Dispatchers.IO) {
+ try {
+ val request = UpdateTaskStatusRequest(status)
+ val response = apiService.updateTaskStatus("Bearer $token", taskId, request)
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ val task = apiResponse.actualTask // Using the new property name
+ if (task != null) {
+ ApiResult.Success(task)
+ } else {
+ ApiResult.Error("No task data in response")
+ }
+ }
+ } ?: ApiResult.Error("Empty response body")
+ } else {
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+
+ suspend fun updateTask(
+ token: String,
+ taskId: String,
+ request: UpdateTaskRequest
+ ): ApiResult {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.updateTask("Bearer $token", taskId, request)
+ if (response.isSuccessful) {
+ response.body()?.let { apiResponse ->
+ if (apiResponse.apiError != null) {
+ ApiResult.Error(apiResponse.apiError)
+ } else {
+ val task = apiResponse.actualTask // Using the new property name
+ if (task != null) {
+ ApiResult.Success(task)
+ } else {
+ ApiResult.Error("No task data in response")
+ }
+ }
+ } ?: ApiResult.Error("Empty response body")
+ } else {
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+
+ suspend fun deleteTask(token: String, taskId: String): ApiResult {
+ return withContext(Dispatchers.IO) {
+ try {
+ val response = apiService.deleteTask("Bearer $token", taskId)
+ if (response.isSuccessful) {
+ response.body()?.let { baseModel ->
+ if (baseModel.apiError != null) {
+ ApiResult.Error(baseModel.apiError)
+ } else {
+ ApiResult.Success(baseModel)
+ }
+ } ?: run {
+ // If no response body, assume success
+ ApiResult.Success(BaseModel())
+ }
+ } else {
+ val errorMessage = try {
+ response.errorBody()?.string() ?: "Unknown error"
+ } catch (e: Exception) {
+ "Network error: ${response.code()}"
+ }
+ ApiResult.Error(errorMessage, response.code())
+ }
+ } catch (e: Exception) {
+ ApiResult.Error(e.message ?: "Network error occurred")
+ }
+ }
+ }
+}
+
+// ApiResult sealed class (unchanged)
+sealed class ApiResult {
+ data class Success(val data: T) : ApiResult()
+ data class Error(val message: String, val code: Int? = null) : ApiResult()
+ data class Loading(val message: String = "Loading...") : ApiResult()
+
+ fun isSuccess(): Boolean = this is Success
+ fun isError(): Boolean = this is Error
+ fun isLoading(): Boolean = this is Loading
+
+ fun getDataOrNull(): T? = if (this is Success) data else null
+ fun getErrorMessage(): String? = if (this is Error) message else null
+}
\ 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 255c2e34..845a01aa 100644
--- a/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt
+++ b/app/src/main/java/deakin/gopher/guardian/services/NavigationService.kt
@@ -3,6 +3,7 @@ package deakin.gopher.guardian.services
import android.app.Activity
import android.content.Intent
import deakin.gopher.guardian.model.login.Role
+import deakin.gopher.guardian.view.logbook.PatientLogbookActivity
import deakin.gopher.guardian.view.general.Homepage4admin
import deakin.gopher.guardian.view.general.Homepage4caretaker
import deakin.gopher.guardian.view.general.Homepage4nurse
@@ -114,4 +115,15 @@ class NavigationService(val activity: Activity) {
intent.putExtra("role", role)
activity.startActivity(intent)
}
+
+ fun toLogbook(patientId: String? = null) {
+ val intent = Intent(
+ activity.applicationContext,
+ PatientLogbookActivity::class.java
+ )
+ patientId?.let {
+ intent.putExtra("PATIENT_ID", it)
+ }
+ activity.startActivity(intent)
+ }
}
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..1c2fc65c 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
@@ -7,23 +7,25 @@ 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.calendar.CreateTaskRequest
+import deakin.gopher.guardian.model.calendar.TaskApiResponse
+import deakin.gopher.guardian.model.calendar.TaskListApiResponse
+import deakin.gopher.guardian.model.calendar.TaskResponse
+
+import deakin.gopher.guardian.model.calendar.UpdateTaskRequest
+import deakin.gopher.guardian.model.calendar.UpdateTaskStatusRequest
+import deakin.gopher.guardian.model.logbook.CreateLogRequest
+import deakin.gopher.guardian.model.logbook.CreateLogResponse
+import deakin.gopher.guardian.model.logbook.DeleteLogResponse
+import deakin.gopher.guardian.model.logbook.LogListResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
-import retrofit2.http.Body
-import retrofit2.http.DELETE
-import retrofit2.http.Field
-import retrofit2.http.FormUrlEncoded
-import retrofit2.http.GET
-import retrofit2.http.Header
-import retrofit2.http.Multipart
-import retrofit2.http.POST
-import retrofit2.http.Part
-import retrofit2.http.Path
-import retrofit2.http.Query
+import retrofit2.http.*
interface ApiService {
+ // Your existing endpoints...
@POST("auth/register")
fun register(
@Body request: RegisterRequest,
@@ -91,4 +93,62 @@ interface ApiService {
@Header("Authorization") token: String,
@Path("id") patientId: String,
): Response
+
+ @POST("caretaker/tasks")
+ suspend fun createTask(
+ @Header("Authorization") token: String,
+ @Body request: CreateTaskRequest
+ ): Response
+
+ @GET("caretaker/tasks")
+ suspend fun getTasks(
+ @Header("Authorization") token: String,
+ @Query("caretakerId") caretakerId: String,
+ @Query("filter") filter: String? = null,
+ @Query("status") status: String? = null,
+ @Query("dueDate") dueDate: String? = null,
+ @Query("page") page: Int = 1,
+ @Query("limit") limit: Int = 10,
+ @Query("sort") sort: String? = null
+ ): Response
+
+ @PUT("caretaker/tasks/{taskId}")
+ suspend fun updateTask(
+ @Header("Authorization") token: String,
+ @Path("taskId") taskId: String,
+ @Body request: UpdateTaskRequest
+ ): Response
+
+ @PATCH("caretaker/tasks/{taskId}/status")
+ suspend fun updateTaskStatus(
+ @Header("Authorization") token: String,
+ @Path("taskId") taskId: String,
+ @Body request: UpdateTaskStatusRequest
+ ): Response
+ @DELETE("caretaker/tasks/{taskId}")
+ suspend fun deleteTask(
+ @Header("Authorization") token: String,
+ @Path("taskId") taskId: String
+ ): Response
+
+ @POST("patient-logs")
+ suspend fun createPatientLog(
+ @Header("Authorization") token: String,
+ @Body request: CreateLogRequest
+ ): Response
+
+ @GET("patient-logs")
+ suspend fun getPatientLogs(
+ @Header("Authorization") token: String,
+ @Query("patient") patientId: String? = null,
+ @Query("page") page: Int? = null,
+ @Query("limit") limit: Int? = null
+ ): Response
+
+ @DELETE("patient-logs/{logId}")
+ suspend fun deletePatientLog(
+ @Header("Authorization") token: String,
+ @Path("logId") logId: String
+ ): Response
+
}
diff --git a/app/src/main/java/deakin/gopher/guardian/util/CalendarUtils.kt b/app/src/main/java/deakin/gopher/guardian/util/CalendarUtils.kt
new file mode 100644
index 00000000..e7ebc63d
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/util/CalendarUtils.kt
@@ -0,0 +1,106 @@
+package deakin.gopher.guardian.util
+
+import java.text.SimpleDateFormat
+import java.util.*
+
+object CalendarUtils {
+
+ fun getWeekDays(centerDate: Date = Date()): List {
+ val days = mutableListOf()
+ val calendar = Calendar.getInstance().apply {
+ time = centerDate
+ set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
+ }
+
+ repeat(7) {
+ days.add(calendar.time)
+ calendar.add(Calendar.DAY_OF_MONTH, 1)
+ }
+ return days
+ }
+
+ fun getWeekStart(date: Date): Date {
+ val calendar = Calendar.getInstance().apply {
+ time = date
+ set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
+ set(Calendar.HOUR_OF_DAY, 0)
+ set(Calendar.MINUTE, 0)
+ set(Calendar.SECOND, 0)
+ set(Calendar.MILLISECOND, 0)
+ }
+ return calendar.time
+ }
+
+ fun getWeekEnd(date: Date): Date {
+ val calendar = Calendar.getInstance().apply {
+ time = getWeekStart(date)
+ add(Calendar.DAY_OF_YEAR, 6)
+ set(Calendar.HOUR_OF_DAY, 23)
+ set(Calendar.MINUTE, 59)
+ set(Calendar.SECOND, 59)
+ set(Calendar.MILLISECOND, 999)
+ }
+ return calendar.time
+ }
+
+ fun isSameDay(date1: Date, date2: Date): Boolean {
+ val cal1 = Calendar.getInstance().apply { time = date1 }
+ val cal2 = Calendar.getInstance().apply { time = date2 }
+ return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+ cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
+ }
+
+ fun isToday(date: Date): Boolean {
+ return isSameDay(date, Date())
+ }
+
+ fun formatDateHeader(date: Date): String {
+ val format = when {
+ isToday(date) -> SimpleDateFormat("'Today', MMM dd", Locale.getDefault())
+ isTomorrow(date) -> SimpleDateFormat("'Tomorrow', MMM dd", Locale.getDefault())
+ isYesterday(date) -> SimpleDateFormat("'Yesterday', MMM dd", Locale.getDefault())
+ else -> SimpleDateFormat("EEE, MMM dd", Locale.getDefault())
+ }
+ return format.format(date)
+ }
+
+ fun formatTime(date: Date): String {
+ val format = SimpleDateFormat("h:mm a", Locale.getDefault())
+ return format.format(date).lowercase()
+ }
+
+ private fun isTomorrow(date: Date): Boolean {
+ val tomorrow = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_YEAR, 1)
+ }.time
+ return isSameDay(date, tomorrow)
+ }
+
+ private fun isYesterday(date: Date): Boolean {
+ val yesterday = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_YEAR, -1)
+ }.time
+ return isSameDay(date, yesterday)
+ }
+
+ fun parseDateString(dateString: String): Date {
+ return try {
+ val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ format.timeZone = TimeZone.getTimeZone("UTC")
+ format.parse(dateString) ?: Date()
+ } catch (e: Exception) {
+ Date()
+ }
+ }
+
+ fun formatDateForApi(date: Date): String {
+ val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ format.timeZone = TimeZone.getTimeZone("UTC")
+ return format.format(date)
+ }
+}
+
+// Extension functions
+fun Date.isToday(): Boolean = CalendarUtils.isToday(this)
+fun Date.formatTime(): String = CalendarUtils.formatTime(this)
+fun Date.formatDateHeader(): String = CalendarUtils.formatDateHeader(this)
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/view/calendar/AddTaskCalendarActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/calendar/AddTaskCalendarActivity.kt
new file mode 100644
index 00000000..7b6a839e
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/view/calendar/AddTaskCalendarActivity.kt
@@ -0,0 +1,324 @@
+package deakin.gopher.guardian.view.calendar
+
+import deakin.gopher.guardian.model.calendar.UpdateTaskRequest
+
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.launch
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.adapter.WeekCalendarAdapter
+import deakin.gopher.guardian.adapter.TimeSlotAdapter
+import deakin.gopher.guardian.adapter.CalendarGridAdapter
+import deakin.gopher.guardian.model.calendar.CreateTaskRequest
+import deakin.gopher.guardian.model.calendar.TaskResponse
+import deakin.gopher.guardian.repository.TaskRepository
+import deakin.gopher.guardian.repository.ApiResult
+import deakin.gopher.guardian.util.CalendarUtils
+import java.text.SimpleDateFormat
+import java.util.*
+
+class AddTaskCalendarActivity : AppCompatActivity() {
+
+ // Calendar components
+ private lateinit var weekRecyclerView: RecyclerView
+ private lateinit var timeSlotRecyclerView: RecyclerView
+ private lateinit var calendarGridRecyclerView: RecyclerView
+ private lateinit var weekTitleText: TextView
+ private lateinit var backButton: ImageButton
+ private lateinit var saveButton: Button
+ private lateinit var todayButton: Button
+ private lateinit var previousWeekButton: ImageButton
+ private lateinit var nextWeekButton: ImageButton
+
+ private lateinit var weekAdapter: WeekCalendarAdapter
+ private lateinit var timeSlotAdapter: TimeSlotAdapter
+ private lateinit var calendarGridAdapter: CalendarGridAdapter
+
+ private var currentWeekStart = Calendar.getInstance()
+ private var selectedDate: Date? = null
+ private var selectedTime: String? = null
+
+ // Task form components
+ private lateinit var taskFormScroll: ScrollView
+ private lateinit var selectedTimeDisplay: TextView
+ private lateinit var taskTitleEditText: EditText
+ private lateinit var taskDescriptionEditText: EditText
+ private lateinit var priorityRadioGroup: RadioGroup
+ private lateinit var patientIdEditText: EditText
+
+ private lateinit var taskRepository: TaskRepository
+ private var isEditMode = false
+ private var editingTaskId: String? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add_task_calendar)
+
+ taskRepository = TaskRepository()
+
+ checkEditMode()
+ initViews()
+ setupRecyclerViews()
+ setupClickListeners()
+ loadCurrentWeek()
+ }
+
+ private fun checkEditMode() {
+ isEditMode = intent.getBooleanExtra("EDIT_MODE", false)
+ if (isEditMode) {
+ editingTaskId = intent.getStringExtra("TASK_ID")
+ // Pre-populate form with existing task data
+ populateFormForEdit()
+ }
+ }
+
+ private fun populateFormForEdit() {
+ intent.getStringExtra("TASK_TITLE")?.let { title ->
+ // We'll populate the form after views are initialized
+ }
+ }
+
+ private fun initViews() {
+ weekRecyclerView = findViewById(R.id.week_header_recycler_view)
+ timeSlotRecyclerView = findViewById(R.id.time_slot_recycler_view)
+ calendarGridRecyclerView = findViewById(R.id.calendar_grid_recycler_view)
+ weekTitleText = findViewById(R.id.week_title_text)
+ backButton = findViewById(R.id.back_button)
+ saveButton = findViewById(R.id.save_button)
+ todayButton = findViewById(R.id.today_button)
+ previousWeekButton = findViewById(R.id.previous_week_button)
+ nextWeekButton = findViewById(R.id.next_week_button)
+
+ taskFormScroll = findViewById(R.id.task_form_scroll)
+ selectedTimeDisplay = findViewById(R.id.selected_time_display)
+ taskTitleEditText = findViewById(R.id.task_title_edit_text)
+ taskDescriptionEditText = findViewById(R.id.task_description_edit_text)
+ priorityRadioGroup = findViewById(R.id.priority_radio_group)
+ patientIdEditText = findViewById(R.id.patient_id_edit_text)
+
+ // If in edit mode, populate the form
+ if (isEditMode) {
+ populateEditForm()
+ }
+ }
+
+ private fun populateEditForm() {
+ intent.getStringExtra("TASK_TITLE")?.let { taskTitleEditText.setText(it) }
+ intent.getStringExtra("TASK_DESCRIPTION")?.let { taskDescriptionEditText.setText(it) }
+ intent.getStringExtra("TASK_PRIORITY")?.let { priority ->
+ when (priority.lowercase()) {
+ "low" -> priorityRadioGroup.check(R.id.radio_priority_low)
+ "medium" -> priorityRadioGroup.check(R.id.radio_priority_medium)
+ "high" -> priorityRadioGroup.check(R.id.radio_priority_high)
+ }
+ }
+ }
+
+ private fun setupRecyclerViews() {
+ weekAdapter = WeekCalendarAdapter(getWeekDays()) { date -> }
+ weekRecyclerView.adapter = weekAdapter
+ weekRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+
+ timeSlotAdapter = TimeSlotAdapter(generateTimeSlots())
+ timeSlotRecyclerView.adapter = timeSlotAdapter
+ timeSlotRecyclerView.layoutManager = LinearLayoutManager(this)
+
+ calendarGridAdapter = CalendarGridAdapter(
+ weekDays = getWeekDays(),
+ timeSlots = generateTimeSlots(),
+ onSlotClick = { date, time -> onTimeSlotSelected(date, time) }
+ )
+ calendarGridRecyclerView.adapter = calendarGridAdapter
+ calendarGridRecyclerView.layoutManager = GridLayoutManager(this, 7)
+ }
+
+ private fun setupClickListeners() {
+ backButton.setOnClickListener { finish() }
+ previousWeekButton.setOnClickListener { navigateWeek(-1) }
+ nextWeekButton.setOnClickListener { navigateWeek(1) }
+ todayButton.setOnClickListener {
+ currentWeekStart = Calendar.getInstance()
+ loadCurrentWeek()
+ }
+ saveButton.setOnClickListener { saveTask() }
+ }
+
+ private fun getWeekDays(): List {
+ val days = mutableListOf()
+ val calendar = currentWeekStart.clone() as Calendar
+ calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
+ repeat(7) {
+ days.add(calendar.time)
+ calendar.add(Calendar.DAY_OF_MONTH, 1)
+ }
+ return days
+ }
+
+ private fun generateTimeSlots(): List {
+ val slots = mutableListOf()
+ for (hour in 6..22) {
+ slots.add(String.format("%02d:00", hour))
+ slots.add(String.format("%02d:30", hour))
+ }
+ return slots
+ }
+
+ private fun navigateWeek(direction: Int) {
+ currentWeekStart.add(Calendar.WEEK_OF_YEAR, direction)
+ loadCurrentWeek()
+ }
+
+ private fun loadCurrentWeek() {
+ weekAdapter.updateDays(getWeekDays())
+ calendarGridAdapter.updateWeekDays(getWeekDays())
+ updateWeekTitle()
+ }
+
+ private fun updateWeekTitle() {
+ val dateFormat = SimpleDateFormat("MMM dd", Locale.getDefault())
+ val weekStart = getWeekDays().first()
+ val weekEnd = getWeekDays().last()
+ weekTitleText.text = "${dateFormat.format(weekStart)} - ${dateFormat.format(weekEnd)}, ${SimpleDateFormat("yyyy", Locale.getDefault()).format(weekEnd)}"
+ }
+
+ private fun onTimeSlotSelected(date: Date, time: String) {
+ selectedDate = date
+ selectedTime = time
+
+ // Show task form
+ taskFormScroll.visibility = View.VISIBLE
+
+ // Update selected time display
+ val dateFormat = SimpleDateFormat("EEEE, MMM dd, yyyy", Locale.getDefault())
+ selectedTimeDisplay.text = "📅 ${dateFormat.format(date)} at $time"
+ }
+
+ private fun saveTask() {
+ if (!validateForm()) return
+
+ val sharedPrefs = getSharedPreferences("guardian_prefs", MODE_PRIVATE)
+ val token = sharedPrefs.getString("auth_token", "") ?: ""
+ val caretakerId = sharedPrefs.getString("user_id", "") ?: "68950c034af33273204ee625"
+
+ if (token.isEmpty()) {
+ showErrorDialog("Please log in to create tasks")
+ return
+ }
+
+ val title = taskTitleEditText.text.toString().trim()
+ val description = taskDescriptionEditText.text.toString().trim()
+ val patientId = patientIdEditText.text.toString().trim()
+ val priority = getSelectedPriority()
+
+ val dueDate = if (selectedDate != null && selectedTime != null) {
+ buildDueDateTime(selectedDate!!, selectedTime!!)
+ } else {
+ CalendarUtils.formatDateForApi(Date()) // Default to now if no selection
+ }
+
+ val taskRequest = CreateTaskRequest(
+ title = title,
+ description = if (description.isNotEmpty()) description else title,
+ priority = priority,
+ status = "pending",
+ dueDate = dueDate,
+ caretaker = caretakerId,
+ patientId = if (patientId.isNotEmpty()) patientId else null
+ )
+
+ // Show loading
+ saveButton.isEnabled = false
+ saveButton.text = "Saving..."
+
+ lifecycleScope.launch {
+ val result = if (isEditMode && editingTaskId != null) {
+ // Update existing task
+ taskRepository.updateTask(token, editingTaskId!!, taskRequest.toUpdateRequest())
+ } else {
+ // Create new task
+ taskRepository.createTask(token, taskRequest)
+ }
+
+ saveButton.isEnabled = true
+ saveButton.text = "Save"
+
+ when (result) {
+ is ApiResult.Success -> {
+ val message = if (isEditMode) "Task updated successfully!" else "Task created successfully!"
+ Toast.makeText(this@AddTaskCalendarActivity, "✅ $message", Toast.LENGTH_LONG).show()
+ setResult(RESULT_OK)
+ finish()
+ }
+ is ApiResult.Error -> {
+ showErrorDialog("Failed to save task: ${result.message}")
+ }
+ is ApiResult.Loading -> {
+ // Handled above
+ }
+ }
+ }
+ }
+
+ private fun validateForm(): Boolean {
+ if (taskTitleEditText.text.toString().trim().isEmpty()) {
+ taskTitleEditText.error = "Task title is required"
+ return false
+ }
+
+ if (selectedDate == null || selectedTime == null) {
+ Toast.makeText(this, "Please select a date and time", Toast.LENGTH_SHORT).show()
+ return false
+ }
+
+ return true
+ }
+
+ private fun getSelectedPriority(): String {
+ return when (priorityRadioGroup.checkedRadioButtonId) {
+ R.id.radio_priority_high -> "high"
+ R.id.radio_priority_low -> "low"
+ else -> "medium"
+ }
+ }
+
+ private fun buildDueDateTime(selectedDate: Date, selectedTime: String): String {
+ val calendar = Calendar.getInstance()
+ calendar.time = selectedDate // Set the date
+
+ // Parse and set the time
+ val timeParts = selectedTime.split(":")
+ calendar.set(Calendar.HOUR_OF_DAY, timeParts[0].toInt())
+ calendar.set(Calendar.MINUTE, timeParts[1].toInt())
+ calendar.set(Calendar.SECOND, 0)
+ calendar.set(Calendar.MILLISECOND, 0)
+
+ return CalendarUtils.formatDateForApi(calendar.time)
+ }
+
+ private fun showErrorDialog(message: String) {
+ AlertDialog.Builder(this)
+ .setTitle("Error")
+ .setMessage(message)
+ .setPositiveButton("OK", null)
+ .show()
+ }
+}
+
+// Extension function to convert CreateTaskRequest to UpdateTaskRequest
+private fun CreateTaskRequest.toUpdateRequest() = UpdateTaskRequest(
+ title = this.title,
+ description = this.description,
+ priority = this.priority,
+ status = this.status,
+ dueDate = this.dueDate,
+ patientId = this.patientId
+)
\ No newline at end of file
diff --git a/app/src/main/java/deakin/gopher/guardian/view/calendar/GuardianCalendarActivity.kt b/app/src/main/java/deakin/gopher/guardian/view/calendar/GuardianCalendarActivity.kt
new file mode 100644
index 00000000..09a8405e
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/view/calendar/GuardianCalendarActivity.kt
@@ -0,0 +1,183 @@
+package deakin.gopher.guardian.view.calendar
+
+
+import android.content.Intent
+import android.os.Bundle
+import android.widget.*
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import kotlinx.coroutines.launch
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.adapter.WeekDaysAdapter
+import deakin.gopher.guardian.adapter.TaskEventsAdapter
+import deakin.gopher.guardian.model.calendar.TaskResponse
+import deakin.gopher.guardian.repository.TaskRepository
+import deakin.gopher.guardian.viewmodel.CalendarViewModel
+
+
+import deakin.gopher.guardian.util.CalendarUtils
+import deakin.gopher.guardian.view.dialog.TaskDetailDialog
+import deakin.gopher.guardian.viewmodel.CalendarViewModelFactory
+import java.util.*
+
+class GuardianCalendarActivity : AppCompatActivity() {
+
+ private lateinit var viewModel: CalendarViewModel
+ private lateinit var taskRepository: TaskRepository
+ private lateinit var viewModelFactory: CalendarViewModelFactory
+
+ // UI components
+ private lateinit var headerText: TextView
+ private lateinit var timeText: TextView
+ private lateinit var weekRecyclerView: RecyclerView
+ private lateinit var eventsRecyclerView: RecyclerView
+ private lateinit var addTaskFab: FloatingActionButton
+ private lateinit var swipeRefreshLayout: SwipeRefreshLayout
+ private lateinit var prevWeekButton: ImageButton
+ private lateinit var nextWeekButton: ImageButton
+ private lateinit var backButton: ImageButton
+
+ private lateinit var weekAdapter: WeekDaysAdapter
+ private lateinit var eventsAdapter: TaskEventsAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_guardian_calendar)
+
+ setupDependencies()
+ initViews()
+ setupRecyclerViews()
+ setupObservers()
+ setupClickListeners()
+ }
+
+ private fun setupDependencies() {
+ taskRepository = TaskRepository()
+ viewModelFactory = CalendarViewModelFactory(taskRepository, this)
+ viewModel = ViewModelProvider(this, viewModelFactory)[CalendarViewModel::class.java]
+ }
+
+ private fun initViews() {
+ headerText = findViewById(R.id.header_text)
+ timeText = findViewById(R.id.time_text)
+ weekRecyclerView = findViewById(R.id.week_recycler_view)
+ eventsRecyclerView = findViewById(R.id.events_recycler_view)
+ addTaskFab = findViewById(R.id.add_task_fab)
+ swipeRefreshLayout = findViewById(R.id.swipe_refresh_layout)
+ prevWeekButton = findViewById(R.id.prev_week_button)
+ nextWeekButton = findViewById(R.id.next_week_button)
+ backButton = findViewById(R.id.back_button)
+ }
+
+ private fun setupRecyclerViews() {
+ weekAdapter = WeekDaysAdapter(emptyList()) { date ->
+ viewModel.selectDate(date)
+ }
+ weekRecyclerView.adapter = weekAdapter
+ weekRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
+
+ eventsAdapter = TaskEventsAdapter(
+ onTaskClick = { task ->
+ TaskDetailDialog.show(
+ context = this,
+ task = task,
+ onEdit = { editTask(it) },
+ onMarkComplete = { taskId -> viewModel.markTaskComplete(taskId) }
+ )
+ },
+ onTaskComplete = { taskId -> viewModel.markTaskComplete(taskId) },
+ onTaskEdit = { task -> editTask(task) }
+ )
+ eventsRecyclerView.adapter = eventsAdapter
+ eventsRecyclerView.layoutManager = LinearLayoutManager(this)
+ }
+
+ private fun setupObservers() {
+ lifecycleScope.launch {
+ viewModel.uiState.collect { state ->
+ swipeRefreshLayout.isRefreshing = state.isLoading
+
+ if (state.error != null) {
+ showErrorMessage(state.error)
+ }
+
+ eventsAdapter.updateTasks(state.filteredTasks)
+ }
+ }
+
+ lifecycleScope.launch {
+ viewModel.selectedDate.collect { calendar ->
+ updateHeaderText(calendar.time)
+ updateWeekDays(calendar.time)
+ }
+ }
+ }
+
+ private fun setupClickListeners() {
+ backButton.setOnClickListener {
+ finish()
+ }
+
+ prevWeekButton.setOnClickListener {
+ viewModel.navigateWeek(-1)
+ }
+
+ nextWeekButton.setOnClickListener {
+ viewModel.navigateWeek(1)
+ }
+
+ addTaskFab.setOnClickListener {
+ val intent = Intent(this, AddTaskCalendarActivity::class.java)
+ startActivityForResult(intent, REQUEST_ADD_TASK)
+ }
+
+ swipeRefreshLayout.setOnRefreshListener {
+ viewModel.refreshTasks()
+ }
+ }
+
+ private fun updateHeaderText(selectedDate: Date) {
+ val isToday = CalendarUtils.isToday(selectedDate)
+ headerText.text = if (isToday) "Today" else CalendarUtils.formatDateHeader(selectedDate)
+
+ timeText.text = CalendarUtils.formatTime(Date())
+ }
+
+ private fun updateWeekDays(selectedDate: Date) {
+ val weekDays = CalendarUtils.getWeekDays(selectedDate)
+ weekAdapter.updateDays(weekDays)
+ }
+
+ private fun editTask(task: TaskResponse) {
+ val intent = Intent(this, AddTaskCalendarActivity::class.java).apply {
+ putExtra("EDIT_MODE", true)
+ putExtra("TASK_ID", task.id)
+ putExtra("TASK_TITLE", task.title)
+ putExtra("TASK_DESCRIPTION", task.description)
+ putExtra("TASK_PRIORITY", task.priority)
+ putExtra("TASK_DUE_DATE", task.dueDate)
+ }
+ startActivityForResult(intent, REQUEST_EDIT_TASK)
+ }
+
+ private fun showErrorMessage(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_LONG).show()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if ((requestCode == REQUEST_ADD_TASK || requestCode == REQUEST_EDIT_TASK) && resultCode == RESULT_OK) {
+ viewModel.refreshTasks()
+ }
+ }
+
+ companion object {
+ private const val REQUEST_ADD_TASK = 1001
+ private const val REQUEST_EDIT_TASK = 1002
+ }
+}
diff --git a/app/src/main/java/deakin/gopher/guardian/view/dialog/LogEntryDialog.kt b/app/src/main/java/deakin/gopher/guardian/view/dialog/LogEntryDialog.kt
new file mode 100644
index 00000000..6f970c27
--- /dev/null
+++ b/app/src/main/java/deakin/gopher/guardian/view/dialog/LogEntryDialog.kt
@@ -0,0 +1,127 @@
+package deakin.gopher.guardian.view.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.Window
+import android.widget.*
+import androidx.appcompat.app.AlertDialog
+import deakin.gopher.guardian.R
+import deakin.gopher.guardian.model.logbook.LogEntry
+import java.text.SimpleDateFormat
+import java.util.*
+
+class LogEntryDialog {
+
+ companion object {
+ fun show(
+ context: Context,
+ patientId: String? = null,
+ existingLog: LogEntry? = null,
+ isViewOnly: Boolean = false,
+ onSave: ((String, String, String) -> Unit)? = null
+ ) {
+ val dialog = Dialog(context)
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ dialog.setCancelable(true)
+
+ val view = LayoutInflater.from(context).inflate(R.layout.dialog_log_entry, null)
+ dialog.setContentView(view)
+
+ setupDialog(view, dialog, patientId, existingLog, isViewOnly, onSave)
+
+ // Show dialog
+ dialog.show()
+
+ // Set dialog size
+ val window = dialog.window
+ window?.setLayout(
+ (context.resources.displayMetrics.widthPixels * 0.9).toInt(),
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ )
+ }
+
+ private fun setupDialog(
+ view: android.view.View,
+ dialog: Dialog,
+ patientId: String?,
+ existingLog: LogEntry?,
+ isViewOnly: Boolean,
+ onSave: ((String, String, String) -> Unit)?
+ ) {
+ val titleText = view.findViewById(R.id.dialog_title)
+ val titleEditText = view.findViewById(R.id.log_title_edit_text)
+ val descriptionEditText = view.findViewById(R.id.log_description_edit_text)
+ val patientIdEditText = view.findViewById(R.id.patient_id_edit_text)
+ val saveButton = view.findViewById