Move tasksResponse state and updateTaskInKanban logic from individual views into TaskViewModel. Both AllTasksView and ResidenceDetailView now delegate to the shared ViewModel, reducing code duplication and ensuring consistent task state management across the app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
13 KiB
Swift
308 lines
13 KiB
Swift
import SwiftUI
|
|
import ComposeApp
|
|
|
|
struct AllTasksView: View {
|
|
@StateObject private var taskViewModel = TaskViewModel()
|
|
@StateObject private var residenceViewModel = ResidenceViewModel()
|
|
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
|
@State private var showAddTask = false
|
|
@State private var showEditTask = false
|
|
@State private var showingUpgradePrompt = false
|
|
@State private var selectedTaskForEdit: TaskResponse?
|
|
@State private var selectedTaskForComplete: TaskResponse?
|
|
|
|
@State private var selectedTaskForArchive: TaskResponse?
|
|
@State private var showArchiveConfirmation = false
|
|
|
|
@State private var selectedTaskForCancel: TaskResponse?
|
|
@State private var showCancelConfirmation = false
|
|
|
|
// Use ViewModel's computed properties
|
|
private var totalTaskCount: Int { taskViewModel.totalTaskCount }
|
|
private var hasNoTasks: Bool { taskViewModel.hasNoTasks }
|
|
private var hasTasks: Bool { taskViewModel.hasTasks }
|
|
private var tasksResponse: TaskColumnsResponse? { taskViewModel.tasksResponse }
|
|
private var isLoadingTasks: Bool { taskViewModel.isLoadingTasks }
|
|
private var tasksError: String? { taskViewModel.tasksError }
|
|
|
|
var body: some View {
|
|
mainContent
|
|
.sheet(isPresented: $showAddTask) {
|
|
AddTaskWithResidenceView(
|
|
isPresented: $showAddTask,
|
|
residences: residenceViewModel.myResidences?.residences.toResidences() ?? []
|
|
)
|
|
}
|
|
.sheet(isPresented: $showEditTask) {
|
|
if let task = selectedTaskForEdit {
|
|
EditTaskView(task: task, isPresented: $showEditTask)
|
|
}
|
|
}
|
|
.sheet(item: $selectedTaskForComplete) { task in
|
|
CompleteTaskView(task: task) { updatedTask in
|
|
if let updatedTask = updatedTask {
|
|
updateTaskInKanban(updatedTask)
|
|
}
|
|
selectedTaskForComplete = nil
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingUpgradePrompt) {
|
|
UpgradePromptView(triggerKey: "add_11th_task", isPresented: $showingUpgradePrompt)
|
|
}
|
|
.alert(L10n.Tasks.archiveTask, isPresented: $showArchiveConfirmation) {
|
|
Button(L10n.Common.cancel, role: .cancel) {
|
|
selectedTaskForArchive = nil
|
|
}
|
|
Button(L10n.Tasks.archive, role: .destructive) {
|
|
if let task = selectedTaskForArchive {
|
|
taskViewModel.archiveTask(id: task.id) { _ in
|
|
loadAllTasks()
|
|
}
|
|
selectedTaskForArchive = nil
|
|
}
|
|
}
|
|
} message: {
|
|
if let task = selectedTaskForArchive {
|
|
Text(L10n.Tasks.archiveConfirm.replacingOccurrences(of: "this task", with: "\"\(task.title)\""))
|
|
}
|
|
}
|
|
.alert(L10n.Tasks.deleteTask, isPresented: $showCancelConfirmation) {
|
|
Button(L10n.Common.cancel, role: .cancel) {
|
|
selectedTaskForCancel = nil
|
|
}
|
|
Button(L10n.Tasks.archive, role: .destructive) {
|
|
if let task = selectedTaskForCancel {
|
|
taskViewModel.cancelTask(id: task.id) { _ in
|
|
loadAllTasks()
|
|
}
|
|
selectedTaskForCancel = nil
|
|
}
|
|
}
|
|
} message: {
|
|
if let task = selectedTaskForCancel {
|
|
Text(L10n.Tasks.archiveConfirm.replacingOccurrences(of: "this task", with: "\"\(task.title)\""))
|
|
}
|
|
}
|
|
.onChange(of: showAddTask) { isShowing in
|
|
if !isShowing {
|
|
loadAllTasks()
|
|
}
|
|
}
|
|
.onChange(of: showEditTask) { isShowing in
|
|
if !isShowing {
|
|
loadAllTasks()
|
|
}
|
|
}
|
|
.onAppear {
|
|
loadAllTasks()
|
|
residenceViewModel.loadMyResidences()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var mainContent: some View {
|
|
ZStack {
|
|
Color.appBackgroundPrimary
|
|
.ignoresSafeArea()
|
|
|
|
if hasNoTasks && isLoadingTasks {
|
|
ProgressView()
|
|
} else if let error = tasksError {
|
|
ErrorView(message: error) {
|
|
loadAllTasks()
|
|
}
|
|
} else if let tasksResponse = tasksResponse {
|
|
if hasNoTasks {
|
|
// Empty state with big button
|
|
VStack(spacing: 24) {
|
|
Spacer()
|
|
|
|
Image(systemName: "checklist")
|
|
.font(.system(size: 64))
|
|
.foregroundStyle(Color.appPrimary.opacity(0.6))
|
|
|
|
Text(L10n.Tasks.noTasksYet)
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
|
|
Text(L10n.Tasks.createFirst)
|
|
.font(.body)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Button(action: {
|
|
// Check if we should show upgrade prompt before adding
|
|
if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTaskCount, limitKey: "tasks") {
|
|
showingUpgradePrompt = true
|
|
} else {
|
|
showAddTask = true
|
|
}
|
|
}) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "plus")
|
|
Text(L10n.Tasks.addButton)
|
|
.fontWeight(.semibold)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 50)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.large)
|
|
.padding(.horizontal, 48)
|
|
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
|
|
|
|
if residenceViewModel.myResidences?.residences.isEmpty ?? true {
|
|
Text(L10n.Tasks.addPropertyFirst)
|
|
.font(.caption)
|
|
.foregroundColor(Color.appError)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
} else {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHGrid(rows: [
|
|
GridItem(.flexible(), spacing: 16)
|
|
], spacing: 16) {
|
|
// 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
|
|
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
|
|
if let task = allTasks.first(where: { $0.id == taskId }) {
|
|
selectedTaskForCancel = task
|
|
showCancelConfirmation = true
|
|
}
|
|
},
|
|
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
|
|
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
|
|
if let task = allTasks.first(where: { $0.id == taskId }) {
|
|
selectedTaskForArchive = task
|
|
showArchiveConfirmation = true
|
|
}
|
|
},
|
|
onUnarchiveTask: { taskId in
|
|
taskViewModel.unarchiveTask(id: taskId) { _ in
|
|
loadAllTasks()
|
|
}
|
|
}
|
|
)
|
|
.frame(width: 350)
|
|
.scrollTransition { content, phase in
|
|
content
|
|
.opacity(phase.isIdentity ? 1 : 0.8)
|
|
.scaleEffect(phase.isIdentity ? 1 : 0.95)
|
|
}
|
|
}
|
|
}
|
|
.scrollTargetLayout()
|
|
.padding(16)
|
|
}
|
|
.scrollTargetBehavior(.viewAligned)
|
|
}
|
|
}
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.background(Color.appBackgroundPrimary)
|
|
.navigationTitle(L10n.Tasks.allTasks)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button(action: {
|
|
// Check if we should show upgrade prompt before adding
|
|
if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTaskCount, limitKey: "tasks") {
|
|
showingUpgradePrompt = true
|
|
} else {
|
|
showAddTask = true
|
|
}
|
|
}) {
|
|
Image(systemName: "plus")
|
|
}
|
|
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
|
|
}
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button(action: {
|
|
loadAllTasks(forceRefresh: true)
|
|
}) {
|
|
Image(systemName: "arrow.clockwise")
|
|
.rotationEffect(.degrees(isLoadingTasks ? 360 : 0))
|
|
.animation(isLoadingTasks ? .linear(duration: 0.5).repeatForever(autoreverses: false) : .default, value: isLoadingTasks)
|
|
}
|
|
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true || isLoadingTasks)
|
|
}
|
|
}
|
|
.onChange(of: taskViewModel.isLoading) { isLoading in
|
|
if !isLoading {
|
|
loadAllTasks()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func loadAllTasks(forceRefresh: Bool = false) {
|
|
taskViewModel.loadTasks(forceRefresh: forceRefresh)
|
|
}
|
|
|
|
private func updateTaskInKanban(_ updatedTask: TaskResponse) {
|
|
taskViewModel.updateTaskInKanban(updatedTask)
|
|
}
|
|
}
|
|
|
|
// Extension to apply corner radius to specific corners
|
|
extension View {
|
|
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
|
clipShape(RoundedCorner(radius: radius, corners: corners))
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationView {
|
|
AllTasksView()
|
|
}
|
|
}
|
|
|
|
extension Array where Element == ResidenceResponse {
|
|
/// Returns the array as-is (for API compatibility)
|
|
func toResidences() -> [ResidenceResponse] {
|
|
return self
|
|
}
|
|
}
|