payfrit-works-ios/PayfritWorks/Views/MyTasksScreen.swift
Schwifty 543e19a664 fix: standardize refresh intervals to 3 seconds across all screens
Per parity decision #4 — both platforms conform to 3-second refresh intervals.
Previously iOS Works was at 2s, Android was at 5s. Now both at 3s.

Screens updated:
- MyTasksScreen: 2s → 3s
- BusinessSelectionScreen: 2s → 3s
- TaskListScreen: 2s → 3s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:21:39 +00:00

278 lines
9.6 KiB
Swift

import SwiftUI
struct MyTasksScreen: View {
@Environment(\.dismiss) var dismiss
@State private var selectedFilter = "active"
@State private var tasksByFilter: [String: [WorkTask]] = [
"active": [], "today": [], "week": [], "completed": []
]
@State private var loadingByFilter: [String: Bool] = [
"active": true, "today": true, "week": true, "completed": true
]
@State private var errorByFilter: [String: String?] = [
"active": nil, "today": nil, "week": nil, "completed": nil
]
private let filters = [
FilterTab(value: "active", label: "Active", icon: "play.fill"),
FilterTab(value: "today", label: "Today", icon: "calendar"),
FilterTab(value: "week", label: "This Week", icon: "calendar.badge.clock"),
FilterTab(value: "completed", label: "Done", icon: "checkmark.circle.fill"),
]
private let refreshTimer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 0) {
// Tab bar
HStack(spacing: 0) {
ForEach(filters, id: \.value) { filter in
Button {
selectedFilter = filter.value
loadTasks(filter.value)
} label: {
VStack(spacing: 4) {
Image(systemName: filter.icon)
.font(.caption)
Text(filter.label)
.font(.caption2)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.foregroundColor(selectedFilter == filter.value ? .white : .white.opacity(0.7))
}
}
}
.background(Color.payfritGreen)
// Indicator
GeometryReader { geo in
let idx = filters.firstIndex(where: { $0.value == selectedFilter }) ?? 0
let width = geo.size.width / CGFloat(filters.count)
Rectangle()
.fill(Color.white)
.frame(width: width, height: 3)
.offset(x: width * CGFloat(idx))
.animation(.easeInOut(duration: 0.2), value: selectedFilter)
}
.frame(height: 3)
.background(Color.payfritGreen)
// Content
TabView(selection: $selectedFilter) {
ForEach(filters, id: \.value) { filter in
taskListView(filter.value)
.tag(filter.value)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
.navigationTitle("My Tasks")
.toolbarBackground(Color.payfritGreen, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button { loadTasks(selectedFilter) } label: {
Image(systemName: "arrow.clockwise")
.foregroundColor(.white)
}
}
}
.task { loadTasks("active") }
.onReceive(refreshTimer) { _ in loadTasks(selectedFilter, silent: true) }
}
// MARK: - Task List per Filter
@ViewBuilder
private func taskListView(_ filterType: String) -> some View {
let isLoading = loadingByFilter[filterType] ?? true
let error = errorByFilter[filterType] ?? nil
let tasks = tasksByFilter[filterType] ?? []
if isLoading {
LoadingView()
} else if let error = error {
ErrorView(message: error) { loadTasks(filterType) }
} else if tasks.isEmpty {
emptyView(filterType)
} else {
List(tasks) { task in
let isCompleted = filterType == "completed"
NavigationLink {
TaskDetailScreen(task: task, showCompleteButton: !isCompleted)
} label: {
taskCard(task, isCompleted: isCompleted)
}
}
.listStyle(.plain)
.refreshable { loadTasks(filterType) }
}
}
private func taskCard(_ task: WorkTask, isCompleted: Bool) -> some View {
HStack {
Rectangle()
.fill(isCompleted ? Color.green : task.color)
.frame(width: 4)
.cornerRadius(2)
if isCompleted {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
}
VStack(alignment: .leading, spacing: 4) {
Text(task.title)
.font(.callout.weight(.semibold))
.strikethrough(isCompleted)
.foregroundColor(isCompleted ? .secondary : .primary)
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)
}
}
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)
Text(task.timeAgo)
.font(.caption2)
.foregroundColor(.secondary)
}
}
Spacer()
if task.isChat {
Image(systemName: "bubble.left.fill")
.foregroundColor(task.color)
.font(.footnote)
}
if !isCompleted {
Text(task.statusName)
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(statusColor(task.statusId))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(statusColor(task.statusId).opacity(0.1))
.cornerRadius(8)
}
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
@ViewBuilder
private func emptyView(_ filterType: String) -> some View {
VStack(spacing: 16) {
Image(systemName: emptyIcon(filterType))
.font(.system(size: 64))
.foregroundColor(.secondary)
Text(emptyMessage(filterType))
.font(.title3)
.foregroundColor(.secondary)
Text(emptySubMessage(filterType))
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
private func emptyIcon(_ f: String) -> String {
switch f {
case "active": return "doc.text"
case "today": return "calendar"
case "week": return "calendar.badge.clock"
case "completed": return "checkmark.circle"
default: return "tray"
}
}
private func emptyMessage(_ f: String) -> String {
switch f {
case "active": return "No active tasks"
case "today": return "No tasks today"
case "week": return "No tasks this week"
case "completed": return "No completed tasks"
default: return "No tasks"
}
}
private func emptySubMessage(_ f: String) -> String {
switch f {
case "active": return "Claim some tasks to get started!"
case "today": return "You haven't worked on any tasks today"
case "week": return "You haven't worked on any tasks this week"
case "completed": return "Complete tasks to see them here"
default: return ""
}
}
// MARK: - Actions
private func loadTasks(_ filterType: String, silent: Bool = false) {
if !silent {
loadingByFilter[filterType] = true
errorByFilter[filterType] = nil
}
Task {
do {
let tasks = try await APIService.shared.listMyTasks(filterType: filterType)
tasksByFilter[filterType] = tasks
loadingByFilter[filterType] = false
} catch {
if !silent {
errorByFilter[filterType] = error.localizedDescription
loadingByFilter[filterType] = false
}
}
}
}
private func statusColor(_ id: Int) -> Color {
switch id {
case 0: return .payfritGreen
case 1: return .payfritGreen
case 2: return .purple
case 3: return .green
default: return .gray
}
}
}
private struct FilterTab {
let value: String
let label: String
let icon: String
}