Commit 2790ec0678
Unsigned
Layout: unified · split
Hutch/Views/Tickets/TicketListView.swift +71 −48
| @@ -205,12 +205,13 @@ struct TicketListView: View { | ||
| 205 | 205 | } |
| 206 | 206 | .swipeActions(edge: .leading, allowsFullSwipe: true) { |
| 207 | 207 | if swipeActionsEnabled { |
| 208 | ticketLeadingSwipeAction(ticket, viewModel: viewModel) | |
| 208 | ticketAssignSwipeAction(ticket, viewModel: viewModel) | |
| 209 | 209 | } |
| 210 | 210 | } |
| 211 | 211 | .swipeActions(edge: .trailing, allowsFullSwipe: false) { |
| 212 | 212 | if swipeActionsEnabled { |
| 213 | ticketTrailingSwipeActions(ticket, viewModel: viewModel) | |
| 213 | ticketStatusSwipeAction(ticket, viewModel: viewModel) | |
| 214 | ticketLabelSwipeAction(ticket, viewModel: viewModel) | |
| 214 | 215 | } |
| 215 | 216 | } |
| 216 | 217 | .task { |
| @@ -294,52 +295,10 @@ struct TicketListView: View { | ||
| 294 | 295 | } |
| 295 | 296 | |
| 296 | 297 | @ViewBuilder |
| 297 | private func ticketLeadingSwipeAction( | |
| 298 | private func ticketAssignSwipeAction( | |
| 298 | 299 | _ ticket: TicketSummary, |
| 299 | 300 | viewModel: TicketListViewModel |
| 300 | 301 | ) -> some View { |
| 301 | if ticket.status.isOpen { | |
| 302 | Button { | |
| 303 | Task { | |
| 304 | await resolveTicket(ticket, viewModel: viewModel) | |
| 305 | } | |
| 306 | } label: { | |
| 307 | Label("Resolve", systemImage: "checkmark.circle") | |
| 308 | } | |
| 309 | .tint(.green) | |
| 310 | } else { | |
| 311 | Button { | |
| 312 | Task { | |
| 313 | await reopenTicket(ticket, viewModel: viewModel) | |
| 314 | } | |
| 315 | } label: { | |
| 316 | Label("Reopen", systemImage: "arrow.uturn.backward") | |
| 317 | } | |
| 318 | .tint(.blue) | |
| 319 | } | |
| 320 | } | |
| 321 | ||
| 322 | private func resolveTicket(_ ticket: TicketSummary, viewModel: TicketListViewModel) async { | |
| 323 | await viewModel.resolveTicket(ticket) | |
| 324 | } | |
| 325 | ||
| 326 | private func reopenTicket(_ ticket: TicketSummary, viewModel: TicketListViewModel) async { | |
| 327 | await viewModel.reopenTicket(ticket) | |
| 328 | } | |
| 329 | ||
| 330 | @ViewBuilder | |
| 331 | private func ticketTrailingSwipeActions( | |
| 332 | _ ticket: TicketSummary, | |
| 333 | viewModel: TicketListViewModel | |
| 334 | ) -> some View { | |
| 335 | Button { | |
| 336 | labelEditorTicket = LabelEditorTicket(id: ticket.id) | |
| 337 | Task { await viewModel.loadTrackerLabels() } | |
| 338 | } label: { | |
| 339 | Label("Edit Labels", systemImage: "tag") | |
| 340 | } | |
| 341 | .tint(.purple) | |
| 342 | ||
| 343 | 302 | if let currentUser = appState.currentUser { |
| 344 | 303 | let isAssigned = ticket.assignees.contains { assignee in |
| 345 | 304 | matchesAssignee(assignee, user: currentUser) |
| @@ -367,6 +326,42 @@ struct TicketListView: View { | ||
| 367 | 326 | } |
| 368 | 327 | } |
| 369 | 328 | |
| 329 | @ViewBuilder | |
| 330 | private func ticketStatusSwipeAction( | |
| 331 | _ ticket: TicketSummary, | |
| 332 | viewModel: TicketListViewModel | |
| 333 | ) -> some View { | |
| 334 | if ticket.status.isOpen { | |
| 335 | Button { | |
| 336 | Task { await viewModel.resolveTicket(ticket) } | |
| 337 | } label: { | |
| 338 | Label("Close", systemImage: "xmark.circle") | |
| 339 | } | |
| 340 | .tint(.red) | |
| 341 | } else { | |
| 342 | Button { | |
| 343 | Task { await viewModel.reopenTicket(ticket) } | |
| 344 | } label: { | |
| 345 | Label("Reopen", systemImage: "arrow.uturn.backward") | |
| 346 | } | |
| 347 | .tint(.blue) | |
| 348 | } | |
| 349 | } | |
| 350 | ||
| 351 | @ViewBuilder | |
| 352 | private func ticketLabelSwipeAction( | |
| 353 | _ ticket: TicketSummary, | |
| 354 | viewModel: TicketListViewModel | |
| 355 | ) -> some View { | |
| 356 | Button { | |
| 357 | labelEditorTicket = LabelEditorTicket(id: ticket.id) | |
| 358 | Task { await viewModel.loadTrackerLabels() } | |
| 359 | } label: { | |
| 360 | Label("Labels", systemImage: "tag") | |
| 361 | } | |
| 362 | .tint(.purple) | |
| 363 | } | |
| 364 | ||
| 370 | 365 | private func matchesAssignee(_ entity: Entity, user: User) -> Bool { |
| 371 | 366 | let assigneeCanonical = normalizedCanonicalName(entity.canonicalName) |
| 372 | 367 | let userCanonical = normalizedCanonicalName(user.canonicalName) |
| @@ -546,6 +541,8 @@ private struct TicketRowView: View { | ||
| 546 | 541 | } |
| 547 | 542 | |
| 548 | 543 | Spacer() |
| 544 | ||
| 545 | TicketStatusBadge(status: ticket.status) | |
| 549 | 546 | } |
| 550 | 547 | |
| 551 | 548 | HStack(spacing: 8) { |
| @@ -584,6 +581,32 @@ private struct TicketRowView: View { | ||
| 584 | 581 | } |
| 585 | 582 | } |
| 586 | 583 | |
| 584 | // MARK: - Ticket Status Badge | |
| 585 | ||
| 586 | private struct TicketStatusBadge: View { | |
| 587 | let status: TicketStatus | |
| 588 | ||
| 589 | var body: some View { | |
| 590 | Text(status.displayName) | |
| 591 | .font(.caption2.weight(.semibold)) | |
| 592 | .padding(.horizontal, 6) | |
| 593 | .padding(.vertical, 2) | |
| 594 | .foregroundStyle(color) | |
| 595 | .background(color.opacity(0.12)) | |
| 596 | .clipShape(Capsule()) | |
| 597 | } | |
| 598 | ||
| 599 | private var color: Color { | |
| 600 | switch status { | |
| 601 | case .reported: .gray | |
| 602 | case .confirmed: .blue | |
| 603 | case .inProgress: .yellow | |
| 604 | case .pending: .orange | |
| 605 | case .resolved: .green | |
| 606 | } | |
| 607 | } | |
| 608 | } | |
| 609 | ||
| 587 | 610 | // MARK: - Ticket Status Icon |
| 588 | 611 | |
| 589 | 612 | struct TicketStatusIcon: View { |
| @@ -622,9 +645,9 @@ struct LabelPill: View { | ||
| 622 | 645 | |
| 623 | 646 | var body: some View { |
| 624 | 647 | Text(label.name) |
| 625 | .font(.caption2.weight(.medium)) | |
| 626 | .padding(.horizontal, 6) | |
| 627 | .padding(.vertical, 2) | |
| 648 | .font(.caption.weight(.medium)) | |
| 649 | .padding(.horizontal, 8) | |
| 650 | .padding(.vertical, 3) | |
| 628 | 651 | .background(backgroundColor) |
| 629 | 652 | .foregroundStyle(foregroundColor) |
| 630 | 653 | .clipShape(Capsule()) |