import "dart:convert"; import "package:http/http.dart" as http; import "../models/cart.dart"; import "../models/chat_message.dart"; import "../models/menu_item.dart"; import "../models/order_detail.dart"; import "../models/order_history.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; import "../models/user_profile.dart"; import "auth_storage.dart"; class ApiRawResponse { final int statusCode; final String rawBody; final Map? json; const ApiRawResponse({ required this.statusCode, required this.rawBody, required this.json, }); } class LoginResponse { final int userId; final String userFirstName; final String token; const LoginResponse({ required this.userId, required this.userFirstName, required this.token, }); factory LoginResponse.fromJson(Map json) { return LoginResponse( userId: (json["UserID"] as num).toInt(), userFirstName: (json["UserFirstName"] as String?) ?? "", token: (json["Token"] as String?) ?? "", ); } } class SendOtpResponse { final String uuid; final String message; const SendOtpResponse({ required this.uuid, required this.message, }); factory SendOtpResponse.fromJson(Map json) { // Try both uppercase and lowercase keys for compatibility final uuid = (json["UUID"] as String?) ?? (json["uuid"] as String?) ?? ""; final message = (json["MESSAGE"] as String?) ?? (json["message"] as String?) ?? ""; return SendOtpResponse( uuid: uuid, message: message, ); } } class UserSearchResult { final int userId; final String name; final String phone; final String email; final String avatarUrl; const UserSearchResult({ required this.userId, required this.name, required this.phone, required this.email, required this.avatarUrl, }); } class VerifyOtpResponse { final int userId; final String token; final bool needsProfile; final String userFirstName; final bool isEmailVerified; const VerifyOtpResponse({ required this.userId, required this.token, required this.needsProfile, required this.userFirstName, required this.isEmailVerified, }); factory VerifyOtpResponse.fromJson(Map json) { return VerifyOtpResponse( userId: (json["UserID"] as num).toInt(), token: (json["Token"] as String?) ?? "", needsProfile: (json["NeedsProfile"] as bool?) ?? true, userFirstName: (json["UserFirstName"] as String?) ?? "", isEmailVerified: (json["IsEmailVerified"] as bool?) ?? false, ); } } class Api { static String? _userToken; // MVP hardcode static int _mvpBusinessId = 17; static void setAuthToken(String? token) => _userToken = token; static void clearAuthToken() => _userToken = null; static void setBusinessId(int? businessId) { if (businessId != null && businessId > 0) { _mvpBusinessId = businessId; } } static void clearCookies() { // no-op } static String get baseUrl { const v = String.fromEnvironment("PAYFRIT_API_BASE_URL"); if (v.isEmpty) { return "https://biz.payfrit.com/api"; } return v; } static Uri _u(String path) { final b = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl; final p = path.startsWith("/") ? path : "/$path"; return Uri.parse("$b$p"); } static Map _headers({required bool json, int? businessIdOverride}) { final h = {}; if (json) h["Content-Type"] = "application/json; charset=utf-8"; final tok = _userToken; if (tok != null && tok.isNotEmpty) { h["X-User-Token"] = tok; } final int bid = (businessIdOverride != null && businessIdOverride > 0) ? businessIdOverride : _mvpBusinessId; h["X-Business-ID"] = bid.toString(); return h; } static Map? _tryDecodeJsonMap(String body) { try { final decoded = jsonDecode(body); if (decoded is Map) return decoded; } catch (_) { // If JSON parsing fails, try to extract JSON from the body // (handles cases where debug output is appended after JSON) final jsonStart = body.indexOf('{'); final jsonEnd = body.lastIndexOf('}'); if (jsonStart >= 0 && jsonEnd > jsonStart) { try { final jsonPart = body.substring(jsonStart, jsonEnd + 1); final decoded = jsonDecode(jsonPart); if (decoded is Map) return decoded; } catch (_) {} } } return null; } static Future _getRaw(String path, {int? businessIdOverride}) async { final url = _u(path); final resp = await http.get(url, headers: _headers(json: false, businessIdOverride: businessIdOverride)); final body = resp.body; final j = _tryDecodeJsonMap(body); return ApiRawResponse(statusCode: resp.statusCode, rawBody: body, json: j); } static Future _postRaw( String path, Map payload, { int? businessIdOverride, }) async { final url = _u(path); final resp = await http.post( url, headers: _headers(json: true, businessIdOverride: businessIdOverride), body: jsonEncode(payload), ); final body = resp.body; final j = _tryDecodeJsonMap(body); return ApiRawResponse(statusCode: resp.statusCode, rawBody: body, json: j); } static bool _ok(Map j) => j["OK"] == true || j["ok"] == true; static String _err(Map j) => (j["ERROR"] ?? j["error"] ?? "").toString(); static List? _pickArray(Map j, List keys) { for (final k in keys) { final v = j[k]; if (v is List) return v; } return null; } static Map _requireJson(ApiRawResponse raw, String label) { final j = raw.json; if (j == null) { throw StateError("$label request failed: ${raw.statusCode}\nNon-JSON response."); } return j; } // ------------------------- // Authentication // ------------------------- static Future login({ required String username, required String password, }) async { final raw = await _postRaw( "/auth/login.cfm", { "username": username, "password": password, }, ); final j = _requireJson(raw, "Login"); if (!_ok(j)) { final err = _err(j); if (err == "bad_credentials") { throw StateError("Invalid email/phone or password"); } else if (err == "missing_fields") { throw StateError("Username and password are required"); } else { throw StateError("Login failed: $err"); } } final response = LoginResponse.fromJson(j); // Store token for future requests setAuthToken(response.token); return response; } static Future logout() async { setAuthToken(null); clearCookies(); await AuthStorage.clearAuth(); } // ------------------------- // Signup / OTP Verification // ------------------------- /// Send OTP to phone number for signup /// Returns UUID to use in verifyOtp static Future sendOtp({required String phone}) async { final raw = await _postRaw("/auth/sendOTP.cfm", {"phone": phone}); final j = _requireJson(raw, "SendOTP"); if (!_ok(j)) { final err = _err(j); if (err == "phone_exists") { throw StateError("This phone number already has an account. Please login instead."); } else if (err == "invalid_phone") { throw StateError("Please enter a valid 10-digit phone number"); } else { throw StateError("Failed to send code: ${j["MESSAGE"] ?? err}"); } } return SendOtpResponse.fromJson(j); } /// Verify OTP and get auth token /// If needsProfile is true, call completeProfile next static Future verifyOtp({ required String uuid, required String otp, }) async { final raw = await _postRaw("/auth/verifyOTP.cfm", { "uuid": uuid, "otp": otp, }); final j = _requireJson(raw, "VerifyOTP"); if (!_ok(j)) { final err = _err(j); if (err == "invalid_otp") { throw StateError("Invalid verification code. Please try again."); } else if (err == "expired") { throw StateError("Verification expired. Please request a new code."); } else { throw StateError("Verification failed: ${j["MESSAGE"] ?? err}"); } } final response = VerifyOtpResponse.fromJson(j); // Store token for future requests setAuthToken(response.token); return response; } /// Complete user profile after phone verification static Future completeProfile({ required String firstName, required String lastName, required String email, }) async { print('[API] completeProfile: token=${_userToken?.substring(0, 8) ?? "NULL"}...'); final raw = await _postRaw("/auth/completeProfile.cfm", { "firstName": firstName, "lastName": lastName, "email": email, }); print('[API] completeProfile response: ${raw.statusCode} - ${raw.rawBody}'); final j = _requireJson(raw, "CompleteProfile"); if (!_ok(j)) { final err = _err(j); if (err == "email_exists") { throw StateError("This email is already associated with another account"); } else if (err == "invalid_email") { throw StateError("Please enter a valid email address"); } else if (err == "unauthorized") { throw StateError("Authentication failed - please try signing up again"); } else { throw StateError("Failed to save profile: ${j["MESSAGE"] ?? err}"); } } } /// Resend OTP to the same phone (uses existing UUID) static Future resendOtp({required String phone}) async { return sendOtp(phone: phone); } // ------------------------- // Login via OTP (for existing verified accounts) // ------------------------- /// Send OTP to phone number for LOGIN (existing accounts only) static Future sendLoginOtp({required String phone}) async { final raw = await _postRaw("/auth/loginOTP.cfm", {"phone": phone}); final j = _requireJson(raw, "LoginOTP"); if (!_ok(j)) { final err = _err(j); if (err == "no_account") { throw StateError("No account found with this phone number. Please sign up first."); } else if (err == "invalid_phone") { throw StateError("Please enter a valid 10-digit phone number"); } else { throw StateError("Failed to send code: ${j["MESSAGE"] ?? err}"); } } return SendOtpResponse.fromJson(j); } /// Verify OTP for LOGIN and get auth token static Future verifyLoginOtp({ required String uuid, required String otp, }) async { final raw = await _postRaw("/auth/verifyLoginOTP.cfm", { "uuid": uuid, "otp": otp, }); final j = _requireJson(raw, "VerifyLoginOTP"); if (!_ok(j)) { final err = _err(j); if (err == "invalid_otp") { throw StateError("Invalid code. Please try again."); } else if (err == "expired") { throw StateError("Session expired. Please request a new code."); } else { throw StateError("Login failed: ${j["MESSAGE"] ?? err}"); } } final response = LoginResponse.fromJson(j); // Store token for future requests setAuthToken(response.token); return response; } // ------------------------- // Businesses (legacy model name: Restaurant) // ------------------------- static Future listRestaurantsRaw() async { return _getRaw("/businesses/list.cfm", businessIdOverride: _mvpBusinessId); } static Future> listRestaurants() async { final raw = await listRestaurantsRaw(); final j = _requireJson(raw, "Businesses"); if (!_ok(j)) { throw StateError( "Businesses API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["Businesses", "BUSINESSES"]); if (arr == null) { throw StateError("Businesses JSON missing Businesses array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(Restaurant.fromJson(e)); } else if (e is Map) { out.add(Restaurant.fromJson(e.cast())); } } return out; } // ------------------------- // Service Points // ------------------------- static Future> listServicePoints({required int businessId}) async { // CRITICAL: endpoint is behaving like it reads JSON body, not query/header. final raw = await _postRaw( "/servicepoints/list.cfm", {"BusinessID": businessId}, businessIdOverride: businessId, ); final j = _requireJson(raw, "ServicePoints"); if (!_ok(j)) { throw StateError( "ServicePoints API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["ServicePoints", "SERVICEPOINTS"]); if (arr == null) { throw StateError("ServicePoints JSON missing ServicePoints array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(ServicePoint.fromJson(e)); } else if (e is Map) { out.add(ServicePoint.fromJson(e.cast())); } } return out; } // ------------------------- // Menu Items // ------------------------- static Future> listMenuItems({required int businessId}) async { final raw = await _postRaw( "/menu/items.cfm", {"BusinessID": businessId}, businessIdOverride: businessId, ); final j = _requireJson(raw, "MenuItems"); if (!_ok(j)) { throw StateError( "MenuItems API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["DETAIL"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["Items", "ITEMS"]); if (arr == null) { throw StateError("MenuItems JSON missing Items array.\nRaw: ${raw.rawBody}"); } final out = []; for (final e in arr) { if (e is Map) { out.add(MenuItem.fromJson(e)); } else if (e is Map) { out.add(MenuItem.fromJson(e.cast())); } } return out; } // ------------------------- // Cart & Orders // ------------------------- static Future getOrCreateCart({ required int userId, required int businessId, required int servicePointId, required int orderTypeId, }) async { final raw = await _postRaw( "/orders/getOrCreateCart.cfm", { "OrderUserID": userId, "BusinessID": businessId, "OrderServicePointID": servicePointId, "OrderTypeID": orderTypeId, }, businessIdOverride: businessId, ); final j = _requireJson(raw, "GetOrCreateCart"); if (!_ok(j)) { throw StateError( "GetOrCreateCart API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future getCart({required int orderId}) async { final raw = await _postRaw( "/orders/getCart.cfm", {"OrderID": orderId}, ); final j = _requireJson(raw, "GetCart"); if (!_ok(j)) { throw StateError( "GetCart API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } return Cart.fromJson(j); } static Future setLineItem({ required int orderId, required int parentOrderLineItemId, required int itemId, required bool isSelected, int quantity = 1, String? remark, }) async { final raw = await _postRaw( "/orders/setLineItem.cfm", { "OrderID": orderId, "ParentOrderLineItemID": parentOrderLineItemId, "ItemID": itemId, "IsSelected": isSelected, "Quantity": quantity, if (remark != null && remark.isNotEmpty) "Remark": remark, }, ); final j = _requireJson(raw, "SetLineItem"); print('[API] setLineItem response: OK=${j["OK"]}, ERROR=${_err(j)}, orderId=$orderId, itemId=$itemId, parentLI=$parentOrderLineItemId'); if (!_ok(j)) { throw StateError( "SetLineItem failed: ${_err(j)} - ${(j["MESSAGE"] ?? "").toString()}", ); } return Cart.fromJson(j); } /// Set the order type (delivery/takeaway) on an existing cart static Future setOrderType({ required int orderId, required int orderTypeId, int? addressId, }) async { final raw = await _postRaw( "/orders/setOrderType.cfm", { "OrderID": orderId, "OrderTypeID": orderTypeId, if (addressId != null) "AddressID": addressId, }, ); final j = _requireJson(raw, "SetOrderType"); if (!_ok(j)) { throw StateError( "SetOrderType failed: ${_err(j)} - ${(j["MESSAGE"] ?? "").toString()}", ); } return Cart.fromJson(j); } static Future submitOrder({required int orderId}) async { final raw = await _postRaw( "/orders/submit.cfm", {"OrderID": orderId}, ); final j = _requireJson(raw, "SubmitOrder"); if (!_ok(j)) { throw StateError( "SubmitOrder API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } } static Future> checkOrderStatus({ required int orderId, required int lastKnownStatusId, }) async { final raw = await _postRaw( "/orders/checkStatusUpdate.cfm", { "OrderID": orderId, "LastKnownStatusID": lastKnownStatusId, }, ); final j = _requireJson(raw, "CheckOrderStatus"); if (!_ok(j)) { throw StateError( "CheckOrderStatus API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } return j; } // ------------------------- // Tasks / Service Requests // ------------------------- /// Call server to the table - creates a service request task static Future callServer({ required int businessId, required int servicePointId, int? orderId, int? userId, String? message, }) async { final body = { "BusinessID": businessId, "ServicePointID": servicePointId, }; if (orderId != null && orderId > 0) body["OrderID"] = orderId; if (userId != null && userId > 0) body["UserID"] = userId; if (message != null && message.isNotEmpty) body["Message"] = message; final raw = await _postRaw("/tasks/callServer.cfm", body); final j = _requireJson(raw, "CallServer"); if (!_ok(j)) { throw StateError( "CallServer failed: ${_err(j)} - ${(j["MESSAGE"] ?? "").toString()}", ); } } // ------------------------- // Beacons // ------------------------- static Future> listAllBeacons() async { final raw = await _getRaw("/beacons/list_all.cfm"); final j = _requireJson(raw, "ListAllBeacons"); if (!_ok(j)) { throw StateError( "ListAllBeacons API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } final arr = _pickArray(j, const ["items", "ITEMS"]); if (arr == null) return {}; final Map uuidToBeaconId = {}; for (final e in arr) { if (e is! Map) continue; final item = e is Map ? e : e.cast(); final uuid = (item["BeaconUUID"] ?? item["BEACONUUID"] ?? "").toString().trim(); final beaconId = item["BeaconID"] ?? item["BEACONID"]; if (uuid.isNotEmpty && beaconId is num) { uuidToBeaconId[uuid] = beaconId.toInt(); } } return uuidToBeaconId; } static Future getBusinessFromBeacon({ required int beaconId, }) async { final raw = await _postRaw( "/beacons/getBusinessFromBeacon.cfm", {"BeaconID": beaconId}, ); final j = _requireJson(raw, "GetBusinessFromBeacon"); if (!_ok(j)) { throw StateError( "GetBusinessFromBeacon API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", ); } final beacon = j["BEACON"] as Map? ?? {}; final business = j["BUSINESS"] as Map? ?? {}; final servicePoint = j["SERVICEPOINT"] as Map? ?? {}; return BeaconBusinessMapping( beaconId: _parseInt(beacon["BeaconID"]) ?? 0, beaconName: (beacon["BeaconName"] as String?) ?? "", businessId: _parseInt(business["BusinessID"]) ?? 0, businessName: (business["BusinessName"] as String?) ?? "", servicePointId: _parseInt(servicePoint["ServicePointID"]) ?? 0, servicePointName: (servicePoint["ServicePointName"] as String?) ?? "", ); } static int? _parseInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); if (value is String) { if (value.isEmpty) return null; return int.tryParse(value); } return null; } /// Check if user has pending orders at a business (for pickup detection) static Future> getPendingOrdersForUser({ required int userId, required int businessId, }) async { final raw = await _getRaw( "/orders/getPendingForUser.cfm?UserID=$userId&BusinessID=$businessId", ); final j = _requireJson(raw, "GetPendingOrdersForUser"); if (!_ok(j)) { return []; // Return empty list on error } final arr = _pickArray(j, const ["ORDERS", "orders"]); if (arr == null) return []; return arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return PendingOrder.fromJson(item); }).toList(); } /// Get list of states/provinces for address forms static Future> getStates() async { final raw = await _getRaw("/addresses/states.cfm"); final j = _requireJson(raw, "GetStates"); if (!_ok(j)) { return []; } final arr = _pickArray(j, const ["STATES", "states"]); if (arr == null) return []; return arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return StateInfo.fromJson(item); }).toList(); } /// Get user's delivery addresses static Future> getDeliveryAddresses() async { final raw = await _getRaw("/addresses/list.cfm"); final j = _requireJson(raw, "GetDeliveryAddresses"); if (!_ok(j)) { return []; } final arr = _pickArray(j, const ["ADDRESSES", "addresses"]); if (arr == null) return []; return arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return DeliveryAddress.fromJson(item); }).toList(); } /// Add a new delivery address static Future addDeliveryAddress({ required String line1, required String city, required int stateId, required String zipCode, String? line2, String? label, bool setAsDefault = false, }) async { final raw = await _postRaw("/addresses/add.cfm", { "Line1": line1, "City": city, "StateID": stateId, "ZIPCode": zipCode, if (line2 != null) "Line2": line2, if (label != null) "Label": label, "SetAsDefault": setAsDefault, }); final j = _requireJson(raw, "AddDeliveryAddress"); if (!_ok(j)) { throw StateError("AddDeliveryAddress failed: ${_err(j)}"); } final addressData = j["ADDRESS"] as Map? ?? {}; return DeliveryAddress.fromJson(addressData); } /// Delete a delivery address static Future deleteDeliveryAddress(int addressId) async { final raw = await _postRaw("/addresses/delete.cfm", { "AddressID": addressId, }); final j = _requireJson(raw, "DeleteDeliveryAddress"); if (!_ok(j)) { throw StateError("DeleteDeliveryAddress failed: ${_err(j)}"); } } /// Set an address as the default delivery address static Future setDefaultDeliveryAddress(int addressId) async { final raw = await _postRaw("/addresses/setDefault.cfm", { "AddressID": addressId, }); final j = _requireJson(raw, "SetDefaultDeliveryAddress"); if (!_ok(j)) { throw StateError("SetDefaultDeliveryAddress failed: ${_err(j)}"); } } /// Get user's avatar URL static Future getAvatar() async { print('[API] getAvatar: token=${_userToken != null ? "${_userToken!.substring(0, 8)}..." : "NULL"}'); final raw = await _getRaw("/auth/avatar.cfm"); print('[API] getAvatar response: ${raw.rawBody}'); final j = _requireJson(raw, "GetAvatar"); if (!_ok(j)) { return const AvatarInfo(hasAvatar: false, avatarUrl: null); } return AvatarInfo( hasAvatar: j["HAS_AVATAR"] == true, avatarUrl: j["AVATAR_URL"] as String?, ); } /// Debug: Check token status with server static Future debugCheckToken() async { try { final raw = await _getRaw("/debug/checkToken.cfm"); print('[API] debugCheckToken response: ${raw.rawBody}'); } catch (e) { print('[API] debugCheckToken error: $e'); } } /// Upload user avatar image static Future uploadAvatar(String filePath) async { // First check token status await debugCheckToken(); final uri = _u("/auth/avatar.cfm"); final request = http.MultipartRequest("POST", uri); // Add auth headers final tok = _userToken; print('[API] uploadAvatar: token=${tok != null ? "${tok.substring(0, 8)}..." : "NULL"}'); if (tok != null && tok.isNotEmpty) { request.headers["X-User-Token"] = tok; } request.headers["X-Business-ID"] = _mvpBusinessId.toString(); // Add the file request.files.add(await http.MultipartFile.fromPath("avatar", filePath)); final streamedResponse = await request.send(); final response = await http.Response.fromStream(streamedResponse); print('[API] uploadAvatar response: ${response.statusCode} - ${response.body}'); final j = _tryDecodeJsonMap(response.body); if (j == null) { throw StateError("UploadAvatar: Invalid JSON response"); } if (!_ok(j)) { throw StateError("UploadAvatar failed: ${_err(j)}"); } return j["AVATAR_URL"] as String? ?? ""; } /// Get order history for current user static Future getOrderHistory({int limit = 20, int offset = 0}) async { print('[API] getOrderHistory: token=${_userToken != null ? "${_userToken!.substring(0, 8)}..." : "NULL"}'); final raw = await _getRaw("/orders/history.cfm?limit=$limit&offset=$offset"); print('[API] getOrderHistory response: ${raw.rawBody.substring(0, raw.rawBody.length > 200 ? 200 : raw.rawBody.length)}'); final j = _requireJson(raw, "GetOrderHistory"); if (!_ok(j)) { throw StateError("GetOrderHistory failed: ${_err(j)}"); } final ordersJson = j["ORDERS"] as List? ?? []; final orders = ordersJson .map((e) => OrderHistoryItem.fromJson(e as Map)) .toList(); return OrderHistoryResponse( orders: orders, totalCount: (j["TOTAL_COUNT"] as num?)?.toInt() ?? orders.length, ); } /// Search for users by phone, email, or name (for group order invites) static Future> searchUsers({ required String query, int? currentUserId, }) async { if (query.length < 3) return []; final raw = await _postRaw("/users/search.cfm", { "Query": query, "CurrentUserID": currentUserId ?? 0, }); final j = _requireJson(raw, "SearchUsers"); if (!_ok(j)) { return []; } final usersJson = j["USERS"] as List? ?? []; return usersJson.map((e) { final user = e as Map; return UserSearchResult( userId: (user["UserID"] as num).toInt(), name: user["Name"] as String? ?? "", phone: user["Phone"] as String? ?? "", email: user["Email"] as String? ?? "", avatarUrl: user["AvatarUrl"] as String? ?? "", ); }).toList(); } /// Get user profile static Future getProfile() async { final raw = await _getRaw("/auth/profile.cfm"); final j = _requireJson(raw, "GetProfile"); if (!_ok(j)) { throw StateError("GetProfile failed: ${_err(j)}"); } final userData = j["USER"] as Map? ?? {}; return UserProfile.fromJson(userData); } /// Update user profile static Future updateProfile({String? firstName, String? lastName}) async { final body = {}; if (firstName != null) body["firstName"] = firstName; if (lastName != null) body["lastName"] = lastName; final raw = await _postRaw("/auth/profile.cfm", body); final j = _requireJson(raw, "UpdateProfile"); if (!_ok(j)) { throw StateError("UpdateProfile failed: ${_err(j)}"); } final userData = j["USER"] as Map? ?? {}; return UserProfile.fromJson(userData); } /// Get order detail by ID static Future getOrderDetail({required int orderId}) async { final raw = await _getRaw("/orders/getDetail.cfm?OrderID=$orderId"); final j = _requireJson(raw, "GetOrderDetail"); if (!_ok(j)) { throw StateError("GetOrderDetail failed: ${_err(j)}"); } final orderData = j["ORDER"] as Map? ?? {}; return OrderDetail.fromJson(orderData); } // ------------------------- // Chat // ------------------------- /// Check if there's an active chat for the service point /// Returns the task ID if found, null otherwise static Future getActiveChat({ required int businessId, required int servicePointId, }) async { final body = { "BusinessID": businessId, "ServicePointID": servicePointId, }; final raw = await _postRaw("/chat/getActiveChat.cfm", body); final j = _requireJson(raw, "GetActiveChat"); if (!_ok(j)) { return null; } final hasActiveChat = j["HAS_ACTIVE_CHAT"] == true; if (!hasActiveChat) return null; final taskId = (j["TASK_ID"] as num?)?.toInt(); return (taskId != null && taskId > 0) ? taskId : null; } /// Create a chat task and return the task ID static Future createChatTask({ required int businessId, required int servicePointId, int? orderId, int? userId, String? initialMessage, }) async { final body = { "BusinessID": businessId, "ServicePointID": servicePointId, }; if (orderId != null && orderId > 0) body["OrderID"] = orderId; if (userId != null && userId > 0) body["UserID"] = userId; if (initialMessage != null && initialMessage.isNotEmpty) { body["Message"] = initialMessage; } final raw = await _postRaw("/tasks/createChat.cfm", body); final j = _requireJson(raw, "CreateChatTask"); if (!_ok(j)) { throw StateError( "CreateChatTask failed: ${_err(j)} - ${(j["MESSAGE"] ?? "").toString()}", ); } return (j["TaskID"] ?? j["TASK_ID"] as num).toInt(); } /// Get chat messages for a task /// Returns messages and whether the chat has been closed by the worker static Future<({List messages, bool chatClosed})> getChatMessages({ required int taskId, int? afterMessageId, }) async { final body = { "TaskID": taskId, }; if (afterMessageId != null && afterMessageId > 0) { body["AfterMessageID"] = afterMessageId; } final raw = await _postRaw("/chat/getMessages.cfm", body); final j = _requireJson(raw, "GetChatMessages"); if (!_ok(j)) { throw StateError("GetChatMessages failed: ${_err(j)}"); } final arr = _pickArray(j, const ["MESSAGES", "messages"]); final messages = arr == null ? [] : arr.map((e) { final item = e is Map ? e : (e as Map).cast(); return ChatMessage.fromJson(item); }).toList(); // Check if chat has been closed (task completed) final chatClosed = j["CHAT_CLOSED"] == true || j["chat_closed"] == true; return (messages: messages, chatClosed: chatClosed); } /// Send a chat message (HTTP fallback when WebSocket unavailable) static Future sendChatMessage({ required int taskId, required String message, int? userId, String? senderType, }) async { final body = { "TaskID": taskId, "Message": message, }; if (userId != null) body["UserID"] = userId; if (senderType != null) body["SenderType"] = senderType; final raw = await _postRaw("/chat/sendMessage.cfm", body); final j = _requireJson(raw, "SendChatMessage"); if (!_ok(j)) { throw StateError("SendChatMessage failed: ${_err(j)}"); } return ((j["MessageID"] ?? j["MESSAGE_ID"]) as num).toInt(); } /// Mark chat messages as read static Future markChatMessagesRead({ required int taskId, required String readerType, }) async { final raw = await _postRaw("/chat/markRead.cfm", { "TaskID": taskId, "ReaderType": readerType, }); final j = _requireJson(raw, "MarkChatMessagesRead"); if (!_ok(j)) { throw StateError("MarkChatMessagesRead failed: ${_err(j)}"); } } /// Get auth token for WebSocket authentication static String? get authToken => _userToken; } class OrderHistoryResponse { final List orders; final int totalCount; const OrderHistoryResponse({ required this.orders, required this.totalCount, }); } class BeaconBusinessMapping { final int beaconId; final String beaconName; final int businessId; final String businessName; final int servicePointId; final String servicePointName; const BeaconBusinessMapping({ required this.beaconId, required this.beaconName, required this.businessId, required this.businessName, required this.servicePointId, required this.servicePointName, }); } class PendingOrder { final int orderId; final String orderUuid; final int orderTypeId; final String orderTypeName; final int statusId; final String statusName; final String submittedOn; final int servicePointId; final String servicePointName; final String businessName; final double subtotal; const PendingOrder({ required this.orderId, required this.orderUuid, required this.orderTypeId, required this.orderTypeName, required this.statusId, required this.statusName, required this.submittedOn, required this.servicePointId, required this.servicePointName, required this.businessName, required this.subtotal, }); factory PendingOrder.fromJson(Map json) { return PendingOrder( orderId: (json["OrderID"] ?? json["ORDERID"] ?? 0) as int, orderUuid: (json["OrderUUID"] ?? json["ORDERUUID"] ?? "") as String, orderTypeId: (json["OrderTypeID"] ?? json["ORDERTYPEID"] ?? 0) as int, orderTypeName: (json["OrderTypeName"] ?? json["ORDERTYPENAME"] ?? "") as String, statusId: (json["OrderStatusID"] ?? json["ORDERSTATUSID"] ?? 0) as int, statusName: (json["StatusName"] ?? json["STATUSNAME"] ?? "") as String, submittedOn: (json["SubmittedOn"] ?? json["SUBMITTEDON"] ?? "") as String, servicePointId: (json["ServicePointID"] ?? json["SERVICEPOINTID"] ?? 0) as int, servicePointName: (json["ServicePointName"] ?? json["SERVICEPOINTNAME"] ?? "") as String, businessName: (json["BusinessName"] ?? json["BUSINESSNAME"] ?? "") as String, subtotal: ((json["Subtotal"] ?? json["SUBTOTAL"] ?? 0) as num).toDouble(), ); } bool get isReady => statusId == 3; bool get isPreparing => statusId == 2; bool get isSubmitted => statusId == 1; } class DeliveryAddress { final int addressId; final String label; final bool isDefault; final String line1; final String line2; final String city; final int stateId; final String stateAbbr; final String stateName; final String zipCode; final String displayText; const DeliveryAddress({ required this.addressId, required this.label, required this.isDefault, required this.line1, required this.line2, required this.city, required this.stateId, required this.stateAbbr, required this.stateName, required this.zipCode, required this.displayText, }); factory DeliveryAddress.fromJson(Map json) { return DeliveryAddress( addressId: (json["AddressID"] ?? json["ADDRESSID"] ?? 0) as int, label: (json["Label"] ?? json["LABEL"] ?? "Address") as String, isDefault: (json["IsDefault"] ?? json["ISDEFAULT"] ?? false) == true, line1: (json["Line1"] ?? json["LINE1"] ?? "") as String, line2: (json["Line2"] ?? json["LINE2"] ?? "") as String, city: (json["City"] ?? json["CITY"] ?? "") as String, stateId: (json["StateID"] ?? json["STATEID"] ?? 0) as int, stateAbbr: (json["StateAbbr"] ?? json["STATEABBR"] ?? "") as String, stateName: (json["StateName"] ?? json["STATENAME"] ?? "") as String, zipCode: (json["ZIPCode"] ?? json["ZIPCODE"] ?? "") as String, displayText: (json["DisplayText"] ?? json["DISPLAYTEXT"] ?? "") as String, ); } } class AvatarInfo { final bool hasAvatar; final String? avatarUrl; const AvatarInfo({ required this.hasAvatar, required this.avatarUrl, }); } class StateInfo { final int stateId; final String abbr; final String name; const StateInfo({ required this.stateId, required this.abbr, required this.name, }); factory StateInfo.fromJson(Map json) { return StateInfo( stateId: (json["StateID"] ?? json["STATEID"] ?? 0) as int, abbr: (json["Abbr"] ?? json["ABBR"] ?? "") as String, name: (json["Name"] ?? json["NAME"] ?? "") as String, ); } }