- Removed all debug print statements from menu_browse_screen.dart - Removed all debug print statements from api.dart - Deleted unused beacon_scan_screen_broken.dart backup file - Kept debugPrint statements in beacon_scan_screen.dart and beacon_permissions.dart for debugging beacon issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
482 lines
13 KiB
Dart
482 lines
13 KiB
Dart
import "dart:convert";
|
|
import "package:http/http.dart" as http;
|
|
|
|
import "../models/cart.dart";
|
|
import "../models/menu_item.dart";
|
|
import "../models/restaurant.dart";
|
|
import "../models/service_point.dart";
|
|
import "auth_storage.dart";
|
|
|
|
class ApiRawResponse {
|
|
final int statusCode;
|
|
final String rawBody;
|
|
final Map<String, dynamic>? 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<String, dynamic> json) {
|
|
return LoginResponse(
|
|
userId: (json["UserID"] as num).toInt(),
|
|
userFirstName: (json["UserFirstName"] as String?) ?? "",
|
|
token: (json["Token"] as String?) ?? "",
|
|
);
|
|
}
|
|
}
|
|
|
|
class Api {
|
|
static String? _userToken;
|
|
|
|
// MVP hardcode
|
|
static int _mvpBusinessId = 17;
|
|
|
|
static void setAuthToken(String? token) => _userToken = token;
|
|
|
|
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("AALISTS_API_BASE_URL");
|
|
if (v.isEmpty) {
|
|
throw StateError(
|
|
"AALISTS_API_BASE_URL is not set. Example (Android emulator): "
|
|
"--dart-define=AALISTS_API_BASE_URL=http://10.0.2.2:8888/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<String, String> _headers({required bool json, int? businessIdOverride}) {
|
|
final h = <String, String>{};
|
|
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<String, dynamic>? _tryDecodeJsonMap(String body) {
|
|
try {
|
|
final decoded = jsonDecode(body);
|
|
if (decoded is Map<String, dynamic>) return decoded;
|
|
} catch (_) {}
|
|
return null;
|
|
}
|
|
|
|
static Future<ApiRawResponse> _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<ApiRawResponse> _postRaw(
|
|
String path,
|
|
Map<String, dynamic> 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<String, dynamic> j) => j["OK"] == true || j["ok"] == true;
|
|
|
|
static String _err(Map<String, dynamic> j) => (j["ERROR"] ?? j["error"] ?? "").toString();
|
|
|
|
static List<dynamic>? _pickArray(Map<String, dynamic> j, List<String> keys) {
|
|
for (final k in keys) {
|
|
final v = j[k];
|
|
if (v is List) return v;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static Map<String, dynamic> _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<LoginResponse> 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<void> logout() async {
|
|
setAuthToken(null);
|
|
clearCookies();
|
|
await AuthStorage.clearAuth();
|
|
}
|
|
|
|
// -------------------------
|
|
// Businesses (legacy model name: Restaurant)
|
|
// -------------------------
|
|
|
|
static Future<ApiRawResponse> listRestaurantsRaw() async {
|
|
return _getRaw("/businesses/list.cfm", businessIdOverride: _mvpBusinessId);
|
|
}
|
|
|
|
static Future<List<Restaurant>> 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 = <Restaurant>[];
|
|
for (final e in arr) {
|
|
if (e is Map<String, dynamic>) {
|
|
out.add(Restaurant.fromJson(e));
|
|
} else if (e is Map) {
|
|
out.add(Restaurant.fromJson(e.cast<String, dynamic>()));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// -------------------------
|
|
// Service Points
|
|
// -------------------------
|
|
|
|
static Future<List<ServicePoint>> 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 = <ServicePoint>[];
|
|
for (final e in arr) {
|
|
if (e is Map<String, dynamic>) {
|
|
out.add(ServicePoint.fromJson(e));
|
|
} else if (e is Map) {
|
|
out.add(ServicePoint.fromJson(e.cast<String, dynamic>()));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// -------------------------
|
|
// Menu Items
|
|
// -------------------------
|
|
|
|
static Future<List<MenuItem>> 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 = <MenuItem>[];
|
|
for (final e in arr) {
|
|
if (e is Map<String, dynamic>) {
|
|
out.add(MenuItem.fromJson(e));
|
|
} else if (e is Map) {
|
|
out.add(MenuItem.fromJson(e.cast<String, dynamic>()));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// -------------------------
|
|
// Cart & Orders
|
|
// -------------------------
|
|
|
|
static Future<Cart> 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<Cart> 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<Cart> 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");
|
|
|
|
if (!_ok(j)) {
|
|
throw StateError(
|
|
"SetLineItem API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}",
|
|
);
|
|
}
|
|
|
|
return Cart.fromJson(j);
|
|
}
|
|
|
|
static Future<void> 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}",
|
|
);
|
|
}
|
|
}
|
|
|
|
// -------------------------
|
|
// Beacons
|
|
// -------------------------
|
|
|
|
static Future<Map<String, int>> 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<String, int> uuidToBeaconId = {};
|
|
|
|
for (final e in arr) {
|
|
if (e is! Map) continue;
|
|
final item = e is Map<String, dynamic> ? e : e.cast<String, dynamic>();
|
|
|
|
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<BeaconBusinessMapping> 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<String, dynamic>? ?? {};
|
|
final business = j["BUSINESS"] as Map<String, dynamic>? ?? {};
|
|
final servicePoint = j["SERVICEPOINT"] as Map<String, dynamic>? ?? {};
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|