iOS parity fixes from Android comparison

- 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 <noreply@anthropic.com>
This commit is contained in:
John Pinkyfloyd 2026-02-16 14:07:24 -08:00
parent c71b9f7dea
commit d69270a4af
5 changed files with 62 additions and 3 deletions

View file

@ -8,6 +8,7 @@ struct Employment: Identifiable {
let businessCity: String let businessCity: String
let employeeStatusId: Int let employeeStatusId: Int
let pendingTaskCount: Int let pendingTaskCount: Int
let activeTaskCount: Int
var id: Int { employeeId } var id: Int { employeeId }
@ -22,6 +23,7 @@ struct Employment: Identifiable {
// Match Flutter: read EmployeeStatusID first (server sends StatusID, which may differ) // Match Flutter: read EmployeeStatusID first (server sends StatusID, which may differ)
employeeStatusId = WorkTask.parseInt(json["EmployeeStatusID"] ?? json["StatusID"]) ?? 0 employeeStatusId = WorkTask.parseInt(json["EmployeeStatusID"] ?? json["StatusID"]) ?? 0
pendingTaskCount = WorkTask.parseInt(json["PendingTaskCount"]) ?? 0 pendingTaskCount = WorkTask.parseInt(json["PendingTaskCount"]) ?? 0
activeTaskCount = WorkTask.parseInt(json["ActiveTaskCount"]) ?? 0
} }
var statusName: String { var statusName: String {

View file

@ -36,7 +36,8 @@ struct WorkTask: Identifiable {
sourceType = (json["SourceType"] as? String) ?? (json["TaskSourceType"] as? String) ?? "" sourceType = (json["SourceType"] as? String) ?? (json["TaskSourceType"] as? String) ?? ""
sourceId = Self.parseInt(json["SourceID"] ?? json["TaskSourceID"]) ?? 0 sourceId = Self.parseInt(json["SourceID"] ?? json["TaskSourceID"]) ?? 0
// Server key varies: CategoryName, Name, TaskCategoryName try all // 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 // Category color as fallback
categoryColor = Self.nonEmpty(json["CategoryColor"]) ?? Self.nonEmpty(json["Color"]) ?? Self.nonEmpty(json["TaskCategoryColor"]) ?? "#888888" categoryColor = Self.nonEmpty(json["CategoryColor"]) ?? Self.nonEmpty(json["Color"]) ?? Self.nonEmpty(json["TaskCategoryColor"]) ?? "#888888"
taskTypeName = (json["TaskTypeName"] as? String) ?? "" taskTypeName = (json["TaskTypeName"] as? String) ?? ""

View file

@ -347,6 +347,59 @@ actor APIService {
businessId = 0 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 // MARK: - Businesses
func getMyBusinesses() async throws -> [Employment] { func getMyBusinesses() async throws -> [Employment] {

View file

@ -18,7 +18,7 @@ final class BeaconScanner: NSObject, ObservableObject {
// RSSI samples for dwell time enforcement // RSSI samples for dwell time enforcement
private var rssiSamples: [Int] = [] 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 private let rssiThreshold = -75
// Track resolved beacons to avoid repeated API calls // Track resolved beacons to avoid repeated API calls

View file

@ -28,7 +28,10 @@ struct TypingEvent {
@MainActor @MainActor
final class ChatService: ObservableObject { 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 isConnected = false
@Published var chatClosed = false @Published var chatClosed = false