187 lines
6.1 KiB
Swift
187 lines
6.1 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - CareScheduleViewModel
|
|
|
|
/// ViewModel for the Care Schedule screen that manages care tasks and filtering
|
|
@MainActor
|
|
@Observable
|
|
final class CareScheduleViewModel {
|
|
// MARK: - Dependencies
|
|
|
|
private let careScheduleRepository: CareScheduleRepositoryProtocol
|
|
private let plantRepository: PlantRepositoryProtocol
|
|
|
|
// MARK: - Properties
|
|
|
|
/// All care tasks loaded from the data source
|
|
private(set) var allTasks: [CareTask] = []
|
|
|
|
/// Mapping of plant IDs to their corresponding Plant objects
|
|
private(set) var plants: [UUID: Plant] = [:]
|
|
|
|
/// The currently selected filter for displaying tasks
|
|
var selectedFilter: TaskFilter = .all
|
|
|
|
/// Indicates whether tasks are currently being loaded
|
|
private(set) var isLoading = false
|
|
|
|
// MARK: - Task Filter
|
|
|
|
/// Available filters for care tasks
|
|
enum TaskFilter: String, CaseIterable {
|
|
case all = "All"
|
|
case watering = "Watering"
|
|
case fertilizing = "Fertilizing"
|
|
case overdue = "Overdue"
|
|
case today = "Today"
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(
|
|
careScheduleRepository: CareScheduleRepositoryProtocol,
|
|
plantRepository: PlantRepositoryProtocol
|
|
) {
|
|
self.careScheduleRepository = careScheduleRepository
|
|
self.plantRepository = plantRepository
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
/// Tasks filtered based on the selected filter
|
|
var filteredTasks: [CareTask] {
|
|
switch selectedFilter {
|
|
case .all:
|
|
return allTasks.filter { !$0.isCompleted }
|
|
case .watering:
|
|
return allTasks.filter { $0.type == .watering && !$0.isCompleted }
|
|
case .fertilizing:
|
|
return allTasks.filter { $0.type == .fertilizing && !$0.isCompleted }
|
|
case .overdue:
|
|
return overdueTasks
|
|
case .today:
|
|
return todayTasks
|
|
}
|
|
}
|
|
|
|
/// Tasks that are overdue (past scheduled date and not completed)
|
|
var overdueTasks: [CareTask] {
|
|
allTasks.filter { $0.isOverdue }
|
|
.sorted { $0.scheduledDate < $1.scheduledDate }
|
|
}
|
|
|
|
/// Tasks scheduled for today
|
|
var todayTasks: [CareTask] {
|
|
let calendar = Calendar.current
|
|
let today = calendar.startOfDay(for: Date())
|
|
|
|
return allTasks.filter { task in
|
|
guard !task.isCompleted else { return false }
|
|
let taskDay = calendar.startOfDay(for: task.scheduledDate)
|
|
return calendar.isDate(taskDay, inSameDayAs: today)
|
|
}
|
|
.sorted { $0.scheduledDate < $1.scheduledDate }
|
|
}
|
|
|
|
/// Upcoming tasks grouped by date (excluding overdue and today's tasks)
|
|
var upcomingTasksByDate: [Date: [CareTask]] {
|
|
let calendar = Calendar.current
|
|
let today = calendar.startOfDay(for: Date())
|
|
|
|
let upcomingTasks = allTasks.filter { task in
|
|
guard !task.isCompleted else { return false }
|
|
let taskDay = calendar.startOfDay(for: task.scheduledDate)
|
|
return taskDay > today
|
|
}
|
|
|
|
var grouped: [Date: [CareTask]] = [:]
|
|
for task in upcomingTasks {
|
|
let dayStart = calendar.startOfDay(for: task.scheduledDate)
|
|
grouped[dayStart, default: []].append(task)
|
|
}
|
|
|
|
// Sort tasks within each date group
|
|
for (date, tasks) in grouped {
|
|
grouped[date] = tasks.sorted { $0.scheduledDate < $1.scheduledDate }
|
|
}
|
|
|
|
return grouped
|
|
}
|
|
|
|
/// Sorted array of upcoming dates for section headers
|
|
var sortedUpcomingDates: [Date] {
|
|
upcomingTasksByDate.keys.sorted()
|
|
}
|
|
|
|
// MARK: - Methods
|
|
|
|
/// Loads all care tasks from the data source
|
|
func loadTasks() async {
|
|
isLoading = true
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
// Load all tasks from the repository
|
|
allTasks = try await careScheduleRepository.fetchAllTasks()
|
|
|
|
// Load all plants for name lookup
|
|
let plantList = try await plantRepository.fetchAll()
|
|
plants = Dictionary(uniqueKeysWithValues: plantList.map { ($0.id, $0) })
|
|
} catch {
|
|
// Log error but don't crash - just show empty state
|
|
print("Failed to load tasks: \(error)")
|
|
allTasks = []
|
|
plants = [:]
|
|
}
|
|
}
|
|
|
|
/// Marks a care task as complete
|
|
/// - Parameter task: The task to mark as complete
|
|
func markComplete(_ task: CareTask) async {
|
|
guard let index = allTasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
|
|
// Update the task with completion date
|
|
let completedTask = task.completed()
|
|
allTasks[index] = completedTask
|
|
|
|
// Persist the change to the repository
|
|
do {
|
|
try await careScheduleRepository.updateTask(completedTask)
|
|
} catch {
|
|
// Revert in-memory state so UI stays consistent with Core Data
|
|
allTasks[index] = task
|
|
}
|
|
}
|
|
|
|
/// Snoozes a task by the specified number of hours
|
|
/// - Parameters:
|
|
/// - task: The task to snooze
|
|
/// - hours: Number of hours to snooze the task
|
|
func snoozeTask(_ task: CareTask, hours: Int) async {
|
|
guard let index = allTasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
|
|
// Create a new task with the updated scheduled date
|
|
let newScheduledDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date()
|
|
let snoozedTask = task.rescheduled(to: newScheduledDate)
|
|
allTasks[index] = snoozedTask
|
|
|
|
// Persist the change to the repository
|
|
do {
|
|
try await careScheduleRepository.updateTask(snoozedTask)
|
|
} catch {
|
|
// Log error - in a production app, you might want to show an alert
|
|
print("Failed to persist snoozed task: \(error)")
|
|
}
|
|
}
|
|
|
|
/// Returns the plant name for a given task
|
|
/// - Parameter task: The care task
|
|
/// - Returns: The plant name or a default string if not found
|
|
func plantName(for task: CareTask) -> String {
|
|
if let plant = plants[task.plantID] {
|
|
return plant.commonNames.first ?? plant.scientificName
|
|
}
|
|
return "Unknown Plant"
|
|
}
|
|
}
|