diff --git a/PayfritWorks/Services/APIService.swift b/PayfritWorks/Services/APIService.swift index 4c4d82b..68dc29d 100644 --- a/PayfritWorks/Services/APIService.swift +++ b/PayfritWorks/Services/APIService.swift @@ -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 +} + diff --git a/PayfritWorks/Views/TaskDetailScreen.swift b/PayfritWorks/Views/TaskDetailScreen.swift index 0d9a950..1cfaebd 100644 --- a/PayfritWorks/Views/TaskDetailScreen.swift +++ b/PayfritWorks/Views/TaskDetailScreen.swift @@ -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)