Update task cards to use menu-based actions and horizontal grid layout

- Replaced individual action buttons with a single Actions menu in DynamicTaskCard
  - Organized menu items into sections (primary, secondary, destructive)
  - Added visual dividers between action groups
  - Blue button styling for better visibility
  - Added debug logging for menu interactions
  - Menu properly handles all action types (mark in progress, complete, edit, cancel, restore, archive, unarchive)

- Converted AllTasksView to use horizontal grid layout (LazyHGrid)
  - Changed from vertical scroll to horizontal scroll
  - Added paging behavior with scroll target alignment
  - Added scroll transitions for smooth animations (fade and scale)
  - Fixed width columns (350pt) for consistent sizing
  - Maintained pull-to-refresh functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-13 14:53:30 -06:00
parent 2b95c3b9c1
commit b2f2627ad5
2 changed files with 161 additions and 104 deletions

View File

@@ -14,6 +14,8 @@ struct DynamicTaskCard: View {
let onUnarchive: () -> Void let onUnarchive: () -> Void
var body: some View { var body: some View {
let _ = print("📋 DynamicTaskCard - Task: \(task.title), ButtonTypes: \(buttonTypes)")
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
HStack { HStack {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
@@ -70,17 +72,34 @@ struct DynamicTaskCard: View {
} }
} }
// Render buttons based on buttonTypes array // Actions menu
VStack(spacing: 8) { if !buttonTypes.isEmpty {
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in Divider()
renderButton(for: buttonType)
Menu {
menuContent
} label: {
HStack {
Image(systemName: "ellipsis.circle.fill")
.font(.title3)
Text("Actions")
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
} }
.zIndex(10)
.menuOrder(.fixed)
} }
} }
.padding(16) .padding(16)
.background(Color(.systemBackground)) .background(Color(.systemBackground))
.cornerRadius(12) .cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2) .shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
.simultaneousGesture(TapGesture(), including: .subviews)
} }
private func formatDate(_ dateString: String) -> String { private func formatDate(_ dateString: String) -> String {
@@ -93,65 +112,104 @@ struct DynamicTaskCard: View {
return dateString return dateString
} }
// MARK: - Menu Content
@ViewBuilder @ViewBuilder
private func renderButton(for buttonType: String) -> some View { private var menuContent: some View {
// Primary actions
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
if isPrimaryAction(buttonType) {
menuButton(for: buttonType)
}
}
// Secondary actions (if any exist)
if buttonTypes.contains(where: { isSecondaryAction($0) }) {
Divider()
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
if isSecondaryAction(buttonType) {
menuButton(for: buttonType)
}
}
}
// Destructive actions (if any exist)
if buttonTypes.contains(where: { isDestructiveAction($0) }) {
Divider()
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
if isDestructiveAction(buttonType) {
menuButton(for: buttonType)
}
}
}
}
private func isPrimaryAction(_ buttonType: String) -> Bool {
["mark_in_progress", "complete", "edit", "uncancel", "unarchive"].contains(buttonType)
}
private func isSecondaryAction(_ buttonType: String) -> Bool {
["archive"].contains(buttonType)
}
private func isDestructiveAction(_ buttonType: String) -> Bool {
["cancel"].contains(buttonType)
}
@ViewBuilder
private func menuButton(for buttonType: String) -> some View {
switch buttonType { switch buttonType {
case "mark_in_progress": case "mark_in_progress":
MarkInProgressButton( Button {
taskId: task.id, print("🔵 Mark In Progress tapped for task: \(task.id)")
onCompletion: onMarkInProgress, onMarkInProgress()
onError: { error in } label: {
print("Error marking in progress: \(error)") Label("Mark Task In Progress", systemImage: "play.circle")
} }
)
case "complete": case "complete":
CompleteTaskButton( Button {
taskId: task.id, print("✅ Complete tapped for task: \(task.id)")
onCompletion: onComplete, onComplete()
onError: { error in } label: {
print("Error completing task: \(error)") Label("Complete Task", systemImage: "checkmark.circle")
} }
)
case "edit": case "edit":
EditTaskButton( Button {
taskId: task.id, print("✏️ Edit tapped for task: \(task.id)")
onCompletion: onEdit, onEdit()
onError: { error in } label: {
print("Error editing task: \(error)") Label("Edit Task", systemImage: "pencil")
} }
)
case "cancel": case "cancel":
CancelTaskButton( Button(role: .destructive) {
taskId: task.id, print("❌ Cancel tapped for task: \(task.id)")
onCompletion: onCancel, onCancel()
onError: { error in } label: {
print("Error cancelling task: \(error)") Label("Cancel Task", systemImage: "xmark.circle")
} }
)
case "uncancel": case "uncancel":
UncancelTaskButton( Button {
taskId: task.id, print("🔄 Restore tapped for task: \(task.id)")
onCompletion: onUncancel, onUncancel()
onError: { error in } label: {
print("Error restoring task: \(error)") Label("Restore Task", systemImage: "arrow.uturn.backward.circle")
} }
)
case "archive": case "archive":
ArchiveTaskButton( Button {
taskId: task.id, print("📦 Archive tapped for task: \(task.id)")
onCompletion: onArchive, onArchive()
onError: { error in } label: {
print("Error archiving task: \(error)") Label("Archive Task", systemImage: "archivebox")
} }
)
case "unarchive": case "unarchive":
UnarchiveTaskButton( Button {
taskId: task.id, print("📤 Unarchive tapped for task: \(task.id)")
onCompletion: onUnarchive, onUnarchive()
onError: { error in } label: {
print("Error unarchiving task: \(error)") Label("Unarchive Task", systemImage: "arrow.up.bin")
} }
)
default: default:
EmptyView() EmptyView()
} }

View File

@@ -77,62 +77,61 @@ struct AllTasksView: View {
} }
.padding() .padding()
} else { } else {
GeometryReader { geometry in ScrollView(.horizontal, showsIndicators: false) {
ScrollView(.horizontal, showsIndicators: false) { LazyHGrid(rows: [
LazyHStack(spacing: 16) { GridItem(.flexible(), spacing: 16)
// Dynamically create columns from response ], spacing: 16) {
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in // Dynamically create columns from response
DynamicTaskColumnView( ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
column: column, DynamicTaskColumnView(
onEditTask: { task in column: column,
selectedTaskForEdit = task onEditTask: { task in
showEditTask = true selectedTaskForEdit = task
}, showEditTask = true
onCancelTask: { taskId in },
taskViewModel.cancelTask(id: taskId) { _ in onCancelTask: { taskId in
loadAllTasks() taskViewModel.cancelTask(id: taskId) { _ in
} loadAllTasks()
}, }
onUncancelTask: { taskId in },
taskViewModel.uncancelTask(id: taskId) { _ in onUncancelTask: { taskId in
loadAllTasks() taskViewModel.uncancelTask(id: taskId) { _ in
} loadAllTasks()
}, }
onMarkInProgress: { taskId in },
taskViewModel.markInProgress(id: taskId) { success in onMarkInProgress: { taskId in
if success { taskViewModel.markInProgress(id: taskId) { success in
loadAllTasks() if success {
}
}
},
onCompleteTask: { task in
selectedTaskForComplete = task
},
onArchiveTask: { taskId in
taskViewModel.archiveTask(id: taskId) { _ in
loadAllTasks()
}
},
onUnarchiveTask: { taskId in
taskViewModel.unarchiveTask(id: taskId) { _ in
loadAllTasks() loadAllTasks()
} }
} }
) },
.frame(width: geometry.size.width - 48) onCompleteTask: { task in
selectedTaskForComplete = task
},
onArchiveTask: { taskId in
taskViewModel.archiveTask(id: taskId) { _ in
loadAllTasks()
}
},
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(.horizontal, 16)
}
.scrollTargetBehavior(.viewAligned)
.refreshable {
loadAllTasks(forceRefresh: true)
}
.safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0)
} }
.scrollTargetLayout()
.padding(16)
} }
.scrollTargetBehavior(.viewAligned)
} }
} }
} }
@@ -154,7 +153,7 @@ struct AllTasksView: View {
}) { }) {
Image(systemName: "arrow.clockwise") Image(systemName: "arrow.clockwise")
.rotationEffect(.degrees(isLoadingTasks ? 360 : 0)) .rotationEffect(.degrees(isLoadingTasks ? 360 : 0))
.animation(isLoadingTasks ? .linear(duration: 1).repeatForever(autoreverses: false) : .default, value: isLoadingTasks) .animation(isLoadingTasks ? .linear(duration: 0.5).repeatForever(autoreverses: false) : .default, value: isLoadingTasks)
} }
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true || isLoadingTasks) .disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true || isLoadingTasks)
} }