From d69270a4af1d0631dc5e41d85775704d98f4e9df Mon Sep 17 00:00:00 2001 From: John Pinkyfloyd Date: Mon, 16 Feb 2026 14:07:24 -0800 Subject: [PATCH] iOS parity fixes from Android comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add profile endpoints (getProfile/updateProfile) to APIService - Fix beacon dwell time: 5 → 30 samples to match Android (~3s) - Make chat WebSocket dev-aware (uses IS_DEV flag) - Add activeTaskCount field to Employment model - Fix categoryName: remove incorrect fallback to taskTypeName Co-Authored-By: Claude Opus 4.5 --- PayfritWorks/Models/Employment.swift | 2 + PayfritWorks/Models/Task.swift | 3 +- PayfritWorks/Services/APIService.swift | 53 +++++++++++++++++++++++ PayfritWorks/Services/BeaconScanner.swift | 2 +- PayfritWorks/Services/ChatService.swift | 5 ++- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/PayfritWorks/Models/Employment.swift b/PayfritWorks/Models/Employment.swift index 89fa1df..cbb17ee 100644 --- a/PayfritWorks/Models/Employment.swift +++ b/PayfritWorks/Models/Employment.swift @@ -8,6 +8,7 @@ struct Employment: Identifiable { let businessCity: String let employeeStatusId: Int let pendingTaskCount: Int + let activeTaskCount: Int var id: Int { employeeId } @@ -22,6 +23,7 @@ struct Employment: Identifiable { // Match Flutter: read EmployeeStatusID first (server sends StatusID, which may differ) employeeStatusId = WorkTask.parseInt(json["EmployeeStatusID"] ?? json["StatusID"]) ?? 0 pendingTaskCount = WorkTask.parseInt(json["PendingTaskCount"]) ?? 0 + activeTaskCount = WorkTask.parseInt(json["ActiveTaskCount"]) ?? 0 } var statusName: String { diff --git a/PayfritWorks/Models/Task.swift b/PayfritWorks/Models/Task.swift index bc2b480..89bbed3 100644 --- a/PayfritWorks/Models/Task.swift +++ b/PayfritWorks/Models/Task.swift @@ -36,7 +36,8 @@ struct WorkTask: Identifiable { sourceType = (json["SourceType"] as? String) ?? (json["TaskSourceType"] as? String) ?? "" sourceId = Self.parseInt(json["SourceID"] ?? json["TaskSourceID"]) ?? 0 // Server key varies: CategoryName, Name, TaskCategoryName — try all - categoryName = Self.nonEmpty(json["CategoryName"]) ?? Self.nonEmpty(json["Name"]) ?? Self.nonEmpty(json["TaskCategoryName"]) ?? Self.nonEmpty(json["TaskTypeName"]) ?? "Uncategorized" + // NOTE: Do NOT fallback to TaskTypeName as it's a different field + categoryName = Self.nonEmpty(json["CategoryName"]) ?? Self.nonEmpty(json["Name"]) ?? Self.nonEmpty(json["TaskCategoryName"]) ?? "Uncategorized" // Category color as fallback categoryColor = Self.nonEmpty(json["CategoryColor"]) ?? Self.nonEmpty(json["Color"]) ?? Self.nonEmpty(json["TaskCategoryColor"]) ?? "#888888" taskTypeName = (json["TaskTypeName"] as? String) ?? "" diff --git a/PayfritWorks/Services/APIService.swift b/PayfritWorks/Services/APIService.swift index ea5c473..4c4d82b 100644 --- a/PayfritWorks/Services/APIService.swift +++ b/PayfritWorks/Services/APIService.swift @@ -347,6 +347,59 @@ actor APIService { businessId = 0 } + // MARK: - Profile + + struct UserProfile { + let userId: Int + let firstName: String + let lastName: String + let email: String + let phone: String + let photoUrl: String + } + + func getProfile() async throws -> UserProfile { + guard let uid = userId, uid > 0 else { + throw APIError.serverError("User not logged in") + } + + let json = try await postJSON("/auth/profile.cfm", payload: [ + "UserID": uid + ]) + + guard ok(json) else { + throw APIError.serverError("Failed to load profile: \(err(json))") + } + + let data = json["DATA"] as? [String: Any] ?? json + return UserProfile( + userId: (data["UserID"] as? Int) ?? (data["USERID"] as? Int) ?? uid, + firstName: (data["FirstName"] as? String) ?? (data["FIRSTNAME"] as? String) ?? "", + lastName: (data["LastName"] as? String) ?? (data["LASTNAME"] as? String) ?? "", + email: (data["Email"] as? String) ?? (data["EMAIL"] as? String) ?? (data["EmailAddress"] as? String) ?? "", + phone: (data["Phone"] as? String) ?? (data["PHONE"] as? String) ?? (data["ContactNumber"] as? String) ?? "", + photoUrl: Self.resolvePhotoUrl((data["PhotoUrl"] as? String) ?? (data["PHOTOURL"] as? String) ?? "") + ) + } + + func updateProfile(firstName: String? = nil, lastName: String? = nil, email: String? = nil, phone: String? = nil) async throws { + guard let uid = userId, uid > 0 else { + throw APIError.serverError("User not logged in") + } + + var payload: [String: Any] = ["UserID": uid] + if let fn = firstName { payload["FirstName"] = fn } + if let ln = lastName { payload["LastName"] = ln } + if let em = email { payload["Email"] = em } + if let ph = phone { payload["Phone"] = ph } + + let json = try await postJSON("/auth/profile.cfm", payload: payload) + + guard ok(json) else { + throw APIError.serverError("Failed to update profile: \(err(json))") + } + } + // MARK: - Businesses func getMyBusinesses() async throws -> [Employment] { diff --git a/PayfritWorks/Services/BeaconScanner.swift b/PayfritWorks/Services/BeaconScanner.swift index ca9b02d..8679784 100644 --- a/PayfritWorks/Services/BeaconScanner.swift +++ b/PayfritWorks/Services/BeaconScanner.swift @@ -18,7 +18,7 @@ final class BeaconScanner: NSObject, ObservableObject { // RSSI samples for dwell time enforcement private var rssiSamples: [Int] = [] - private let minSamplesToConfirm = 5 // ~5 seconds at 1Hz ranging + private let minSamplesToConfirm = 30 // ~3 seconds to match Android (30 samples at ~100ms) private let rssiThreshold = -75 // Track resolved beacons to avoid repeated API calls diff --git a/PayfritWorks/Services/ChatService.swift b/PayfritWorks/Services/ChatService.swift index 0984862..eb82992 100644 --- a/PayfritWorks/Services/ChatService.swift +++ b/PayfritWorks/Services/ChatService.swift @@ -28,7 +28,10 @@ struct TypingEvent { @MainActor final class ChatService: ObservableObject { - private static let wsBaseURL = "wss://app.payfrit.com:3001" + // Dev-aware WebSocket URL - matches API dev/prod switching + private static var wsBaseURL: String { + IS_DEV ? "wss://dev.payfrit.com:3001" : "wss://app.payfrit.com:3001" + } @Published var isConnected = false @Published var chatClosed = false