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
var body: some View {
let _ = print("📋 DynamicTaskCard - Task: \(task.title), ButtonTypes: \(buttonTypes)")
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
@@ -70,17 +72,34 @@ struct DynamicTaskCard: View {
}
}
// Render buttons based on buttonTypes array
VStack(spacing: 8) {
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
renderButton(for: buttonType)
// Actions menu
if !buttonTypes.isEmpty {
Divider()
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)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
.simultaneousGesture(TapGesture(), including: .subviews)
}
private func formatDate(_ dateString: String) -> String {
@@ -93,65 +112,104 @@ struct DynamicTaskCard: View {
return dateString
}
// MARK: - Menu Content
@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 {
case "mark_in_progress":
MarkInProgressButton(
taskId: task.id,
onCompletion: onMarkInProgress,
onError: { error in
print("Error marking in progress: \(error)")
}
)
Button {
print("🔵 Mark In Progress tapped for task: \(task.id)")
onMarkInProgress()
} label: {
Label("Mark Task In Progress", systemImage: "play.circle")
}
case "complete":
CompleteTaskButton(
taskId: task.id,
onCompletion: onComplete,
onError: { error in
print("Error completing task: \(error)")
}
)
Button {
print("✅ Complete tapped for task: \(task.id)")
onComplete()
} label: {
Label("Complete Task", systemImage: "checkmark.circle")
}
case "edit":
EditTaskButton(
taskId: task.id,
onCompletion: onEdit,
onError: { error in
print("Error editing task: \(error)")
}
)
Button {
print("✏️ Edit tapped for task: \(task.id)")
onEdit()
} label: {
Label("Edit Task", systemImage: "pencil")
}
case "cancel":
CancelTaskButton(
taskId: task.id,
onCompletion: onCancel,
onError: { error in
print("Error cancelling task: \(error)")
}
)
Button(role: .destructive) {
print("❌ Cancel tapped for task: \(task.id)")
onCancel()
} label: {
Label("Cancel Task", systemImage: "xmark.circle")
}
case "uncancel":
UncancelTaskButton(
taskId: task.id,
onCompletion: onUncancel,
onError: { error in
print("Error restoring task: \(error)")
}
)
Button {
print("🔄 Restore tapped for task: \(task.id)")
onUncancel()
} label: {
Label("Restore Task", systemImage: "arrow.uturn.backward.circle")
}
case "archive":
ArchiveTaskButton(
taskId: task.id,
onCompletion: onArchive,
onError: { error in
print("Error archiving task: \(error)")
}
)
Button {
print("📦 Archive tapped for task: \(task.id)")
onArchive()
} label: {
Label("Archive Task", systemImage: "archivebox")
}
case "unarchive":
UnarchiveTaskButton(
taskId: task.id,
onCompletion: onUnarchive,
onError: { error in
print("Error unarchiving task: \(error)")
}
)
Button {
print("📤 Unarchive tapped for task: \(task.id)")
onUnarchive()
} label: {
Label("Unarchive Task", systemImage: "arrow.up.bin")
}
default:
EmptyView()
}

View File

@@ -77,62 +77,61 @@ struct AllTasksView: View {
}
.padding()
} else {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(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
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
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
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()
}
}
)
.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")
.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)
}