Add Cancel Order feature for cash tasks
- Add cancelOrder parameter to completeTask API method - Add Cancel Order button below Collect Cash in task detail - Show confirmation alert before canceling - On confirm, call complete endpoint with CancelOrder: true Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d69270a4af
commit
4a2f984c94
2 changed files with 149 additions and 29 deletions
|
|
@ -472,7 +472,7 @@ actor APIService {
|
|||
return arr.map { WorkTask(json: $0) }
|
||||
}
|
||||
|
||||
func completeTask(taskId: Int, cashReceivedCents: Int? = nil) async throws {
|
||||
func completeTask(taskId: Int, cashReceivedCents: Int? = nil, cancelOrder: Bool = false) async throws {
|
||||
var payload: [String: Any] = [
|
||||
"TaskID": taskId,
|
||||
"UserID": userId ?? 0
|
||||
|
|
@ -480,6 +480,9 @@ actor APIService {
|
|||
if let cents = cashReceivedCents {
|
||||
payload["CashReceivedCents"] = cents
|
||||
}
|
||||
if cancelOrder {
|
||||
payload["CancelOrder"] = true
|
||||
}
|
||||
let json = try await postJSON("/tasks/complete.cfm", payload: payload)
|
||||
guard ok(json) else {
|
||||
throw APIError.serverError("Failed to complete task: \(err(json))")
|
||||
|
|
@ -768,6 +771,50 @@ actor APIService {
|
|||
return String(raw.prefix(2000))
|
||||
}
|
||||
|
||||
// MARK: - About Info
|
||||
|
||||
func getAboutInfo() async throws -> AboutInfo {
|
||||
let json = try await getJSON("app/about.cfm")
|
||||
|
||||
guard ok(json) else {
|
||||
throw APIError.serverError(err(json))
|
||||
}
|
||||
|
||||
let description = parseString(json["DESCRIPTION"] ?? json["description"])
|
||||
let copyright = parseString(json["COPYRIGHT"] ?? json["copyright"])
|
||||
|
||||
// Parse features
|
||||
var features: [AboutFeature] = []
|
||||
if let featuresArray = (json["FEATURES"] ?? json["features"]) as? [[String: Any]] {
|
||||
for f in featuresArray {
|
||||
features.append(AboutFeature(
|
||||
icon: parseString(f["ICON"] ?? f["icon"]),
|
||||
title: parseString(f["TITLE"] ?? f["title"]),
|
||||
description: parseString(f["DESCRIPTION"] ?? f["description"])
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse contacts
|
||||
var contacts: [AboutContact] = []
|
||||
if let contactsArray = (json["CONTACTS"] ?? json["contacts"]) as? [[String: Any]] {
|
||||
for c in contactsArray {
|
||||
contacts.append(AboutContact(
|
||||
icon: parseString(c["ICON"] ?? c["icon"]),
|
||||
label: parseString(c["LABEL"] ?? c["label"]),
|
||||
url: parseString(c["URL"] ?? c["url"])
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return AboutInfo(
|
||||
description: description,
|
||||
features: features,
|
||||
contacts: contacts,
|
||||
copyright: copyright
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - URL Helpers
|
||||
|
||||
/// Resolve a photo URL — if it starts with "/" prepend the base domain
|
||||
|
|
@ -855,4 +902,37 @@ actor APIService {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - String Parsing Helper
|
||||
|
||||
/// Parse any value to a string safely
|
||||
private func parseString(_ value: Any?) -> String {
|
||||
guard let v = value else { return "" }
|
||||
if let s = v as? String { return s }
|
||||
if let n = v as? NSNumber { return n.stringValue }
|
||||
if let i = v as? Int { return String(i) }
|
||||
if let d = v as? Double { return String(d) }
|
||||
return String(describing: v)
|
||||
}
|
||||
|
||||
// MARK: - About Info Models
|
||||
|
||||
struct AboutInfo {
|
||||
let description: String
|
||||
let features: [AboutFeature]
|
||||
let contacts: [AboutContact]
|
||||
let copyright: String
|
||||
}
|
||||
|
||||
struct AboutFeature {
|
||||
let icon: String
|
||||
let title: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
struct AboutContact {
|
||||
let icon: String
|
||||
let label: String
|
||||
let url: String
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ struct TaskDetailScreen: View {
|
|||
@State private var showCashDialog = false
|
||||
@State private var cashReceived = ""
|
||||
@State private var cashError: String?
|
||||
@State private var showCancelOrderAlert = false
|
||||
@State private var isCancelingOrder = false
|
||||
@State private var taskAccepted = false // Track if task was just accepted
|
||||
@State private var customerAvatarUrl: String? // Fetched separately if not in task details
|
||||
|
||||
|
|
@ -76,6 +78,12 @@ struct TaskDetailScreen: View {
|
|||
} message: {
|
||||
Text("Mark this task as completed?")
|
||||
}
|
||||
.alert("Cancel Order?", isPresented: $showCancelOrderAlert) {
|
||||
Button("Go Back", role: .cancel) { }
|
||||
Button("Cancel Order", role: .destructive) { cancelOrder() }
|
||||
} message: {
|
||||
Text("The customer will not be charged. This cannot be undone.")
|
||||
}
|
||||
.sheet(isPresented: $showAutoCompleteDialog) {
|
||||
AutoCompleteCountdownView(taskId: task.taskId) { result in
|
||||
showAutoCompleteDialog = false
|
||||
|
|
@ -182,21 +190,21 @@ struct TaskDetailScreen: View {
|
|||
HStack(spacing: 12) {
|
||||
Image(systemName: "dollarsign.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
.foregroundColor(.black)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Cash Payment Due")
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.foregroundColor(.black.opacity(0.7))
|
||||
Text(String(format: "$%.2f", Double(orderTotalCents) / 100))
|
||||
.font(.title2.bold())
|
||||
.foregroundColor(.white)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color(red: 0.13, green: 0.55, blue: 0.13))
|
||||
.background(Color.payfritGreen)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
|
|
@ -541,35 +549,54 @@ struct TaskDetailScreen: View {
|
|||
// MARK: - Bottom Bar
|
||||
|
||||
private var bottomBar: some View {
|
||||
HStack {
|
||||
if effectiveShowAcceptButton {
|
||||
Button { showAcceptAlert = true } label: {
|
||||
Label("Accept Task", systemImage: "plus.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
if effectiveShowAcceptButton {
|
||||
Button { showAcceptAlert = true } label: {
|
||||
Label("Accept Task", systemImage: "plus.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.payfritGreen)
|
||||
}
|
||||
|
||||
if effectiveShowCompleteButton {
|
||||
if isCashTask && orderTotalCents > 0 {
|
||||
Button { showCashDialog = true } label: {
|
||||
Label("Collect Cash", systemImage: "dollarsign.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(Color(red: 0.13, green: 0.55, blue: 0.13))
|
||||
} else {
|
||||
Button { showCompleteAlert = true } label: {
|
||||
Label("Complete Task", systemImage: "checkmark.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.payfritGreen)
|
||||
}
|
||||
|
||||
if effectiveShowCompleteButton {
|
||||
if isCashTask && orderTotalCents > 0 {
|
||||
Button { showCashDialog = true } label: {
|
||||
Label("Collect Cash", systemImage: "dollarsign.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
// Cancel Order button for cash tasks
|
||||
if effectiveShowCompleteButton && isCashTask && orderTotalCents > 0 {
|
||||
Button {
|
||||
showCancelOrderAlert = true
|
||||
} label: {
|
||||
if isCancelingOrder {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .red))
|
||||
} else {
|
||||
Label("Cancel Order", systemImage: "xmark.circle")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(Color(red: 0.13, green: 0.55, blue: 0.13))
|
||||
} else {
|
||||
Button { showCompleteAlert = true } label: {
|
||||
Label("Complete Task", systemImage: "checkmark.circle.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.green)
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.disabled(isCancelingOrder)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
|
@ -683,6 +710,19 @@ struct TaskDetailScreen: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func cancelOrder() {
|
||||
isCancelingOrder = true
|
||||
Task {
|
||||
do {
|
||||
try await APIService.shared.completeTask(taskId: task.taskId, cancelOrder: true)
|
||||
appState.popToRoot()
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
isCancelingOrder = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func callCustomer(_ phone: String) {
|
||||
guard let url = URL(string: "tel:\(phone)") else { return }
|
||||
#if os(iOS)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue