Compare commits
1 commit
873cbba2aa
...
46e008b20b
| Author | SHA1 | Date | |
|---|---|---|---|
| 46e008b20b |
2567 changed files with 7421 additions and 228 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,5 @@
|
|||
# Xcode
|
||||
build/
|
||||
build-sim/
|
||||
DerivedData/
|
||||
*.xcuserstate
|
||||
*.xcworkspacedata
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@
|
|||
B01000000047 /* AccountScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000047; };
|
||||
B01000000048 /* AboutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000048; };
|
||||
B01000000049 /* ProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000049; };
|
||||
B0100000004A /* RatingDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0200000004A; };
|
||||
|
||||
/* Resources */
|
||||
B01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B02000000060; };
|
||||
|
|
@ -91,7 +90,6 @@
|
|||
B02000000047 /* AccountScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountScreen.swift; sourceTree = "<group>"; };
|
||||
B02000000048 /* AboutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutScreen.swift; sourceTree = "<group>"; };
|
||||
B02000000049 /* ProfileScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileScreen.swift; sourceTree = "<group>"; };
|
||||
B0200000004A /* RatingDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingDialog.swift; sourceTree = "<group>"; };
|
||||
|
||||
/* Resources */
|
||||
B02000000060 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
|
@ -188,7 +186,6 @@
|
|||
B02000000047 /* AccountScreen.swift */,
|
||||
B02000000048 /* AboutScreen.swift */,
|
||||
B02000000049 /* ProfileScreen.swift */,
|
||||
B0200000004A /* RatingDialog.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -315,7 +312,6 @@
|
|||
B01000000047 /* AccountScreen.swift in Sources */,
|
||||
B01000000048 /* AboutScreen.swift in Sources */,
|
||||
B01000000049 /* ProfileScreen.swift in Sources */,
|
||||
B0100000004A /* RatingDialog.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ enum APIError: LocalizedError {
|
|||
case .serverError(let msg): return msg
|
||||
case .unauthorized: return "Unauthorized"
|
||||
case .networkError(let msg): return msg
|
||||
case .ratingRequired: return "Rating required"
|
||||
case .ratingRequired: return "Customer rating is required before completing this task"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -492,14 +492,11 @@ actor APIService {
|
|||
|
||||
/// Complete a task. Returns CashCompletionResult when cashReceivedCents is provided.
|
||||
@discardableResult
|
||||
func completeTask(taskId: Int, workerRating: [String: Bool]? = nil, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws -> CashCompletionResult? {
|
||||
func completeTask(taskId: Int, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws -> CashCompletionResult? {
|
||||
var payload: [String: Any] = [
|
||||
"TaskID": taskId,
|
||||
"UserID": userId ?? 0
|
||||
]
|
||||
if let rating = workerRating {
|
||||
payload["workerRating"] = rating
|
||||
}
|
||||
if let cents = cashReceivedCents {
|
||||
payload["CashReceivedCents"] = cents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
/// Customer rating dialog shown when completing a service point task.
|
||||
/// Matches Android RatingDialog: 4 yes/no questions about the customer.
|
||||
struct RatingDialog: View {
|
||||
let onSubmit: ([String: Bool]) -> Void
|
||||
let onDismiss: () -> Void
|
||||
let isSubmitting: Bool
|
||||
|
||||
@State private var prepared: Bool?
|
||||
@State private var completedScope: Bool?
|
||||
@State private var respectful: Bool?
|
||||
@State private var wouldAutoAssign: Bool?
|
||||
|
||||
private var allAnswered: Bool {
|
||||
prepared != nil && completedScope != nil && respectful != nil && wouldAutoAssign != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
// Star icon
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color(red: 1.0, green: 0.76, blue: 0.03)) // Golden yellow
|
||||
|
||||
Text("Rate this customer")
|
||||
.font(.title2.bold())
|
||||
|
||||
Text("Quick feedback helps improve the experience.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
RatingQuestion(question: "Was the customer prepared?", value: $prepared)
|
||||
RatingQuestion(question: "Was the scope clear?", value: $completedScope)
|
||||
RatingQuestion(question: "Was the customer respectful?", value: $respectful)
|
||||
RatingQuestion(question: "Would you serve them again?", value: $wouldAutoAssign)
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if !isSubmitting {
|
||||
Button("Cancel") {
|
||||
onDismiss()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Button {
|
||||
guard let p = prepared, let c = completedScope,
|
||||
let r = respectful, let w = wouldAutoAssign else { return }
|
||||
onSubmit([
|
||||
"prepared": p,
|
||||
"completedScope": c,
|
||||
"respectful": r,
|
||||
"wouldAutoAssign": w
|
||||
])
|
||||
} label: {
|
||||
if isSubmitting {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.frame(maxWidth: .infinity, minHeight: 44)
|
||||
} else {
|
||||
Text("Submit & Complete")
|
||||
.font(.headline)
|
||||
.frame(maxWidth: .infinity, minHeight: 44)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.payfritGreen)
|
||||
.disabled(!allAnswered || isSubmitting)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(20)
|
||||
.shadow(radius: 20)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single yes/no rating question with thumb up/down toggle chips.
|
||||
private struct RatingQuestion: View {
|
||||
let question: String
|
||||
@Binding var value: Bool?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(question)
|
||||
.font(.subheadline.weight(.medium))
|
||||
|
||||
HStack(spacing: 8) {
|
||||
ToggleChip(
|
||||
label: "Yes",
|
||||
icon: "hand.thumbsup.fill",
|
||||
isSelected: value == true,
|
||||
selectedColor: .green
|
||||
) {
|
||||
value = true
|
||||
}
|
||||
|
||||
ToggleChip(
|
||||
label: "No",
|
||||
icon: "hand.thumbsdown.fill",
|
||||
isSelected: value == false,
|
||||
selectedColor: .red
|
||||
) {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
/// A selectable chip with icon, similar to Android FilterChip.
|
||||
private struct ToggleChip: View {
|
||||
let label: String
|
||||
let icon: String
|
||||
let isSelected: Bool
|
||||
let selectedColor: Color
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 4) {
|
||||
if isSelected {
|
||||
Image(systemName: icon)
|
||||
.font(.caption)
|
||||
.foregroundColor(selectedColor)
|
||||
}
|
||||
Text(label)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(isSelected ? selectedColor : .primary)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
isSelected
|
||||
? selectedColor.opacity(0.15)
|
||||
: Color(.systemGray5)
|
||||
)
|
||||
.cornerRadius(20)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(isSelected ? selectedColor.opacity(0.4) : Color.clear, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
Color.black.opacity(0.4).ignoresSafeArea()
|
||||
RatingDialog(
|
||||
onSubmit: { rating in print(rating) },
|
||||
onDismiss: { print("dismissed") },
|
||||
isSubmitting: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,10 +30,6 @@ struct TaskDetailScreen: View {
|
|||
@State private var taskAccepted = false // Track if task was just accepted
|
||||
@State private var customerAvatarUrl: String? // Fetched separately if not in task details
|
||||
|
||||
// Rating dialog
|
||||
@State private var showRatingDialog = false
|
||||
@State private var isSubmittingRating = false
|
||||
|
||||
// Computed properties for effective button visibility after accepting
|
||||
private var effectiveShowAcceptButton: Bool { showAcceptButton && !taskAccepted }
|
||||
private var effectiveShowCompleteButton: Bool { showCompleteButton || taskAccepted }
|
||||
|
|
@ -93,10 +89,6 @@ struct TaskDetailScreen: View {
|
|||
showAutoCompleteDialog = false
|
||||
if result == "success" {
|
||||
appState.popToRoot()
|
||||
} else if result == "rating_required" {
|
||||
autoCompleting = false
|
||||
beaconDetected = false
|
||||
showRatingDialog = true
|
||||
} else if result == "cancelled" || result == "error" {
|
||||
autoCompleting = false
|
||||
beaconDetected = false
|
||||
|
|
@ -123,25 +115,6 @@ struct TaskDetailScreen: View {
|
|||
onComplete: { appState.popToRoot() }
|
||||
)
|
||||
}
|
||||
.overlay {
|
||||
if showRatingDialog {
|
||||
ZStack {
|
||||
Color.black.opacity(0.4)
|
||||
.ignoresSafeArea()
|
||||
.onTapGesture {
|
||||
if !isSubmittingRating { showRatingDialog = false }
|
||||
}
|
||||
|
||||
RatingDialog(
|
||||
onSubmit: { rating in submitRating(rating) },
|
||||
onDismiss: { showRatingDialog = false },
|
||||
isSubmitting: isSubmittingRating
|
||||
)
|
||||
}
|
||||
.transition(.opacity)
|
||||
.animation(.easeInOut(duration: 0.2), value: showRatingDialog)
|
||||
}
|
||||
}
|
||||
.task { await loadDetails() }
|
||||
.onDisappear { beaconScanner?.dispose() }
|
||||
.onChange(of: appState.shouldPopToTaskList) { shouldPop in
|
||||
|
|
@ -764,34 +737,12 @@ struct TaskDetailScreen: View {
|
|||
do {
|
||||
try await APIService.shared.completeTask(taskId: task.taskId)
|
||||
appState.popToRoot()
|
||||
} catch let apiError as APIError {
|
||||
if case .ratingRequired = apiError {
|
||||
showRatingDialog = true
|
||||
} else {
|
||||
self.error = apiError.localizedDescription
|
||||
}
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func submitRating(_ rating: [String: Bool]) {
|
||||
isSubmittingRating = true
|
||||
Task {
|
||||
do {
|
||||
try await APIService.shared.completeTask(taskId: task.taskId, workerRating: rating)
|
||||
showRatingDialog = false
|
||||
isSubmittingRating = false
|
||||
appState.popToRoot()
|
||||
} catch {
|
||||
isSubmittingRating = false
|
||||
showRatingDialog = false
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelOrder() {
|
||||
isCancelingOrder = true
|
||||
Task {
|
||||
|
|
@ -1070,12 +1021,6 @@ struct AutoCompleteCountdownView: View {
|
|||
message = "Closing this window now"
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
onResult("success")
|
||||
} catch let apiError as APIError {
|
||||
if case .ratingRequired = apiError {
|
||||
onResult("rating_required")
|
||||
} else {
|
||||
onResult("error")
|
||||
}
|
||||
} catch {
|
||||
onResult("error")
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue