wip
This commit is contained in:
@@ -5,7 +5,7 @@ struct ResidenceDetailView: View {
|
||||
let residenceId: Int32
|
||||
@StateObject private var viewModel = ResidenceViewModel()
|
||||
@StateObject private var taskViewModel = TaskViewModel()
|
||||
@State private var tasksResponse: TasksByResidenceResponse?
|
||||
@State private var tasksResponse: TaskColumnsResponse?
|
||||
@State private var isLoadingTasks = false
|
||||
@State private var tasksError: String?
|
||||
@State private var showAddTask = false
|
||||
@@ -41,18 +41,18 @@ struct ResidenceDetailView: View {
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { task in
|
||||
taskViewModel.cancelTask(id: task.id) { _ in
|
||||
onCancelTask: { taskId in
|
||||
taskViewModel.cancelTask(id: taskId) { _ in
|
||||
loadResidenceTasks()
|
||||
}
|
||||
},
|
||||
onUncancelTask: { task in
|
||||
taskViewModel.uncancelTask(id: task.id) { _ in
|
||||
onUncancelTask: { taskId in
|
||||
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||
loadResidenceTasks()
|
||||
}
|
||||
},
|
||||
onMarkInProgress: { task in
|
||||
taskViewModel.markInProgress(id: task.id) { success in
|
||||
onMarkInProgress: { taskId in
|
||||
taskViewModel.markInProgress(id: taskId) { success in
|
||||
if success {
|
||||
loadResidenceTasks()
|
||||
}
|
||||
@@ -61,13 +61,13 @@ struct ResidenceDetailView: View {
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { task in
|
||||
taskViewModel.archiveTask(id: task.id) { _ in
|
||||
onArchiveTask: { taskId in
|
||||
taskViewModel.archiveTask(id: taskId) { _ in
|
||||
loadResidenceTasks()
|
||||
}
|
||||
},
|
||||
onUnarchiveTask: { task in
|
||||
taskViewModel.unarchiveTask(id: task.id) { _ in
|
||||
onUnarchiveTask: { taskId in
|
||||
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||
loadResidenceTasks()
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,7 @@ struct ResidenceDetailView: View {
|
||||
|
||||
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||
taskApi.getTasksByResidence(token: token, residenceId: residenceId, days: 30) { result, error in
|
||||
if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> {
|
||||
if let successResult = result as? ApiResultSuccess<TaskColumnsResponse> {
|
||||
self.tasksResponse = successResult.data
|
||||
self.isLoadingTasks = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
|
||||
185
iosApp/iosApp/Subviews/Task/TaskActionButtons.swift
Normal file
185
iosApp/iosApp/Subviews/Task/TaskActionButtons.swift
Normal file
@@ -0,0 +1,185 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
// MARK: - Edit Task Button
|
||||
struct EditTaskButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
// Edit navigates to edit screen - handled by parent
|
||||
onCompletion()
|
||||
}) {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cancel Task Button
|
||||
struct CancelTaskButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
viewModel.cancelTask(id: taskId) { success in
|
||||
if success {
|
||||
onCompletion()
|
||||
} else {
|
||||
onError("Failed to cancel task")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Label("Cancel", systemImage: "xmark.circle")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Uncancel (Restore) Task Button
|
||||
struct UncancelTaskButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
viewModel.uncancelTask(id: taskId) { success in
|
||||
if success {
|
||||
onCompletion()
|
||||
} else {
|
||||
onError("Failed to restore task")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Label("Restore", systemImage: "arrow.uturn.backward")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mark In Progress Button
|
||||
struct MarkInProgressButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
viewModel.markInProgress(id: taskId) { success in
|
||||
if success {
|
||||
onCompletion()
|
||||
} else {
|
||||
onError("Failed to mark task in progress")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
Text("In Progress")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Complete Task Button
|
||||
struct CompleteTaskButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
// Complete shows dialog - handled by parent
|
||||
onCompletion()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 18, height: 18)
|
||||
Text("Complete")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Archive Task Button
|
||||
struct ArchiveTaskButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
viewModel.archiveTask(id: taskId) { success in
|
||||
if success {
|
||||
onCompletion()
|
||||
} else {
|
||||
onError("Failed to archive task")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Label("Archive", systemImage: "archivebox")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Unarchive Task Button
|
||||
struct UnarchiveTaskButton: View {
|
||||
let taskId: Int32
|
||||
let onCompletion: () -> Void
|
||||
let onError: (String) -> Void
|
||||
|
||||
@StateObject private var viewModel = TaskViewModel()
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
viewModel.unarchiveTask(id: taskId) { success in
|
||||
if success {
|
||||
onCompletion()
|
||||
} else {
|
||||
onError("Failed to unarchive task")
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Label("Unarchive", systemImage: "tray.and.arrow.up")
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.blue)
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,18 @@ import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct TasksSection: View {
|
||||
let tasksResponse: TasksByResidenceResponse
|
||||
let tasksResponse: TaskColumnsResponse
|
||||
let onEditTask: (TaskDetail) -> Void
|
||||
let onCancelTask: (TaskDetail) -> Void
|
||||
let onUncancelTask: (TaskDetail) -> Void
|
||||
let onMarkInProgress: (TaskDetail) -> Void
|
||||
let onCancelTask: (Int32) -> Void
|
||||
let onUncancelTask: (Int32) -> Void
|
||||
let onMarkInProgress: (Int32) -> Void
|
||||
let onCompleteTask: (TaskDetail) -> Void
|
||||
let onArchiveTask: (TaskDetail) -> Void
|
||||
let onUnarchiveTask: (TaskDetail) -> Void
|
||||
let onArchiveTask: (Int32) -> Void
|
||||
let onUnarchiveTask: (Int32) -> Void
|
||||
|
||||
private var hasNoTasks: Bool {
|
||||
tasksResponse.columns.allSatisfy { $0.tasks.isEmpty }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -17,79 +21,40 @@ struct TasksSection: View {
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
|
||||
if tasksResponse.upcomingTasks.isEmpty && tasksResponse.inProgressTasks.isEmpty && tasksResponse.doneTasks.isEmpty && tasksResponse.archivedTasks.isEmpty {
|
||||
if hasNoTasks {
|
||||
EmptyTasksView()
|
||||
} else {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
// Upcoming Column
|
||||
TaskColumnView(
|
||||
title: "Upcoming",
|
||||
icon: "calendar",
|
||||
color: .blue,
|
||||
count: tasksResponse.upcomingTasks.count,
|
||||
tasks: tasksResponse.upcomingTasks,
|
||||
onEditTask: onEditTask,
|
||||
onCancelTask: onCancelTask,
|
||||
onUncancelTask: onUncancelTask,
|
||||
onMarkInProgress: onMarkInProgress,
|
||||
onCompleteTask: onCompleteTask,
|
||||
onArchiveTask: onArchiveTask,
|
||||
onUnarchiveTask: onUnarchiveTask
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
|
||||
// In Progress Column
|
||||
TaskColumnView(
|
||||
title: "In Progress",
|
||||
icon: "play.circle",
|
||||
color: .orange,
|
||||
count: tasksResponse.inProgressTasks.count,
|
||||
tasks: tasksResponse.inProgressTasks,
|
||||
onEditTask: onEditTask,
|
||||
onCancelTask: onCancelTask,
|
||||
onUncancelTask: onUncancelTask,
|
||||
onMarkInProgress: nil,
|
||||
onCompleteTask: onCompleteTask,
|
||||
onArchiveTask: onArchiveTask,
|
||||
onUnarchiveTask: onUnarchiveTask
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
|
||||
// Done Column
|
||||
TaskColumnView(
|
||||
title: "Done",
|
||||
icon: "checkmark.circle",
|
||||
color: .green,
|
||||
count: tasksResponse.doneTasks.count,
|
||||
tasks: tasksResponse.doneTasks,
|
||||
onEditTask: onEditTask,
|
||||
onCancelTask: nil,
|
||||
onUncancelTask: nil,
|
||||
onMarkInProgress: nil,
|
||||
onCompleteTask: nil,
|
||||
onArchiveTask: onArchiveTask,
|
||||
onUnarchiveTask: onUnarchiveTask
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
|
||||
// Archived Column
|
||||
TaskColumnView(
|
||||
title: "Archived",
|
||||
icon: "archivebox",
|
||||
color: .gray,
|
||||
count: tasksResponse.archivedTasks.count,
|
||||
tasks: tasksResponse.archivedTasks,
|
||||
onEditTask: onEditTask,
|
||||
onCancelTask: nil,
|
||||
onUncancelTask: nil,
|
||||
onMarkInProgress: nil,
|
||||
onCompleteTask: nil,
|
||||
onArchiveTask: nil,
|
||||
onUnarchiveTask: onUnarchiveTask
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
// Dynamically create columns from response
|
||||
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
|
||||
DynamicTaskColumnView(
|
||||
column: column,
|
||||
onEditTask: { task in
|
||||
onEditTask(task)
|
||||
},
|
||||
onCancelTask: { taskId in
|
||||
onCancelTask(taskId)
|
||||
},
|
||||
onUncancelTask: { taskId in
|
||||
onUncancelTask(taskId)
|
||||
},
|
||||
onMarkInProgress: { taskId in
|
||||
onMarkInProgress(taskId)
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
onCompleteTask(task)
|
||||
},
|
||||
onArchiveTask: { taskId in
|
||||
onArchiveTask(taskId)
|
||||
},
|
||||
onUnarchiveTask: { taskId in
|
||||
onUnarchiveTask(taskId)
|
||||
}
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.padding(.horizontal, 16)
|
||||
@@ -104,69 +69,79 @@ struct TasksSection: View {
|
||||
|
||||
#Preview {
|
||||
TasksSection(
|
||||
tasksResponse: TasksByResidenceResponse(
|
||||
residenceId: "1",
|
||||
tasksResponse: TaskColumnsResponse(
|
||||
columns: [
|
||||
TaskColumn(
|
||||
name: "upcoming_tasks",
|
||||
displayName: "Upcoming",
|
||||
buttonTypes: ["edit", "cancel", "uncancel", "mark_in_progress", "complete", "archive"],
|
||||
icons: ["ios": "calendar", "android": "CalendarToday", "web": "calendar"],
|
||||
color: "#007AFF",
|
||||
tasks: [
|
||||
TaskDetail(
|
||||
id: 1,
|
||||
residence: 1,
|
||||
title: "Clean Gutters",
|
||||
description: "Remove all debris",
|
||||
category: TaskCategory(id: 1, name: "maintenance", description: ""),
|
||||
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", description: ""),
|
||||
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "Monthly", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: ""),
|
||||
dueDate: "2024-12-15",
|
||||
estimatedCost: "150.00",
|
||||
actualCost: nil,
|
||||
notes: nil,
|
||||
archived: false,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: true,
|
||||
completions: []
|
||||
)
|
||||
],
|
||||
count: 1
|
||||
),
|
||||
TaskColumn(
|
||||
name: "done_tasks",
|
||||
displayName: "Done",
|
||||
buttonTypes: ["edit", "archive"],
|
||||
icons: ["ios": "checkmark.circle", "android": "CheckCircle", "web": "check-circle"],
|
||||
color: "#34C759",
|
||||
tasks: [
|
||||
TaskDetail(
|
||||
id: 2,
|
||||
residence: 1,
|
||||
title: "Fix Leaky Faucet",
|
||||
description: "Kitchen sink fixed",
|
||||
category: TaskCategory(id: 2, name: "plumbing", description: ""),
|
||||
priority: TaskPriority(id: 3, name: "high", displayName: "High", description: ""),
|
||||
frequency: TaskFrequency(id: 6, name: "once", displayName: "One Time", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", description: ""),
|
||||
dueDate: "2024-11-01",
|
||||
estimatedCost: "200.00",
|
||||
actualCost: nil,
|
||||
notes: nil,
|
||||
archived: false,
|
||||
createdAt: "2024-10-01T00:00:00Z",
|
||||
updatedAt: "2024-11-05T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: false,
|
||||
completions: []
|
||||
)
|
||||
],
|
||||
count: 1
|
||||
)
|
||||
],
|
||||
daysThreshold: 30,
|
||||
summary: CategorizedTaskSummary(
|
||||
upcoming: 3,
|
||||
inProgress: 1,
|
||||
done: 2,
|
||||
archived: 0
|
||||
),
|
||||
upcomingTasks: [
|
||||
TaskDetail(
|
||||
id: 1,
|
||||
residence: 1,
|
||||
title: "Clean Gutters",
|
||||
description: "Remove all debris",
|
||||
category: TaskCategory(id: 1, name: "maintenance", description: "General upkeep tasks"),
|
||||
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", description: "Standard priority"),
|
||||
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "Monthly", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: "Awaiting completion"),
|
||||
dueDate: "2024-12-15",
|
||||
estimatedCost: "150.00",
|
||||
actualCost: nil,
|
||||
notes: nil,
|
||||
archived: false,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: true,
|
||||
completions: []
|
||||
)
|
||||
],
|
||||
inProgressTasks: [],
|
||||
doneTasks: [
|
||||
TaskDetail(
|
||||
id: 2,
|
||||
residence: 1,
|
||||
title: "Fix Leaky Faucet",
|
||||
description: "Kitchen sink fixed",
|
||||
category: TaskCategory(id: 2, name: "plumbing", description: "Plumbing tasks"),
|
||||
priority: TaskPriority(id: 3, name: "high", displayName: "High", description: "High priority"),
|
||||
frequency: TaskFrequency(id: 6, name: "once", displayName: "One Time", daySpan: 0, notifyDays: 0),
|
||||
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", description: "Task completed"),
|
||||
dueDate: "2024-11-01",
|
||||
estimatedCost: "200.00",
|
||||
actualCost: nil,
|
||||
notes: nil,
|
||||
archived: false,
|
||||
createdAt: "2024-10-01T00:00:00Z",
|
||||
updatedAt: "2024-11-05T00:00:00Z",
|
||||
nextScheduledDate: nil,
|
||||
showCompletedButton: false,
|
||||
completions: []
|
||||
)
|
||||
],
|
||||
archivedTasks: []
|
||||
residenceId: "1"
|
||||
),
|
||||
onEditTask: { _ in },
|
||||
onCancelTask: { _ in },
|
||||
onUncancelTask: { _ in },
|
||||
onMarkInProgress: { _ in },
|
||||
onCompleteTask: { _ in }
|
||||
, onArchiveTask: { _ in }
|
||||
, onUnarchiveTask: { _ in }
|
||||
onCompleteTask: { _ in },
|
||||
onArchiveTask: { _ in },
|
||||
onUnarchiveTask: { _ in }
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ struct AddTaskView: View {
|
||||
frequency: Int32(frequency.id),
|
||||
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
||||
priority: Int32(priority.id),
|
||||
status: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||
dueDate: dueDateString,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
||||
)
|
||||
|
||||
@@ -237,6 +237,7 @@ struct AddTaskWithResidenceView: View {
|
||||
frequency: Int32(frequency.id),
|
||||
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
||||
priority: Int32(priority.id),
|
||||
status: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||
dueDate: dueDateString,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
||||
)
|
||||
|
||||
@@ -4,31 +4,28 @@ import ComposeApp
|
||||
struct AllTasksView: View {
|
||||
@StateObject private var taskViewModel = TaskViewModel()
|
||||
@StateObject private var residenceViewModel = ResidenceViewModel()
|
||||
@State private var tasksResponse: AllTasksResponse?
|
||||
@State private var tasksResponse: TaskColumnsResponse?
|
||||
@State private var isLoadingTasks = false
|
||||
@State private var tasksError: String?
|
||||
@State private var showAddTask = false
|
||||
@State private var showEditTask = false
|
||||
@State private var selectedTaskForEdit: TaskDetail?
|
||||
@State private var selectedTaskForComplete: TaskDetail?
|
||||
|
||||
|
||||
private var hasNoTasks: Bool {
|
||||
guard let response = tasksResponse else { return true }
|
||||
return response.upcomingTasks.isEmpty &&
|
||||
response.inProgressTasks.isEmpty &&
|
||||
response.doneTasks.isEmpty &&
|
||||
response.archivedTasks.isEmpty
|
||||
return response.columns.allSatisfy { $0.tasks.isEmpty }
|
||||
}
|
||||
|
||||
|
||||
private var hasTasks: Bool {
|
||||
!hasNoTasks
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(.systemGroupedBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
|
||||
if isLoadingTasks {
|
||||
ProgressView()
|
||||
} else if let error = tasksError {
|
||||
@@ -40,20 +37,20 @@ struct AllTasksView: View {
|
||||
// Empty state with big button
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
|
||||
|
||||
Image(systemName: "checklist")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(.blue.opacity(0.6))
|
||||
|
||||
|
||||
Text("No tasks yet")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
|
||||
Text("Create your first task to get started")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
|
||||
Button(action: {
|
||||
showAddTask = true
|
||||
}) {
|
||||
@@ -69,13 +66,13 @@ struct AllTasksView: View {
|
||||
.controlSize(.large)
|
||||
.padding(.horizontal, 48)
|
||||
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
|
||||
|
||||
|
||||
if residenceViewModel.myResidences?.residences.isEmpty ?? true {
|
||||
Text("Add a property first from the Residences tab")
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
@@ -83,128 +80,47 @@ struct AllTasksView: View {
|
||||
GeometryReader { geometry in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
// Upcoming Column
|
||||
TaskColumnView(
|
||||
title: "Upcoming",
|
||||
icon: "calendar",
|
||||
color: .blue,
|
||||
count: tasksResponse.upcomingTasks.count,
|
||||
tasks: tasksResponse.upcomingTasks,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { task in
|
||||
taskViewModel.cancelTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUncancelTask: { task in
|
||||
taskViewModel.uncancelTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onMarkInProgress: { task in
|
||||
taskViewModel.markInProgress(id: task.id) { success in
|
||||
if success {
|
||||
// Dynamically create columns from response
|
||||
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
|
||||
DynamicTaskColumnView(
|
||||
column: column,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { taskId in
|
||||
taskViewModel.cancelTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUncancelTask: { taskId in
|
||||
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onMarkInProgress: { taskId in
|
||||
taskViewModel.markInProgress(id: taskId) { success in
|
||||
if success {
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { taskId in
|
||||
taskViewModel.archiveTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUnarchiveTask: { taskId in
|
||||
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { task in
|
||||
taskViewModel.archiveTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUnarchiveTask: nil
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
|
||||
|
||||
// In Progress Column
|
||||
TaskColumnView(
|
||||
title: "In Progress",
|
||||
icon: "play.circle",
|
||||
color: .orange,
|
||||
count: tasksResponse.inProgressTasks.count,
|
||||
tasks: tasksResponse.inProgressTasks,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: { task in
|
||||
taskViewModel.cancelTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUncancelTask: { task in
|
||||
taskViewModel.uncancelTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onMarkInProgress: nil,
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
},
|
||||
onArchiveTask: { task in
|
||||
taskViewModel.archiveTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUnarchiveTask: nil
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
|
||||
// Done Column
|
||||
TaskColumnView(
|
||||
title: "Done",
|
||||
icon: "checkmark.circle",
|
||||
color: .green,
|
||||
count: tasksResponse.doneTasks.count,
|
||||
tasks: tasksResponse.doneTasks,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: nil,
|
||||
onUncancelTask: nil,
|
||||
onMarkInProgress: nil,
|
||||
onCompleteTask: nil,
|
||||
onArchiveTask: { task in
|
||||
taskViewModel.archiveTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
},
|
||||
onUnarchiveTask: nil
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
|
||||
// Archived Column
|
||||
TaskColumnView(
|
||||
title: "Archived",
|
||||
icon: "archivebox",
|
||||
color: .gray,
|
||||
count: tasksResponse.archivedTasks.count,
|
||||
tasks: tasksResponse.archivedTasks,
|
||||
onEditTask: { task in
|
||||
selectedTaskForEdit = task
|
||||
showEditTask = true
|
||||
},
|
||||
onCancelTask: nil,
|
||||
onUncancelTask: nil,
|
||||
onMarkInProgress: nil,
|
||||
onCompleteTask: nil,
|
||||
onArchiveTask: nil,
|
||||
onUnarchiveTask: { task in
|
||||
taskViewModel.unarchiveTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
)
|
||||
.frame(width: geometry.size.width - 48)
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
.padding(.horizontal, 16)
|
||||
@@ -258,93 +174,100 @@ struct AllTasksView: View {
|
||||
loadAllTasks()
|
||||
residenceViewModel.loadMyResidences()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAllTasks() {
|
||||
guard let token = TokenStorage.shared.getToken() else { return }
|
||||
|
||||
isLoadingTasks = true
|
||||
tasksError = nil
|
||||
|
||||
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||
taskApi.getTasks(token: token, days: 30) { result, error in
|
||||
if let successResult = result as? ApiResultSuccess<AllTasksResponse> {
|
||||
self.tasksResponse = successResult.data
|
||||
self.isLoadingTasks = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.tasksError = errorResult.message
|
||||
self.isLoadingTasks = false
|
||||
} else if let error = error {
|
||||
self.tasksError = error.localizedDescription
|
||||
self.isLoadingTasks = false
|
||||
private func loadAllTasks() {
|
||||
guard let token = TokenStorage.shared.getToken() else { return }
|
||||
|
||||
isLoadingTasks = true
|
||||
tasksError = nil
|
||||
|
||||
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||
taskApi.getTasks(token: token, days: 30) { result, error in
|
||||
if let successResult = result as? ApiResultSuccess<TaskColumnsResponse> {
|
||||
self.tasksResponse = successResult.data
|
||||
self.isLoadingTasks = false
|
||||
} else if let errorResult = result as? ApiResultError {
|
||||
self.tasksError = errorResult.message
|
||||
self.isLoadingTasks = false
|
||||
} else if let error = error {
|
||||
self.tasksError = error.localizedDescription
|
||||
self.isLoadingTasks = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TaskColumnView: View {
|
||||
let title: String
|
||||
let icon: String
|
||||
let color: Color
|
||||
let count: Int
|
||||
let tasks: [TaskDetail]
|
||||
/// Dynamic task column view that adapts based on the column configuration
|
||||
struct DynamicTaskColumnView: View {
|
||||
let column: TaskColumn
|
||||
let onEditTask: (TaskDetail) -> Void
|
||||
let onCancelTask: ((TaskDetail) -> Void)?
|
||||
let onUncancelTask: ((TaskDetail) -> Void)?
|
||||
let onMarkInProgress: ((TaskDetail) -> Void)?
|
||||
let onCompleteTask: ((TaskDetail) -> Void)?
|
||||
let onArchiveTask: ((TaskDetail) -> Void)?
|
||||
let onUnarchiveTask: ((TaskDetail) -> Void)?
|
||||
let onCancelTask: (Int32) -> Void
|
||||
let onUncancelTask: (Int32) -> Void
|
||||
let onMarkInProgress: (Int32) -> Void
|
||||
let onCompleteTask: (TaskDetail) -> Void
|
||||
let onArchiveTask: (Int32) -> Void
|
||||
let onUnarchiveTask: (Int32) -> Void
|
||||
|
||||
// Get icon from API response, with fallback
|
||||
private var columnIcon: String {
|
||||
column.icons["ios"] ?? "list.bullet"
|
||||
}
|
||||
|
||||
private var columnColor: Color {
|
||||
Color(hex: column.color) ?? .primary
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Tasks List
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
// Header
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
Image(systemName: columnIcon)
|
||||
.font(.headline)
|
||||
.foregroundColor(color)
|
||||
|
||||
Text(title)
|
||||
.foregroundColor(columnColor)
|
||||
|
||||
Text(column.displayName)
|
||||
.font(.headline)
|
||||
.foregroundColor(color)
|
||||
|
||||
.foregroundColor(columnColor)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(count)")
|
||||
|
||||
Text("\(column.count)")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(color)
|
||||
.background(columnColor)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
if tasks.isEmpty {
|
||||
|
||||
if column.tasks.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
Image(systemName: columnIcon)
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(color.opacity(0.3))
|
||||
|
||||
.foregroundColor(columnColor.opacity(0.3))
|
||||
|
||||
Text("No tasks")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
ForEach(tasks, id: \.id) { task in
|
||||
TaskCard(
|
||||
ForEach(column.tasks, id: \.id) { task in
|
||||
DynamicTaskCard(
|
||||
task: task,
|
||||
buttonTypes: column.buttonTypes,
|
||||
onEdit: { onEditTask(task) },
|
||||
onCancel: onCancelTask != nil ? { onCancelTask?(task) } : nil,
|
||||
onUncancel: onUncancelTask != nil ? { onUncancelTask?(task) } : nil,
|
||||
onMarkInProgress: onMarkInProgress != nil ? { onMarkInProgress?(task) } : nil,
|
||||
onComplete: onCompleteTask != nil ? { onCompleteTask?(task) } : nil,
|
||||
onArchive: onArchiveTask != nil ? { onArchiveTask?(task) } : nil,
|
||||
onUnarchive: onUnarchiveTask != nil ? { onUnarchiveTask?(task) } : nil
|
||||
onCancel: { onCancelTask(task.id) },
|
||||
onUncancel: { onUncancelTask(task.id) },
|
||||
onMarkInProgress: { onMarkInProgress(task.id) },
|
||||
onComplete: { onCompleteTask(task) },
|
||||
onArchive: { onArchiveTask(task.id) },
|
||||
onUnarchive: { onUnarchiveTask(task.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -354,6 +277,155 @@ struct TaskColumnView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Task card that dynamically renders buttons based on the column's button types
|
||||
struct DynamicTaskCard: View {
|
||||
let task: TaskDetail
|
||||
let buttonTypes: [String]
|
||||
let onEdit: () -> Void
|
||||
let onCancel: () -> Void
|
||||
let onUncancel: () -> Void
|
||||
let onMarkInProgress: () -> Void
|
||||
let onComplete: () -> Void
|
||||
let onArchive: () -> Void
|
||||
let onUnarchive: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(task.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
if let status = task.status {
|
||||
StatusBadge(status: status.name)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
PriorityBadge(priority: task.priority.name)
|
||||
}
|
||||
|
||||
if let description = task.description_, !description.isEmpty {
|
||||
Text(description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Label(task.frequency.displayName, systemImage: "repeat")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Label(formatDate(task.dueDate), systemImage: "calendar")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
if task.completions.count > 0 {
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(.green)
|
||||
Text("Completed \(task.completions.count) time\(task.completions.count == 1 ? "" : "s")")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// Render buttons based on buttonTypes array
|
||||
VStack(spacing: 8) {
|
||||
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
|
||||
renderButton(for: buttonType)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
|
||||
}
|
||||
|
||||
private func formatDate(_ dateString: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
if let date = formatter.date(from: dateString) {
|
||||
formatter.dateStyle = .medium
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
return dateString
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func renderButton(for buttonType: String) -> some View {
|
||||
switch buttonType {
|
||||
case "mark_in_progress":
|
||||
MarkInProgressButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onMarkInProgress,
|
||||
onError: { error in
|
||||
print("Error marking in progress: \(error)")
|
||||
}
|
||||
)
|
||||
case "complete":
|
||||
CompleteTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onComplete,
|
||||
onError: { error in
|
||||
print("Error completing task: \(error)")
|
||||
}
|
||||
)
|
||||
case "edit":
|
||||
EditTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onEdit,
|
||||
onError: { error in
|
||||
print("Error editing task: \(error)")
|
||||
}
|
||||
)
|
||||
case "cancel":
|
||||
CancelTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onCancel,
|
||||
onError: { error in
|
||||
print("Error cancelling task: \(error)")
|
||||
}
|
||||
)
|
||||
case "uncancel":
|
||||
UncancelTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onUncancel,
|
||||
onError: { error in
|
||||
print("Error restoring task: \(error)")
|
||||
}
|
||||
)
|
||||
case "archive":
|
||||
ArchiveTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onArchive,
|
||||
onError: { error in
|
||||
print("Error archiving task: \(error)")
|
||||
}
|
||||
)
|
||||
case "unarchive":
|
||||
UnarchiveTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onUnarchive,
|
||||
onError: { error in
|
||||
print("Error unarchiving task: \(error)")
|
||||
}
|
||||
)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to apply corner radius to specific corners
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
@@ -364,7 +436,7 @@ extension View {
|
||||
struct RoundedCorner: Shape {
|
||||
var radius: CGFloat = .infinity
|
||||
var corners: UIRectCorner = .allCorners
|
||||
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(
|
||||
roundedRect: rect,
|
||||
@@ -381,7 +453,6 @@ struct RoundedCorner: Shape {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Array where Element == ResidenceWithTasks {
|
||||
/// Converts an array of ResidenceWithTasks into an array of Residence.
|
||||
/// Adjust the mapping inside as needed to match your model initializers.
|
||||
@@ -414,3 +485,31 @@ extension Array where Element == ResidenceWithTasks {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
/// Initialize Color from hex string (e.g., "#007AFF" or "007AFF")
|
||||
init?(hex: String) {
|
||||
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var int: UInt64 = 0
|
||||
Scanner(string: hex).scanHexInt64(&int)
|
||||
let a, r, g, b: UInt64
|
||||
switch hex.count {
|
||||
case 3: // RGB (12-bit)
|
||||
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||
case 6: // RGB (24-bit)
|
||||
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||
case 8: // ARGB (32-bit)
|
||||
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(
|
||||
.sRGB,
|
||||
red: Double(r) / 255,
|
||||
green: Double(g) / 255,
|
||||
blue: Double(b) / 255,
|
||||
opacity: Double(a) / 255
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ struct EditTaskView: View {
|
||||
frequency: frequency.id,
|
||||
intervalDays: nil,
|
||||
priority: priority.id,
|
||||
status: KotlinInt(value: status.id),
|
||||
dueDate: dueDate,
|
||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user