287 lines
10 KiB
Swift
287 lines
10 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: 2, 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 {
|
|
ProgressView()
|
|
} else if let error = error {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "exclamationmark.circle")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(.red)
|
|
Text(error)
|
|
.multilineTextAlignment(.center)
|
|
Button("Retry") { loadTasks(filterType) }
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
.padding()
|
|
} 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
|
|
}
|