This commit is contained in:
Trey t
2025-11-07 12:21:48 -06:00
parent 66fe773398
commit 1b777049a8
27 changed files with 2003 additions and 718 deletions

View File

@@ -12,8 +12,6 @@ struct ResidenceDetailView: View {
@State private var showEditResidence = false
@State private var showEditTask = false
@State private var selectedTaskForEdit: TaskDetail?
@State private var showInProgressTasks = false
@State private var showDoneTasks = false
@State private var selectedTaskForComplete: TaskDetail?
var body: some View {
@@ -39,8 +37,6 @@ struct ResidenceDetailView: View {
if let tasksResponse = tasksResponse {
TasksSection(
tasksResponse: tasksResponse,
showInProgressTasks: $showInProgressTasks,
showDoneTasks: $showDoneTasks,
onEditTask: { task in
selectedTaskForEdit = task
showEditTask = true

View File

@@ -145,12 +145,13 @@ struct TaskCard: View {
description: "Remove all debris from gutters",
category: TaskCategory(id: 1, name: "maintenance", description: ""),
priority: TaskPriority(id: 2, name: "medium", displayName: "", description: ""),
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "30"),
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "30", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 1, name: "pending", displayName: "", 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,

View File

@@ -3,8 +3,6 @@ import ComposeApp
struct TasksSection: View {
let tasksResponse: TasksByResidenceResponse
@Binding var showInProgressTasks: Bool
@Binding var showDoneTasks: Bool
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (TaskDetail) -> Void
let onUncancelTask: (TaskDetail) -> Void
@@ -13,104 +11,82 @@ struct TasksSection: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Tasks")
.font(.title2)
.fontWeight(.bold)
Text("Tasks")
.font(.title2)
.fontWeight(.bold)
Spacer()
HStack(spacing: 8) {
TaskPill(count: Int32(tasksResponse.summary.upcoming), label: "Upcoming", color: .blue)
TaskPill(count: Int32(tasksResponse.summary.inProgress), label: "In Progress", color: .orange)
TaskPill(count: Int32(tasksResponse.summary.done), label: "Done", color: .green)
}
}
if tasksResponse.upcomingTasks.isEmpty && tasksResponse.inProgressTasks.isEmpty && tasksResponse.doneTasks.isEmpty {
if tasksResponse.upcomingTasks.isEmpty && tasksResponse.inProgressTasks.isEmpty && tasksResponse.doneTasks.isEmpty && tasksResponse.archivedTasks.isEmpty {
EmptyTasksView()
} else {
// Upcoming tasks
ForEach(tasksResponse.upcomingTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: nil,
onMarkInProgress: { onMarkInProgress(task) },
onComplete: { onCompleteTask(task) }
)
}
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
)
.frame(width: geometry.size.width - 48)
// In Progress tasks section
if !tasksResponse.inProgressTasks.isEmpty {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("In Progress (\(tasksResponse.inProgressTasks.count))", systemImage: "play.circle")
.font(.headline)
.foregroundColor(.orange)
// 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
)
.frame(width: geometry.size.width - 48)
Spacer()
// 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
)
.frame(width: geometry.size.width - 48)
Image(systemName: showInProgressTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
.font(.caption)
}
.padding(.top, 8)
.contentShape(Rectangle())
.onTapGesture {
showInProgressTasks.toggle()
}
if showInProgressTasks {
ForEach(tasksResponse.inProgressTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: nil,
onMarkInProgress: nil,
onComplete: { onCompleteTask(task) }
)
}
}
}
}
// Done tasks section
if !tasksResponse.doneTasks.isEmpty {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Done (\(tasksResponse.doneTasks.count))", systemImage: "checkmark.circle")
.font(.headline)
.foregroundColor(.green)
Spacer()
Image(systemName: showDoneTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
.font(.caption)
}
.padding(.top, 8)
.contentShape(Rectangle())
.onTapGesture {
showDoneTasks.toggle()
}
if showDoneTasks {
ForEach(tasksResponse.doneTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: nil,
onUncancel: nil,
onMarkInProgress: nil,
onComplete: nil
)
}
// 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
)
.frame(width: geometry.size.width - 48)
}
.scrollTargetLayout()
.padding(.horizontal, 16)
}
.scrollTargetBehavior(.viewAligned)
}
.frame(height: 500)
}
}
}
@@ -134,12 +110,13 @@ struct TasksSection: View {
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"),
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,
@@ -156,22 +133,22 @@ struct TasksSection: View {
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"),
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: "185.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: []
),
showInProgressTasks: .constant(true),
showDoneTasks: .constant(true),
onEditTask: { _ in },
onCancelTask: { _ in },
onUncancelTask: { _ in },

View File

@@ -214,7 +214,6 @@ struct AddTaskView: View {
frequency: Int32(frequency.id),
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
priority: Int32(priority.id),
status: Int32(status.id),
dueDate: dueDateString,
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
)

View File

@@ -0,0 +1,254 @@
import SwiftUI
import ComposeApp
struct AddTaskWithResidenceView: View {
@Binding var isPresented: Bool
let residences: [Residence]
@StateObject private var viewModel = TaskViewModel()
@StateObject private var lookupsManager = LookupsManager.shared
@FocusState private var focusedField: Field?
// Form fields
@State private var selectedResidence: Residence?
@State private var title: String = ""
@State private var description: String = ""
@State private var selectedCategory: TaskCategory?
@State private var selectedFrequency: TaskFrequency?
@State private var selectedPriority: TaskPriority?
@State private var selectedStatus: TaskStatus?
@State private var dueDate: Date = Date()
@State private var intervalDays: String = ""
@State private var estimatedCost: String = ""
// Validation errors
@State private var titleError: String = ""
@State private var residenceError: String = ""
enum Field {
case title, description, intervalDays, estimatedCost
}
var body: some View {
NavigationView {
if lookupsManager.isLoading {
VStack(spacing: 16) {
ProgressView()
Text("Loading...")
.foregroundColor(.secondary)
}
} else {
Form {
Section(header: Text("Property")) {
Picker("Property", selection: $selectedResidence) {
Text("Select Property").tag(nil as Residence?)
ForEach(residences, id: \.id) { residence in
Text(residence.name).tag(residence as Residence?)
}
}
if !residenceError.isEmpty {
Text(residenceError)
.font(.caption)
.foregroundColor(.red)
}
}
Section(header: Text("Task Details")) {
TextField("Title", text: $title)
.focused($focusedField, equals: .title)
if !titleError.isEmpty {
Text(titleError)
.font(.caption)
.foregroundColor(.red)
}
TextField("Description (optional)", text: $description, axis: .vertical)
.lineLimit(3...6)
.focused($focusedField, equals: .description)
}
Section(header: Text("Category")) {
Picker("Category", selection: $selectedCategory) {
Text("Select Category").tag(nil as TaskCategory?)
ForEach(lookupsManager.taskCategories, id: \.id) { category in
Text(category.name.capitalized).tag(category as TaskCategory?)
}
}
}
Section(header: Text("Scheduling")) {
Picker("Frequency", selection: $selectedFrequency) {
Text("Select Frequency").tag(nil as TaskFrequency?)
ForEach(lookupsManager.taskFrequencies, id: \.id) { frequency in
Text(frequency.displayName).tag(frequency as TaskFrequency?)
}
}
if selectedFrequency?.name != "once" {
TextField("Custom Interval (days, optional)", text: $intervalDays)
.keyboardType(.numberPad)
.focused($focusedField, equals: .intervalDays)
}
DatePicker("Due Date", selection: $dueDate, displayedComponents: .date)
}
Section(header: Text("Priority & Status")) {
Picker("Priority", selection: $selectedPriority) {
Text("Select Priority").tag(nil as TaskPriority?)
ForEach(lookupsManager.taskPriorities, id: \.id) { priority in
Text(priority.displayName).tag(priority as TaskPriority?)
}
}
Picker("Status", selection: $selectedStatus) {
Text("Select Status").tag(nil as TaskStatus?)
ForEach(lookupsManager.taskStatuses, id: \.id) { status in
Text(status.displayName).tag(status as TaskStatus?)
}
}
}
Section(header: Text("Cost")) {
TextField("Estimated Cost (optional)", text: $estimatedCost)
.keyboardType(.decimalPad)
.focused($focusedField, equals: .estimatedCost)
}
if let errorMessage = viewModel.errorMessage {
Section {
Text(errorMessage)
.foregroundColor(.red)
.font(.caption)
}
}
}
.navigationTitle("Add Task")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
submitForm()
}
.disabled(viewModel.isLoading)
}
}
.onAppear {
setDefaults()
}
.onChange(of: viewModel.taskCreated) { created in
if created {
isPresented = false
}
}
}
}
}
private func setDefaults() {
if selectedResidence == nil && !residences.isEmpty {
selectedResidence = residences.first
}
if selectedCategory == nil && !lookupsManager.taskCategories.isEmpty {
selectedCategory = lookupsManager.taskCategories.first
}
if selectedFrequency == nil && !lookupsManager.taskFrequencies.isEmpty {
selectedFrequency = lookupsManager.taskFrequencies.first { $0.name == "once" } ?? lookupsManager.taskFrequencies.first
}
if selectedPriority == nil && !lookupsManager.taskPriorities.isEmpty {
selectedPriority = lookupsManager.taskPriorities.first { $0.name == "medium" } ?? lookupsManager.taskPriorities.first
}
if selectedStatus == nil && !lookupsManager.taskStatuses.isEmpty {
selectedStatus = lookupsManager.taskStatuses.first { $0.name == "pending" } ?? lookupsManager.taskStatuses.first
}
}
private func validateForm() -> Bool {
var isValid = true
if selectedResidence == nil {
residenceError = "Property is required"
isValid = false
} else {
residenceError = ""
}
if title.isEmpty {
titleError = "Title is required"
isValid = false
} else {
titleError = ""
}
if selectedCategory == nil {
viewModel.errorMessage = "Please select a category"
isValid = false
}
if selectedFrequency == nil {
viewModel.errorMessage = "Please select a frequency"
isValid = false
}
if selectedPriority == nil {
viewModel.errorMessage = "Please select a priority"
isValid = false
}
if selectedStatus == nil {
viewModel.errorMessage = "Please select a status"
isValid = false
}
return isValid
}
private func submitForm() {
guard validateForm() else { return }
guard let residence = selectedResidence,
let category = selectedCategory,
let frequency = selectedFrequency,
let priority = selectedPriority,
let status = selectedStatus else {
return
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dueDateString = dateFormatter.string(from: dueDate)
let request = TaskCreateRequest(
residence: Int32(residence.id),
title: title,
description: description.isEmpty ? nil : description,
category: Int32(category.id),
frequency: Int32(frequency.id),
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
priority: Int32(priority.id),
dueDate: dueDateString,
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
)
viewModel.createTask(request: request) { success in
if success {
// View will dismiss automatically via onChange
}
}
}
}
#Preview {
AddTaskWithResidenceView(isPresented: .constant(true), residences: [])
}

