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:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user