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