View File

@@ -3,21 +3,32 @@ import ComposeApp
struct AllTasksView: View {
@StateObject private var taskViewModel = TaskViewModel()
@StateObject private var residenceViewModel = ResidenceViewModel()
@State private var tasksResponse: AllTasksResponse?
@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 showInProgressTasks = false
@State private var showDoneTasks = false
@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
}
private var hasTasks: Bool {
!hasNoTasks
}
var body: some View {
ZStack {
Color(.systemGroupedBackground)
.ignoresSafeArea()
if isLoadingTasks {
ProgressView()
} else if let error = tasksError {
@@ -25,68 +36,179 @@ struct AllTasksView: View {
loadAllTasks()
}
} else if let tasksResponse = tasksResponse {
ScrollView {
VStack(spacing: 16) {
// Header Card
VStack(spacing: 12) {
Image(systemName: "checklist")
.font(.system(size: 48))
.foregroundStyle(.blue.gradient)
Text("All Tasks")
.font(.title)
.fontWeight(.bold)
Text("Tasks across all your properties")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
.padding(.horizontal)
.padding(.top)
// Tasks Section
AllTasksSectionView(
tasksResponse: tasksResponse,
showInProgressTasks: $showInProgressTasks,
showDoneTasks: $showDoneTasks,
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 {
loadAllTasks()
}
}
},
onCompleteTask: { task in
selectedTaskForComplete = task
if hasNoTasks {
// 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
}) {
HStack(spacing: 8) {
Image(systemName: "plus")
Text("Add Task")
.fontWeight(.semibold)
}
)
.padding(.horizontal)
.frame(maxWidth: .infinity)
.frame(height: 50)
}
.buttonStyle(.borderedProminent)
.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()
} 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: { 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 {
loadAllTasks()
}
}
},
onCompleteTask: { task in
selectedTaskForComplete = task
}
)
.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
}
)
.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
)
.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
)
.frame(width: geometry.size.width - 48)
}
.scrollTargetLayout()
.padding(.horizontal, 16)
}
.scrollTargetBehavior(.viewAligned)
}
.padding(.bottom)
}
}
}
.ignoresSafeArea(edges: .bottom)
.navigationTitle("All Tasks")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAddTask = true
}) {
Image(systemName: "plus")
}
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
}
}
.sheet(isPresented: $showAddTask) {
AddTaskWithResidenceView(
isPresented: $showAddTask,
residences: residenceViewModel.myResidences?.residences.toResidences() ?? [],
)
}
.sheet(isPresented: $showEditTask) {
if let task = selectedTaskForEdit {
EditTaskView(task: task, isPresented: $showEditTask)
@@ -98,6 +220,11 @@ struct AllTasksView: View {
loadAllTasks()
}
}
.onChange(of: showAddTask) { isShowing in
if !isShowing {
loadAllTasks()
}
}
.onChange(of: showEditTask) { isShowing in
if !isShowing {
loadAllTasks()
@@ -105,155 +232,118 @@ struct AllTasksView: View {
}
.onAppear {
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<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
struct TaskColumnView: View {
let title: String
let icon: String
let color: Color
let count: Int
let tasks: [TaskDetail]
let onEditTask: (TaskDetail) -> Void
let onCancelTask: ((TaskDetail) -> Void)?
let onUncancelTask: ((TaskDetail) -> Void)?
let onMarkInProgress: ((TaskDetail) -> Void)?
let onCompleteTask: ((TaskDetail) -> Void)?
var body: some View {
VStack(spacing: 0) {
// Tasks List
ScrollView {
VStack(spacing: 16) {
// Header
HStack(spacing: 8) {
Image(systemName: icon)
.font(.headline)
.foregroundColor(color)
Text(title)
.font(.headline)
.foregroundColor(color)
Spacer()
Text("\(count)")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(color)
.cornerRadius(12)
}
if tasks.isEmpty {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.system(size: 40))
.foregroundColor(color.opacity(0.3))
Text("No tasks")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
} else {
ForEach(tasks, id: \.id) { task in
TaskCard(
task: task,
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
)
}
}
}
}
}
}
}
struct AllTasksSectionView: View {
let tasksResponse: AllTasksResponse
@Binding var showInProgressTasks: Bool
@Binding var showDoneTasks: Bool
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (TaskDetail) -> Void
let onUncancelTask: (TaskDetail) -> Void
let onMarkInProgress: (TaskDetail) -> Void
let onCompleteTask: (TaskDetail) -> Void
// Extension to apply corner radius to specific corners
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Task summary pills
HStack(spacing: 8) {
TaskPill(
count: Int32(tasksResponse.summary.upcoming),
label: "Upcoming",
color: .blue
)
TaskPill(
count: Int32(tasksResponse.summary.inProgress),
label: "In Progress",
color: .orange
)
TaskPill(
count: Int32(tasksResponse.summary.done),
label: "Done",
color: .green
)
}
.padding(.bottom, 4)
// Upcoming tasks
if !tasksResponse.upcomingTasks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Label("Upcoming (\(tasksResponse.upcomingTasks.count))", systemImage: "calendar")
.font(.headline)
.foregroundColor(.blue)
ForEach(tasksResponse.upcomingTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: { onUncancelTask(task) },
onMarkInProgress: { onMarkInProgress(task) },
onComplete: { onCompleteTask(task) }
)
}
}
}
// In Progress section (collapsible)
if !tasksResponse.inProgressTasks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack {
Label("In Progress (\(tasksResponse.inProgressTasks.count))", systemImage: "play.circle")
.font(.headline)
.foregroundColor(.orange)
Spacer()
Image(systemName: showInProgressTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
showInProgressTasks.toggle()
}
}
if showInProgressTasks {
ForEach(tasksResponse.inProgressTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: { onUncancelTask(task) },
onMarkInProgress: nil,
onComplete: { onCompleteTask(task) }
)
}
}
}
}
// Done section (collapsible)
if !tasksResponse.doneTasks.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack {
Label("Done (\(tasksResponse.doneTasks.count))", systemImage: "checkmark.circle")
.font(.headline)
.foregroundColor(.green)
Spacer()
Image(systemName: showDoneTasks ? "chevron.up" : "chevron.down")
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
showDoneTasks.toggle()
}
}
if showDoneTasks {
ForEach(tasksResponse.doneTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: nil,
onUncancel: nil,
onMarkInProgress: nil,
onComplete: nil
)
}
}
}
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
@@ -262,3 +352,37 @@ struct AllTasksSectionView: View {
AllTasksView()
}
}
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.
func toResidences() -> [Residence] {
return self.map { item in
return Residence(
id: item.id,
owner: KotlinInt(value: item.owner),
ownerUsername: item.ownerUsername,
name: item.name,
propertyType: item.propertyType,
streetAddress: item.streetAddress,
apartmentUnit: item.apartmentUnit,
city: item.city,
stateProvince: item.stateProvince,
postalCode: item.postalCode,
country: item.country,
bedrooms: item.bedrooms != nil ? KotlinInt(nonretainedObject: item.bedrooms!) : nil,
bathrooms: item.bathrooms != nil ? KotlinFloat(float: Float(item.bathrooms!)) : nil,
squareFootage: item.squareFootage != nil ? KotlinInt(nonretainedObject: item.squareFootage!) : nil,
lotSize: item.lotSize != nil ? KotlinFloat(float: Float(item.lotSize!)) : nil,
yearBuilt: item.yearBuilt != nil ? KotlinInt(nonretainedObject: item.yearBuilt!) : nil,
description: item.description,
purchaseDate: item.purchaseDate,
purchasePrice: item.purchasePrice,
isPrimary: item.isPrimary,
createdAt: item.createdAt,
updatedAt: item.updatedAt
)
}
}
}

View File

@@ -149,7 +149,6 @@ struct EditTaskView: View {
frequency: frequency.id,
intervalDays: nil,
priority: priority.id,
status: status.id,
dueDate: dueDate,
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
)