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:
parent
c71b9f7dea
commit
d69270a4af
5 changed files with 62 additions and 3 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) ?? ""
|
||||||
|
|
|
||||||
|
|
@ -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] {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue