wip
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
254
iosApp/iosApp/Task/AddTaskWithResidenceView.swift
Normal file
254
iosApp/iosApp/Task/AddTaskWithResidenceView.swift
Normal 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: [])
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user