Replace status_id with in_progress boolean across mobile apps
- Remove TaskStatus model and status_id foreign key references - Add in_progress boolean field to task models and forms - Update TaskApi to use dedicated POST endpoints for task actions: - POST /tasks/:id/cancel/ instead of PATCH with is_cancelled - POST /tasks/:id/uncancel/ - POST /tasks/:id/archive/ - POST /tasks/:id/unarchive/ - Fix iOS TaskViewModel to use error-first pattern for Kotlin-Swift generic type bridging issues - Update iOS callback signatures to pass full TaskResponse instead of just taskId to avoid stale closure lookups - Add in_progress localization strings - Update widget preview data to use inProgress boolean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -221,12 +221,9 @@ struct AllTasksView: View {
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { taskId in
|
||||
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
|
||||
if let task = allTasks.first(where: { $0.id == taskId }) {
|
||||
selectedTaskForCancel = task
|
||||
showCancelConfirmation = true
|
||||
}
|
||||
onCancelTask: { task in
|
||||
selectedTaskForCancel = task
|
||||
showCancelConfirmation = true
|
||||
},
|
||||
onUncancelTask: { taskId in
|
||||
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||
@@ -243,12 +240,9 @@ struct AllTasksView: View {
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { taskId in
|
||||
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
|
||||
if let task = allTasks.first(where: { $0.id == taskId }) {
|
||||
selectedTaskForArchive = task
|
||||
showArchiveConfirmation = true
|
||||
}
|
||||
onArchiveTask: { task in
|
||||
selectedTaskForArchive = task
|
||||
showArchiveConfirmation = true
|
||||
},
|
||||
onUnarchiveTask: { taskId in
|
||||
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||
|
||||
@@ -39,8 +39,8 @@ struct CompleteTaskView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
if let status = task.status {
|
||||
Text(status.displayName)
|
||||
if task.inProgress {
|
||||
Text(L10n.Tasks.inProgress)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
@@ -29,15 +29,13 @@ struct TaskFormView: View {
|
||||
(!needsResidenceSelection || selectedResidence != nil) &&
|
||||
selectedCategory != nil &&
|
||||
selectedFrequency != nil &&
|
||||
selectedPriority != nil &&
|
||||
selectedStatus != nil
|
||||
selectedPriority != nil
|
||||
}
|
||||
|
||||
// Lookups from DataManagerObservable
|
||||
private var taskCategories: [TaskCategory] { dataManager.taskCategories }
|
||||
private var taskFrequencies: [TaskFrequency] { dataManager.taskFrequencies }
|
||||
private var taskPriorities: [TaskPriority] { dataManager.taskPriorities }
|
||||
private var taskStatuses: [TaskStatus] { dataManager.taskStatuses }
|
||||
private var isLoadingLookups: Bool { !dataManager.lookupsInitialized }
|
||||
|
||||
// Form fields
|
||||
@@ -47,7 +45,7 @@ struct TaskFormView: View {
|
||||
@State private var selectedCategory: TaskCategory?
|
||||
@State private var selectedFrequency: TaskFrequency?
|
||||
@State private var selectedPriority: TaskPriority?
|
||||
@State private var selectedStatus: TaskStatus?
|
||||
@State private var inProgress: Bool
|
||||
@State private var dueDate: Date
|
||||
@State private var intervalDays: String
|
||||
@State private var estimatedCost: String
|
||||
@@ -66,7 +64,7 @@ struct TaskFormView: View {
|
||||
_selectedCategory = State(initialValue: task.category)
|
||||
_selectedFrequency = State(initialValue: task.frequency)
|
||||
_selectedPriority = State(initialValue: task.priority)
|
||||
_selectedStatus = State(initialValue: task.status)
|
||||
_inProgress = State(initialValue: task.inProgress)
|
||||
|
||||
// Parse date from string
|
||||
let formatter = DateFormatter()
|
||||
@@ -78,6 +76,7 @@ struct TaskFormView: View {
|
||||
} else {
|
||||
_title = State(initialValue: "")
|
||||
_description = State(initialValue: "")
|
||||
_inProgress = State(initialValue: false)
|
||||
_dueDate = State(initialValue: Date())
|
||||
_intervalDays = State(initialValue: "")
|
||||
_estimatedCost = State(initialValue: "")
|
||||
@@ -246,16 +245,11 @@ struct TaskFormView: View {
|
||||
}
|
||||
}
|
||||
|
||||
Picker(L10n.Tasks.status, selection: $selectedStatus) {
|
||||
Text(L10n.Tasks.selectStatus).tag(nil as TaskStatus?)
|
||||
ForEach(taskStatuses, id: \.id) { status in
|
||||
Text(status.displayName).tag(status as TaskStatus?)
|
||||
}
|
||||
}
|
||||
Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress)
|
||||
} header: {
|
||||
Text(L10n.Tasks.priorityAndStatus)
|
||||
} footer: {
|
||||
Text(L10n.Tasks.bothRequired)
|
||||
Text(L10n.Tasks.required)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
@@ -409,11 +403,6 @@ struct TaskFormView: View {
|
||||
selectedPriority = taskPriorities.first { $0.name == "medium" } ?? taskPriorities.first
|
||||
}
|
||||
|
||||
if selectedStatus == nil && !taskStatuses.isEmpty {
|
||||
// Default to "pending"
|
||||
selectedStatus = taskStatuses.first { $0.name == "pending" } ?? taskStatuses.first
|
||||
}
|
||||
|
||||
// Set default residence if provided
|
||||
if needsResidenceSelection && selectedResidence == nil, let residences = residences, !residences.isEmpty {
|
||||
selectedResidence = residences.first
|
||||
@@ -452,11 +441,6 @@ struct TaskFormView: View {
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if selectedStatus == nil {
|
||||
viewModel.errorMessage = "Please select a status"
|
||||
isValid = false
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
@@ -465,8 +449,7 @@ struct TaskFormView: View {
|
||||
|
||||
guard let category = selectedCategory,
|
||||
let frequency = selectedFrequency,
|
||||
let priority = selectedPriority,
|
||||
let status = selectedStatus else {
|
||||
let priority = selectedPriority else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -483,7 +466,7 @@ struct TaskFormView: View {
|
||||
description: description.isEmpty ? nil : description,
|
||||
categoryId: KotlinInt(int: Int32(category.id)),
|
||||
priorityId: KotlinInt(int: Int32(priority.id)),
|
||||
statusId: KotlinInt(int: Int32(status.id)),
|
||||
inProgress: inProgress,
|
||||
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
||||
assignedToId: nil,
|
||||
dueDate: dueDateString,
|
||||
@@ -514,7 +497,7 @@ struct TaskFormView: View {
|
||||
description: description.isEmpty ? nil : description,
|
||||
categoryId: KotlinInt(int: Int32(category.id)),
|
||||
priorityId: KotlinInt(int: Int32(priority.id)),
|
||||
statusId: selectedStatus.map { KotlinInt(int: Int32($0.id)) },
|
||||
inProgress: inProgress,
|
||||
frequencyId: KotlinInt(int: Int32(frequency.id)),
|
||||
assignedToId: nil,
|
||||
dueDate: dueDateString,
|
||||
|
||||
@@ -75,14 +75,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.createTask(request: request)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.create)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.create, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
self.actionState = .success(.create)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.create, error.localizedDescription)
|
||||
@@ -100,14 +99,16 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.cancelTask(taskId: id)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.cancel)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
// Check for error first, then treat non-error as success
|
||||
// This handles Kotlin-Swift generic type bridging issues
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.cancel, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
// Not an error = success (DataManager is updated by APILayer)
|
||||
self.actionState = .success(.cancel)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.cancel, error.localizedDescription)
|
||||
@@ -125,14 +126,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.uncancelTask(taskId: id)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.uncancel)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.uncancel, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
self.actionState = .success(.uncancel)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.uncancel, error.localizedDescription)
|
||||
@@ -150,14 +150,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.markInProgress(taskId: id)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.markInProgress)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.markInProgress, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
self.actionState = .success(.markInProgress)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.markInProgress, error.localizedDescription)
|
||||
@@ -175,14 +174,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.archiveTask(taskId: id)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.archive)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.archive, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
self.actionState = .success(.archive)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.archive, error.localizedDescription)
|
||||
@@ -200,14 +198,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.unarchiveTask(taskId: id)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.unarchive)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.unarchive, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
self.actionState = .success(.unarchive)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.unarchive, error.localizedDescription)
|
||||
@@ -225,14 +222,13 @@ class TaskViewModel: ObservableObject {
|
||||
do {
|
||||
let result = try await APILayer.shared.updateTask(id: id, request: request)
|
||||
|
||||
if result is ApiResultSuccess<TaskResponse> {
|
||||
self.actionState = .success(.update)
|
||||
// DataManager is updated by APILayer, view updates via observation
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
if let error = result as? ApiResultError {
|
||||
self.actionState = .error(.update, ErrorMessageParser.parse(error.message))
|
||||
self.errorMessage = ErrorMessageParser.parse(error.message)
|
||||
completion(false)
|
||||
} else {
|
||||
self.actionState = .success(.update)
|
||||
completion(true)
|
||||
}
|
||||
} catch {
|
||||
self.actionState = .error(.update, error.localizedDescription)
|
||||
@@ -406,7 +402,7 @@ class TaskViewModel: ObservableObject {
|
||||
tasksResponse = TaskColumnsResponse(
|
||||
columns: newColumns,
|
||||
daysThreshold: currentResponse.daysThreshold,
|
||||
residenceId: currentResponse.residenceId
|
||||
residenceId: currentResponse.residenceId, summary: nil
|
||||
)
|
||||
}
|
||||
|
||||
@@ -434,7 +430,7 @@ class TaskViewModel: ObservableObject {
|
||||
tasksResponse = TaskColumnsResponse(
|
||||
columns: newColumns,
|
||||
daysThreshold: currentResponse.daysThreshold,
|
||||
residenceId: currentResponse.residenceId
|
||||
residenceId: currentResponse.residenceId, summary: nil
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user