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) }
|
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] = [
|
var payload: [String: Any] = [
|
||||||
"TaskID": taskId,
|
"TaskID": taskId,
|
||||||
"UserID": userId ?? 0
|
"UserID": userId ?? 0
|
||||||
|
|
@ -480,6 +480,9 @@ actor APIService {
|
||||||
if let cents = cashReceivedCents {
|
if let cents = cashReceivedCents {
|
||||||
payload["CashReceivedCents"] = cents
|
payload["CashReceivedCents"] = cents
|
||||||
}
|
}
|
||||||
|
if cancelOrder {
|
||||||
|
payload["CancelOrder"] = true
|
||||||
|
}
|
||||||
let json = try await postJSON("/tasks/complete.cfm", payload: payload)
|
let json = try await postJSON("/tasks/complete.cfm", payload: payload)
|
||||||
guard ok(json) else {
|
guard ok(json) else {
|
||||||
throw APIError.serverError("Failed to complete task: \(err(json))")
|
throw APIError.serverError("Failed to complete task: \(err(json))")
|
||||||
|
|
@ -768,6 +771,50 @@ actor APIService {
|
||||||
return String(raw.prefix(2000))
|
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
|
// MARK: - URL Helpers
|
||||||
|
|
||||||
/// Resolve a photo URL — if it starts with "/" prepend the base domain
|
/// 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 showCashDialog = false
|
||||||
@State private var cashReceived = ""
|
@State private var cashReceived = ""
|
||||||
@State private var cashError: String?
|
@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 taskAccepted = false // Track if task was just accepted
|
||||||
@State private var customerAvatarUrl: String? // Fetched separately if not in task details
|
@State private var customerAvatarUrl: String? // Fetched separately if not in task details
|
||||||
|
|
||||||
|
|
@ -76,6 +78,12 @@ struct TaskDetailScreen: View {
|
||||||
} message: {
|
} message: {
|
||||||
Text("Mark this task as completed?")
|
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) {
|
.sheet(isPresented: $showAutoCompleteDialog) {
|
||||||
AutoCompleteCountdownView(taskId: task.taskId) { result in
|
AutoCompleteCountdownView(taskId: task.taskId) { result in
|
||||||
showAutoCompleteDialog = false
|
showAutoCompleteDialog = false
|
||||||
|
|
@ -182,21 +190,21 @@ struct TaskDetailScreen: View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "dollarsign.circle.fill")
|
Image(systemName: "dollarsign.circle.fill")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.black)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Cash Payment Due")
|
Text("Cash Payment Due")
|
||||||
.font(.caption.weight(.medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(.white.opacity(0.9))
|
.foregroundColor(.black.opacity(0.7))
|
||||||
Text(String(format: "$%.2f", Double(orderTotalCents) / 100))
|
Text(String(format: "$%.2f", Double(orderTotalCents) / 100))
|
||||||
.font(.title2.bold())
|
.font(.title2.bold())
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.black)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.background(Color(red: 0.13, green: 0.55, blue: 0.13))
|
.background(Color.payfritGreen)
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,6 +549,7 @@ struct TaskDetailScreen: View {
|
||||||
// MARK: - Bottom Bar
|
// MARK: - Bottom Bar
|
||||||
|
|
||||||
private var bottomBar: some View {
|
private var bottomBar: some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
HStack {
|
HStack {
|
||||||
if effectiveShowAcceptButton {
|
if effectiveShowAcceptButton {
|
||||||
Button { showAcceptAlert = true } label: {
|
Button { showAcceptAlert = true } label: {
|
||||||
|
|
@ -572,6 +581,24 @@ struct TaskDetailScreen: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.disabled(isCancelingOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.background(.ultraThinMaterial)
|
.background(.ultraThinMaterial)
|
||||||
|
|
@ -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) {
|
private func callCustomer(_ phone: String) {
|
||||||
guard let url = URL(string: "tel:\(phone)") else { return }
|
guard let url = URL(string: "tel:\(phone)") else { return }
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue