TaskListScreen, MyTasksScreen, and BusinessSelectionScreen all had 2-second refresh timers. Changed to 5 seconds to match Android and reduce server load. Chat polling (3s) left unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
240 lines
8.1 KiB
Swift
240 lines
8.1 KiB
Swift
import SwiftUI
|
|
|
|
struct TaskListScreen: View {
|
|
var businessName: String = ""
|
|
|
|
@EnvironmentObject var appState: AppState
|
|
@State private var tasks: [WorkTask] = []
|
|
@State private var isLoading = true
|
|
@State private var error: String?
|
|
@State private var lastRefresh = Date()
|
|
@State private var selectedTask: WorkTask?
|
|
@State private var showingMyTasks = false
|
|
|
|
private let refreshTimer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .bottomTrailing) {
|
|
Group {
|
|
if isLoading {
|
|
ProgressView()
|
|
} else if let error = error {
|
|
errorView(error)
|
|
} else if tasks.isEmpty {
|
|
emptyView
|
|
} else {
|
|
taskListView
|
|
}
|
|
}
|
|
.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, 16)
|
|
}
|
|
.navigationTitle(businessName.isEmpty ? "Available Tasks" : businessName)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button { loadTasks() } label: {
|
|
Image(systemName: "arrow.clockwise")
|
|
}
|
|
}
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Menu {
|
|
Label(appState.userName ?? "Worker", systemImage: "person.circle")
|
|
.disabled(true)
|
|
Divider()
|
|
Button { showingMyTasks = true } label: {
|
|
Label("My Tasks", systemImage: "checkmark.circle")
|
|
}
|
|
Button(role: .destructive) { logout() } label: {
|
|
Label("Logout", systemImage: "rectangle.portrait.and.arrow.right")
|
|
}
|
|
} label: {
|
|
Image(systemName: "ellipsis.circle")
|
|
}
|
|
}
|
|
}
|
|
.navigationDestination(isPresented: $showingMyTasks) {
|
|
MyTasksScreen()
|
|
}
|
|
.task { loadTasks() }
|
|
.onAppear { loadTasks(silent: true) }
|
|
.onReceive(refreshTimer) { _ in loadTasks(silent: true) }
|
|
}
|
|
|
|
// MARK: - List
|
|
|
|
private var taskListView: some View {
|
|
List(tasks) { task in
|
|
NavigationLink {
|
|
TaskDetailScreen(task: task, showAcceptButton: true)
|
|
} label: {
|
|
taskRow(task)
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
.refreshable { loadTasks() }
|
|
}
|
|
|
|
private func taskRow(_ task: WorkTask) -> some View {
|
|
HStack(spacing: 12) {
|
|
// Left color bar
|
|
Rectangle()
|
|
.fill(task.color)
|
|
.frame(width: 4)
|
|
.cornerRadius(2)
|
|
|
|
// Task type icon - uses task type color
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(task.color.opacity(0.15))
|
|
.frame(width: 40, height: 40)
|
|
Image(systemName: task.isChat ? "bubble.left.and.bubble.right.fill" : "doc.text.fill")
|
|
.foregroundColor(task.color)
|
|
.font(.subheadline)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(task.title)
|
|
.font(.callout.weight(.semibold))
|
|
|
|
if !task.locationDisplay.isEmpty {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: task.deliveryAddress.isEmpty ? "mappin.circle" : "bicycle")
|
|
.font(.caption2)
|
|
.foregroundColor(task.color)
|
|
Text(task.locationDisplay)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
} else if !task.details.isEmpty {
|
|
Text(task.details)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
HStack(spacing: 8) {
|
|
Text(task.categoryName)
|
|
.font(.caption2)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(task.color)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(task.color.opacity(0.2))
|
|
.cornerRadius(8)
|
|
|
|
if task.isChat {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "bubble.left.fill")
|
|
.font(.caption2)
|
|
Text("Chat")
|
|
.font(.caption2)
|
|
.fontWeight(.medium)
|
|
}
|
|
.foregroundColor(task.color)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(task.color.opacity(0.15))
|
|
.cornerRadius(8)
|
|
}
|
|
|
|
if task.isCash && task.orderTotal > 0 {
|
|
Text(String(format: "$%.2f", task.orderTotal / 100))
|
|
.font(.caption2.bold())
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(Color(red: 0.13, green: 0.55, blue: 0.13))
|
|
.clipShape(Capsule())
|
|
}
|
|
|
|
Text(task.timeAgo)
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if task.isChat {
|
|
Image(systemName: "bubble.right.fill")
|
|
.foregroundColor(task.color)
|
|
.font(.body)
|
|
} else {
|
|
Image(systemName: "chevron.right")
|
|
.foregroundColor(.secondary)
|
|
.font(.caption)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
|
|
// MARK: - Empty / Error
|
|
|
|
private var emptyView: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "checkmark.circle")
|
|
.font(.system(size: 64))
|
|
.foregroundColor(.secondary)
|
|
Text("No pending tasks")
|
|
.font(.title3)
|
|
.foregroundColor(.secondary)
|
|
Text("Check back soon!")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
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") { loadTasks() }
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
private func loadTasks(silent: Bool = false) {
|
|
if !silent {
|
|
isLoading = true
|
|
error = nil
|
|
}
|
|
Task {
|
|
do {
|
|
let result = try await APIService.shared.listPendingTasks()
|
|
tasks = result
|
|
isLoading = false
|
|
lastRefresh = Date()
|
|
} catch {
|
|
if !silent {
|
|
self.error = error.localizedDescription
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func logout() {
|
|
Task {
|
|
await APIService.shared.logout()
|
|
await AuthStorage.shared.clearAuth()
|
|
appState.clearAuth()
|
|
}
|
|
}
|
|
}
|