import SwiftUI struct TaskDetailScreen: View { let task: WorkTask var showCompleteButton = false var showAcceptButton = false @EnvironmentObject var appState: AppState @Environment(\.dismiss) var dismiss @State private var details: TaskDetails? @State private var isLoading = true @State private var error: String? // Beacon @State private var beaconDetected = false @State private var bluetoothOff = false @State private var autoCompleting = false @State private var showAutoCompleteDialog = false @State private var showAcceptAlert = false @State private var showCompleteAlert = false @State private var beaconScanner: BeaconScanner? @State private var showingMyTasks = false @State private var showingChat = false var body: some View { ZStack(alignment: .bottomTrailing) { Group { if isLoading { ProgressView() } else if let error = error { errorView(error) } else if let details = details { contentView(details) } } .frame(maxWidth: .infinity, maxHeight: .infinity) Button { showingMyTasks = true } label: { Image(systemName: "checkmark.circle.fill") .font(.title3) .padding(12) .background(Color.payfritGreen) .foregroundColor(.white) .clipShape(Circle()) .shadow(radius: 4) } .padding(.trailing, 16) .padding(.bottom, 80) } .navigationTitle(task.title) .toolbarBackground(task.color, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) .alert("Accept Task?", isPresented: $showAcceptAlert) { Button("Cancel", role: .cancel) { } Button("Accept") { acceptTask() } } message: { Text("Claim this task and add it to your tasks?") } .alert("Complete Task?", isPresented: $showCompleteAlert) { Button("Cancel", role: .cancel) { } Button("Complete") { completeTask() } } message: { Text("Mark this task as completed?") } .sheet(isPresented: $showAutoCompleteDialog) { AutoCompleteCountdownView(taskId: task.taskId) { result in showAutoCompleteDialog = false if result == "success" { dismiss() } else if result == "cancelled" || result == "error" { autoCompleting = false beaconDetected = false beaconScanner?.resetSamples() beaconScanner?.startScanning() } } } .navigationDestination(isPresented: $showingMyTasks) { MyTasksScreen() } .navigationDestination(isPresented: $showingChat) { ChatScreen(taskId: task.taskId, userType: "worker", otherPartyName: details?.customerFullName) } .task { await loadDetails() } .onDisappear { beaconScanner?.dispose() } } // MARK: - Content @ViewBuilder private func contentView(_ details: TaskDetails) -> some View { ScrollView { VStack(spacing: 16) { customerSection(details) if task.isChat { chatButton(details) } locationSection(details) if !details.tableMembers.isEmpty { tableMembersSection(details) } if !details.mainItems.isEmpty { orderItemsSection(details) } if !details.orderRemarks.isEmpty { remarksSection(details) } Spacer().frame(height: 80) } .padding(16) } .safeAreaInset(edge: .bottom) { if showAcceptButton || showCompleteButton { bottomBar } } } // MARK: - Customer private func customerSection(_ d: TaskDetails) -> some View { HStack(spacing: 16) { // Avatar ZStack { Circle() .fill(task.color.opacity(0.2)) .frame(width: 64, height: 64) if !d.customerPhotoUrl.isEmpty, let url = URL(string: d.customerPhotoUrl) { AsyncImage(url: url) { image in image.resizable().scaledToFill() } placeholder: { initialsView(d) } .frame(width: 64, height: 64) .clipShape(Circle()) } else { initialsView(d) } } VStack(alignment: .leading, spacing: 4) { Text(d.customerFullName) .font(.title3.bold()) if !d.customerPhone.isEmpty { Text(d.customerPhone) .font(.subheadline) .foregroundColor(.secondary) } } Spacer() if !d.customerPhone.isEmpty { Button { callCustomer(d.customerPhone) } label: { Image(systemName: "phone.fill") .foregroundColor(.green) .font(.title2) } } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } private func initialsView(_ d: TaskDetails) -> some View { let f = d.customerFirstName.isEmpty ? "" : String(d.customerFirstName.prefix(1)).uppercased() let l = d.customerLastName.isEmpty ? "" : String(d.customerLastName.prefix(1)).uppercased() let text = "\(f)\(l)".isEmpty ? "?" : "\(f)\(l)" return Text(text) .font(.title2.bold()) .foregroundColor(task.color) } // MARK: - Chat Button @ViewBuilder private func chatButton(_ d: TaskDetails) -> some View { Button { showingChat = true } label: { HStack(spacing: 12) { ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color.payfritGreen.opacity(0.1)) .frame(width: 48, height: 48) Image(systemName: "bubble.left.and.bubble.right.fill") .foregroundColor(.payfritGreen) .font(.title2) } VStack(alignment: .leading, spacing: 2) { Text("Chat with Customer") .font(.callout.weight(.medium)) .foregroundColor(.primary) Text(d.customerFullName) .font(.caption) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.caption) } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } } // MARK: - Location private func locationSection(_ d: TaskDetails) -> some View { HStack(spacing: 16) { ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color.payfritGreen.opacity(0.1)) .frame(width: 48, height: 48) Image(systemName: d.isDelivery ? "bicycle" : "table.furniture") .foregroundColor(.payfritGreen) .font(.title2) } VStack(alignment: .leading, spacing: 4) { Text(d.isDelivery ? "Delivery" : "Table Service") .font(.caption) .foregroundColor(.secondary) Text(d.locationDisplay) .font(.callout.weight(.medium)) } Spacer() if d.isDelivery && d.deliveryLat != 0 { Button { openMaps(d) } label: { Image(systemName: "arrow.triangle.turn.up.right.diamond.fill") .foregroundColor(.payfritGreen) } } if !d.isDelivery && !d.beaconUUID.isEmpty && showCompleteButton { beaconIndicator } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } @ViewBuilder private var beaconIndicator: some View { if autoCompleting { ProgressView() } else if bluetoothOff { Image(systemName: "antenna.radiowaves.left.and.right.slash") .foregroundColor(.payfritGreen) } else if beaconDetected { Image(systemName: "antenna.radiowaves.left.and.right") .foregroundColor(.green) } else { Image(systemName: "antenna.radiowaves.left.and.right") .foregroundColor(.payfritGreen) .opacity(0.7) } } // MARK: - Table Members private func tableMembersSection(_ d: TaskDetails) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: "person.3.fill") .foregroundColor(.secondary) Text("Table Members (\(d.tableMembers.count))") .font(.subheadline.weight(.semibold)) } FlowLayout(spacing: 8) { ForEach(d.tableMembers) { member in HStack(spacing: 6) { Circle() .fill(member.isHost ? Color.yellow.opacity(0.3) : Color(.systemGray4)) .frame(width: 28, height: 28) .overlay( Text(member.initials) .font(.caption2) ) Text(member.fullName) .font(.subheadline) } .padding(.horizontal, 10) .padding(.vertical, 6) .background(member.isHost ? Color.yellow.opacity(0.1) : Color(.systemGray6)) .cornerRadius(16) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(member.isHost ? Color.yellow : Color.clear, lineWidth: 1) ) } } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } // MARK: - Order Items private func orderItemsSection(_ d: TaskDetails) -> some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: "list.bullet.rectangle.portrait") .foregroundColor(.secondary) Text("Order Items (\(d.mainItems.count))") .font(.subheadline.weight(.semibold)) } ForEach(d.mainItems) { item in orderItemRow(item, details: d) } } .padding(16) .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) } private func orderItemRow(_ item: OrderLineItem, details d: TaskDetails) -> some View { let modifiers = d.modifiers(for: item.lineItemId) return HStack(alignment: .top, spacing: 12) { // Item image placeholder RoundedRectangle(cornerRadius: 8) .fill(Color(.systemGray5)) .frame(width: 56, height: 56) .overlay( Image(systemName: "fork.knife") .foregroundColor(.secondary) ) VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { Text("\(item.quantity)x") .font(.caption.bold()) .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 2) .background(task.color) .cornerRadius(4) Text(item.itemName) .font(.callout.weight(.semibold)) } ForEach(modifiers) { mod in let children = d.lineItems.filter { $0.parentLineItemId == mod.lineItemId } if !children.isEmpty { ForEach(children) { child in Text("- \(mod.itemName): \(child.itemName)") .font(.caption) .foregroundColor(.secondary) } } else { Text("- \(mod.itemName)") .font(.caption) .foregroundColor(.secondary) } } if !item.remark.isEmpty { Text("\"\(item.remark)\"") .font(.caption) .foregroundColor(.payfritGreen) .italic() } } } .padding(.bottom, 8) } // MARK: - Remarks private func remarksSection(_ d: TaskDetails) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: "note.text") .foregroundColor(.yellow) VStack(alignment: .leading, spacing: 4) { Text("Order Notes") .font(.caption.weight(.semibold)) .foregroundColor(.yellow) Text(d.orderRemarks) .font(.callout) } Spacer() } .padding(16) .background(Color.yellow.opacity(0.1)) .cornerRadius(12) } // MARK: - Bottom Bar private var bottomBar: some View { HStack { if showAcceptButton { Button { showAcceptAlert = true } label: { Label("Accept Task", systemImage: "plus.circle.fill") .frame(maxWidth: .infinity) .padding(.vertical, 14) } .buttonStyle(.borderedProminent) .tint(.payfritGreen) } if showCompleteButton { Button { showCompleteAlert = true } label: { Label("Complete Task", systemImage: "checkmark.circle.fill") .frame(maxWidth: .infinity) .padding(.vertical, 14) } .buttonStyle(.borderedProminent) .tint(.green) } } .padding(.horizontal) .padding(.vertical, 12) .background(.ultraThinMaterial) } // MARK: - Error private func errorView(_ message: String) -> some View { VStack(spacing: 16) { Image(systemName: "exclamationmark.circle") .font(.system(size: 48)) .foregroundColor(.red) Text(message) .multilineTextAlignment(.center) Button("Retry") { Task { await loadDetails() } } .buttonStyle(.borderedProminent) } .padding() } // MARK: - Actions private func loadDetails() async { isLoading = true error = nil do { let d = try await APIService.shared.getTaskDetails(taskId: task.taskId) details = d isLoading = false if showCompleteButton && !d.beaconUUID.isEmpty { startBeaconScanning(d.beaconUUID) } } catch { self.error = error.localizedDescription isLoading = false } } private func startBeaconScanning(_ uuid: String) { let scanner = BeaconScanner( targetUUID: uuid, onBeaconDetected: { [self] _ in if !beaconDetected && !autoCompleting { beaconDetected = true autoCompleting = true beaconScanner?.stopScanning() showAutoCompleteDialog = true } }, onBluetoothOff: { bluetoothOff = true }, onPermissionDenied: { self.error = "Bluetooth permission is required for auto-complete. Please enable it in Settings." } ) beaconScanner = scanner scanner.startScanning() } private func acceptTask() { Task { do { try await APIService.shared.acceptTask(taskId: task.taskId) if task.isChat { showingChat = true } else { dismiss() } } catch { self.error = error.localizedDescription } } } private func completeTask() { Task { do { try await APIService.shared.completeTask(taskId: task.taskId) dismiss() } catch { self.error = error.localizedDescription } } } private func callCustomer(_ phone: String) { guard let url = URL(string: "tel:\(phone)") else { return } #if os(iOS) UIApplication.shared.open(url) #endif } private func openMaps(_ d: TaskDetails) { guard d.deliveryLat != 0, d.deliveryLng != 0 else { return } let urlStr = "https://www.google.com/maps/dir/?api=1&destination=\(d.deliveryLat),\(d.deliveryLng)" guard let url = URL(string: urlStr) else { return } #if os(iOS) UIApplication.shared.open(url) #endif } } // MARK: - Auto-Complete Countdown struct AutoCompleteCountdownView: View { let taskId: Int let onResult: (String) -> Void @State private var countdown = 3 @State private var completing = false @State private var message = "Auto-completing in" var body: some View { VStack(spacing: 24) { Image(systemName: completing ? "checkmark.circle.fill" : "timer") .font(.system(size: 48)) .foregroundColor(completing ? .green : .payfritGreen) Text(completing ? "Task Completed!" : "Auto-Complete") .font(.title2.bold()) Text(completing ? message : "\(message) \(countdown)...") .font(.body) if !completing { Button("Cancel") { onResult("cancelled") } .buttonStyle(.bordered) } } .padding(32) .presentationDetents([.height(280)]) .task { await startCountdown() } } private func startCountdown() async { for i in stride(from: 3, through: 1, by: -1) { countdown = i try? await Task.sleep(nanoseconds: 1_000_000_000) } completing = true message = "Completing task..." do { try await APIService.shared.completeTask(taskId: taskId) message = "Closing this window now" try? await Task.sleep(nanoseconds: 1_000_000_000) onResult("success") } catch { onResult("error") } } } // MARK: - Flow Layout (for table members) struct FlowLayout: Layout { var spacing: CGFloat = 8 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let result = arrangeSubviews(proposal: proposal, subviews: subviews) return result.size } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let result = arrangeSubviews(proposal: proposal, subviews: subviews) for (index, position) in result.positions.enumerated() { subviews[index].place(at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y), proposal: .unspecified) } } private func arrangeSubviews(proposal: ProposedViewSize, subviews: Subviews) -> (positions: [CGPoint], size: CGSize) { let maxWidth = proposal.width ?? .infinity var positions: [CGPoint] = [] var currentX: CGFloat = 0 var currentY: CGFloat = 0 var lineHeight: CGFloat = 0 var totalWidth: CGFloat = 0 for subview in subviews { let size = subview.sizeThatFits(.unspecified) if currentX + size.width > maxWidth && currentX > 0 { currentX = 0 currentY += lineHeight + spacing lineHeight = 0 } positions.append(CGPoint(x: currentX, y: currentY)) lineHeight = max(lineHeight, size.height) currentX += size.width + spacing totalWidth = max(totalWidth, currentX) } return (positions, CGSize(width: totalWidth, height: currentY + lineHeight)) } }