Files
PlantGuide/PlantGuide/Presentation/Scenes/CareSchedule/CareScheduleViewModel.swift
treyt 60189a5406 fix: issue #17 - upcoming tasks
Automated fix by Tony CI v3.
Refs #17

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-09 22:23:56 -05:00

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"
}
}