Add batch actions for multi-task completion (Phase 7)

Implement batch task completion feature allowing users to select and
complete multiple care tasks at once. Adds edit mode to Today View with
selection checkmarks, floating BatchActionBar, and confirmation dialog
for completing more than 3 tasks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-23 15:19:21 -06:00
parent efd935568a
commit f41c77876a
11 changed files with 403 additions and 7 deletions

View File

@@ -157,7 +157,7 @@ final class CreateCareScheduleUseCaseTests: XCTestCase {
// Then
let wateringTasks = result.tasks.filter { $0.type == .watering }
XCTAssertTrue(wateringTasks.allSatisfy { $0.notes?.contains("thorough") ?? false })
XCTAssertTrue(wateringTasks.allSatisfy { $0.notes.contains("thorough") })
}
func testExecute_WateringTasks_HaveCorrectPlantID() async throws {
@@ -242,7 +242,7 @@ final class CreateCareScheduleUseCaseTests: XCTestCase {
// Then
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
XCTAssertTrue(fertilizerTasks.allSatisfy { $0.notes?.contains("highNitrogen") ?? false })
XCTAssertTrue(fertilizerTasks.allSatisfy { $0.notes.contains("highNitrogen") })
}
// MARK: - User Preferences Tests

View File

@@ -26,6 +26,7 @@ final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @uncheck
var fetchAllTasksCallCount = 0
var updateTaskCallCount = 0
var deleteCallCount = 0
var batchCompleteCallCount = 0
// MARK: - Error Configuration
@@ -35,6 +36,7 @@ final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @uncheck
var shouldThrowOnFetchAllTasks = false
var shouldThrowOnUpdateTask = false
var shouldThrowOnDelete = false
var shouldThrowOnBatchComplete = false
var errorToThrow: Error = NSError(
domain: "MockError",
@@ -48,6 +50,7 @@ final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @uncheck
var lastFetchedPlantID: UUID?
var lastUpdatedTask: CareTask?
var lastDeletedPlantID: UUID?
var lastBatchCompletedTaskIDs: Set<UUID>?
// MARK: - CareScheduleRepositoryProtocol
@@ -111,6 +114,35 @@ final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @uncheck
schedules.removeValue(forKey: plantID)
}
func batchCompleteTasks(_ taskIDs: Set<UUID>) async throws {
batchCompleteCallCount += 1
lastBatchCompletedTaskIDs = taskIDs
if shouldThrowOnBatchComplete {
throw errorToThrow
}
let now = Date()
for (plantID, var schedule) in schedules {
var modified = false
for (index, task) in schedule.tasks.enumerated() {
if taskIDs.contains(task.id) && !task.isCompleted {
schedule.tasks[index] = CareTask(
id: task.id,
plantID: task.plantID,
type: task.type,
scheduledDate: task.scheduledDate,
completedDate: now,
notes: task.notes
)
modified = true
}
}
if modified {
schedules[plantID] = schedule
}
}
}
// MARK: - Helper Methods
/// Resets all state for clean test setup
@@ -123,6 +155,7 @@ final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @uncheck
fetchAllTasksCallCount = 0
updateTaskCallCount = 0
deleteCallCount = 0
batchCompleteCallCount = 0
shouldThrowOnSave = false
shouldThrowOnFetch = false
@@ -130,11 +163,13 @@ final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @uncheck
shouldThrowOnFetchAllTasks = false
shouldThrowOnUpdateTask = false
shouldThrowOnDelete = false
shouldThrowOnBatchComplete = false
lastSavedSchedule = nil
lastFetchedPlantID = nil
lastUpdatedTask = nil
lastDeletedPlantID = nil
lastBatchCompletedTaskIDs = nil
}
/// Adds a schedule directly to storage (bypasses save method)