Clean up debug statements and sanitize error messages

- Remove 60+ debug print statements from services and screens
- Sanitize error messages to not expose internal API details
- Remove stack trace exposure in beacon_scan_screen
- Delete unused order_home_screen.dart
- Remove unused ChatScreen route from app_router
- Fix widget_test.dart to compile
- Remove unused foundation.dart import from menu_browse_screen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-11 13:44:58 -08:00
parent 65b5b82546
commit 768b882ca7
8 changed files with 18 additions and 523 deletions

View file

@ -2,7 +2,6 @@ import "package:flutter/material.dart";
import "../screens/account_screen.dart";
import "../screens/about_screen.dart";
import "../screens/chat_screen.dart";
import "../screens/address_edit_screen.dart";
import "../screens/address_list_screen.dart";
import "../screens/beacon_scan_screen.dart";
@ -35,8 +34,6 @@ class AppRoutes {
static const String addressEdit = "/address-edit";
static const String about = "/about";
static const String signup = "/signup";
static const String chat = "/chat";
static Map<String, WidgetBuilder> get routes => {
splash: (_) => const SplashScreen(),
login: (_) => const LoginScreen(),

View file

@ -58,12 +58,10 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
Future<void> _startScanFlow() async {
// Step 1: Request permissions
setState(() => _status = 'Requesting permissions...');
print('[BeaconScan] 🔐 Requesting permissions...');
final granted = await BeaconPermissions.requestPermissions();
if (!granted) {
print('[BeaconScan] ❌ Permissions DENIED');
setState(() {
_status = 'Permissions denied - Please enable Location & Bluetooth';
_permissionsGranted = false;
@ -71,17 +69,14 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
return;
}
print('[BeaconScan] ✅ Permissions GRANTED');
setState(() => _permissionsGranted = true);
// Step 1.5: Check if Bluetooth is ON
setState(() => _status = 'Checking Bluetooth...');
print('[BeaconScan] 📶 Checking Bluetooth state...');
final bluetoothOn = await BeaconPermissions.ensureBluetoothEnabled();
if (!bluetoothOn) {
print('[BeaconScan] ❌ Bluetooth is OFF');
setState(() {
_status = 'Please turn on Bluetooth to scan for tables';
_scanning = false;
@ -92,28 +87,16 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
return;
}
print('[BeaconScan] ✅ Bluetooth is ON');
// Step 2: Fetch all active beacons from server
setState(() => _status = 'Loading beacon data...');
try {
_uuidToBeaconId = await Api.listAllBeacons();
print('[BeaconScan] ========================================');
print('[BeaconScan] Loaded ${_uuidToBeaconId.length} beacons from database:');
_uuidToBeaconId.forEach((uuid, beaconId) {
print('[BeaconScan] BeaconID=$beaconId');
print('[BeaconScan] UUID=$uuid');
print('[BeaconScan] ---');
});
print('[BeaconScan] ========================================');
} catch (e) {
debugPrint('[BeaconScan] Error loading beacons: $e');
_uuidToBeaconId = {};
}
if (_uuidToBeaconId.isEmpty) {
print('[BeaconScan] ⚠️ No beacons in database, going to restaurant select');
if (mounted) _navigateToRestaurantSelect();
return;
}
@ -133,75 +116,52 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
await flutterBeacon.initializeScanning;
// Brief delay to let Bluetooth subsystem fully initialize
// Without this, the first scan cycle may complete immediately with no results
await Future.delayed(const Duration(milliseconds: 1500));
// Create regions for all known UUIDs
final regions = _uuidToBeaconId.keys.map((uuid) {
// Format UUID with dashes for the plugin
final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}';
print('[BeaconScan] 🔍 Creating region for UUID: $uuid -> $formattedUUID');
return Region(identifier: uuid, proximityUUID: formattedUUID);
}).toList();
print('[BeaconScan] 📡 Created ${regions.length} regions for scanning');
if (regions.isEmpty) {
if (mounted) _navigateToRestaurantSelect();
return;
}
// Perform scan cycles - always complete all cycles for dine-in beacon detection
print('[BeaconScan] 🔄 Starting scan cycles');
// Perform scan cycles
for (int scanCycle = 1; scanCycle <= 3; scanCycle++) {
// Update status message for each cycle
if (mounted) {
setState(() => _status = _scanMessages[scanCycle - 1]);
}
print('[BeaconScan] ----- Scan cycle $scanCycle/3 -----');
StreamSubscription<RangingResult>? subscription;
subscription = flutterBeacon.ranging(regions).listen((result) {
print('[BeaconScan] 📶 Ranging result: ${result.beacons.length} beacons in range');
for (var beacon in result.beacons) {
final rawUUID = beacon.proximityUUID;
final uuid = rawUUID.toUpperCase().replaceAll('-', '');
final rssi = beacon.rssi;
print('[BeaconScan] 📍 Raw beacon detected: UUID=$rawUUID (normalized: $uuid), RSSI=$rssi, Major=${beacon.major}, Minor=${beacon.minor}');
if (_uuidToBeaconId.containsKey(uuid)) {
// Collect RSSI samples for averaging
_beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi);
_beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1;
print('[BeaconScan] ✅ MATCHED! BeaconID=${_uuidToBeaconId[uuid]}, Sample #${_beaconDetectionCount[uuid]}, RSSI=$rssi');
} else {
print('[BeaconScan] ⚠️ UUID not in database, ignoring');
}
}
});
// Wait for this scan cycle to collect beacon data
await Future.delayed(const Duration(milliseconds: 2000));
await subscription.cancel();
// Short pause between scan cycles
if (scanCycle < 3) {
await Future.delayed(const Duration(milliseconds: 500));
}
}
print('[BeaconScan] ✔️ Scan complete');
if (!mounted) return;
// Analyze results and select best beacon
print('[BeaconScan] ===== SCAN COMPLETE =====');
print('[BeaconScan] 📊 Total beacons detected: ${_beaconRssiSamples.length}');
final beaconScores = <String, BeaconScore>{};
for (final uuid in _beaconRssiSamples.keys) {
@ -209,10 +169,7 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
final detections = _beaconDetectionCount[uuid]!;
final beaconId = _uuidToBeaconId[uuid]!;
// Calculate average RSSI
final avgRssi = samples.reduce((a, b) => a + b) / samples.length;
// Calculate RSSI variance for stability metric
final variance = samples.map((r) => (r - avgRssi) * (r - avgRssi)).reduce((a, b) => a + b) / samples.length;
beaconScores[uuid] = BeaconScore(
@ -226,24 +183,7 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
);
}
if (beaconScores.isNotEmpty) {
print('[BeaconScan] Beacon analysis results:');
final sorted = beaconScores.values.toList()
..sort((a, b) => b.avgRssi.compareTo(a.avgRssi)); // Sort by avg RSSI descending
for (final score in sorted) {
print('[BeaconScan] - BeaconID=${score.beaconId}:');
print('[BeaconScan] Avg RSSI: ${score.avgRssi.toStringAsFixed(1)}');
print('[BeaconScan] Range: ${score.minRssi} to ${score.maxRssi}');
print('[BeaconScan] Detections: ${score.detectionCount}');
print('[BeaconScan] Variance: ${score.variance.toStringAsFixed(2)}');
}
}
print('[BeaconScan] ==========================');
if (beaconScores.isEmpty) {
// No Payfrit beacons found - stop scanning and go to business list
print('[BeaconScan] 🚫 No Payfrit beacons found, navigating to restaurant select');
setState(() {
_scanning = false;
_status = 'No nearby tables detected';
@ -252,14 +192,11 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
if (mounted) _navigateToRestaurantSelect();
return;
} else {
// Find beacon with highest average RSSI and minimum detections
final best = _findBestBeacon(beaconScores);
if (best != null) {
print('[BeaconScan] 🎯 Selected beacon: BeaconID=${best.beaconId} (avg RSSI: ${best.avgRssi.toStringAsFixed(1)})');
setState(() => _status = 'Beacon detected! Loading business...');
await _autoSelectBusinessFromBeacon(best.beaconId);
} else {
print('[BeaconScan] ⚠️ No beacon met minimum confidence threshold');
setState(() {
_scanning = false;
_status = 'No strong beacon signal';
@ -269,8 +206,6 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
}
}
} catch (e) {
print('[BeaconScan] ❌ ERROR during scan: $e');
print('[BeaconScan] Stack trace: ${StackTrace.current}');
if (mounted) {
setState(() {
_scanning = false;
@ -335,25 +270,21 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
bool hasEnoughSamples = _beaconRssiSamples.values.any((samples) => samples.length >= 2);
if (!hasEnoughSamples) return false;
// Check if there's a clear winner (one beacon significantly stronger than others)
// or if all beacons have low variance in their readings
// Check if there's a clear winner or if all beacons have low variance
for (final entry in _beaconRssiSamples.entries) {
final samples = entry.value;
if (samples.length < 3) continue;
// Calculate variance
final avg = samples.reduce((a, b) => a + b) / samples.length;
final variance = samples.map((r) => (r - avg) * (r - avg)).reduce((a, b) => a + b) / samples.length;
// If variance is high (readings fluctuating a lot), keep scanning
// Variance > 50 means RSSI is jumping around too much
// If variance is high, keep scanning
if (variance > 50) {
print('[BeaconScan] ⏳ Beacon ${_uuidToBeaconId[entry.key]} has high variance (${variance.toStringAsFixed(1)}), continuing scan');
return false;
}
}
// If we have multiple beacons, check if there's a clear strongest one
// If multiple beacons, check if there's a clear strongest one
if (_beaconRssiSamples.length > 1) {
final avgRssis = <String, double>{};
for (final entry in _beaconRssiSamples.entries) {
@ -363,20 +294,17 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
}
}
// Sort by RSSI descending
final sorted = avgRssis.entries.toList()..sort((a, b) => b.value.compareTo(a.value));
// If top two beacons are within 5 dB, keep scanning for more clarity
if (sorted.length >= 2) {
final diff = sorted[0].value - sorted[1].value;
if (diff < 5) {
print('[BeaconScan] ⏳ Top 2 beacons are close (diff=${diff.toStringAsFixed(1)} dB), continuing scan');
return false;
}
}
}
print('[BeaconScan] ✓ Readings are stable, can exit early');
return true;
}

View file

@ -87,9 +87,7 @@ class _ChatScreenState extends State<ChatScreen> {
});
try {
debugPrint('[Chat] Loading messages for task ${widget.taskId}...');
final result = await Api.getChatMessages(taskId: widget.taskId);
debugPrint('[Chat] Loaded ${result.messages.length} messages, chatClosed: ${result.chatClosed}');
if (mounted) {
final wasClosed = result.chatClosed && !_chatEnded;
setState(() {
@ -108,10 +106,9 @@ class _ChatScreenState extends State<ChatScreen> {
}
}
} catch (e) {
debugPrint('[Chat] Error loading messages: $e');
if (mounted) {
setState(() {
_error = 'Failed to load messages: $e';
_error = 'Failed to load messages';
_isLoading = false;
});
}
@ -124,7 +121,6 @@ class _ChatScreenState extends State<ChatScreen> {
// Auth should already be loaded by _initializeChat
final token = Api.authToken;
if (token == null || token.isEmpty) {
debugPrint('[Chat] No auth token, skipping WebSocket (will use HTTP fallback with polling)');
setState(() => _isConnecting = false);
_startPolling();
return;
@ -163,7 +159,6 @@ class _ChatScreenState extends State<ChatScreen> {
_eventSubscription = _chatService.events.listen((event) {
switch (event.type) {
case ChatEventType.joined:
debugPrint('Joined chat room');
break;
case ChatEventType.userJoined:
final name = event.data?['userName'] ?? 'Someone';
@ -204,7 +199,6 @@ class _ChatScreenState extends State<ChatScreen> {
}
break;
case ChatEventType.disconnected:
debugPrint('Disconnected from chat');
break;
case ChatEventType.error:
if (mounted) {
@ -228,7 +222,6 @@ class _ChatScreenState extends State<ChatScreen> {
if (mounted) {
setState(() => _isConnecting = false);
if (!connected) {
debugPrint('Failed to connect to WebSocket, using HTTP fallback with polling');
_startPolling();
}
}
@ -281,7 +274,7 @@ class _ChatScreenState extends State<ChatScreen> {
}
}
} catch (e) {
debugPrint('[Chat] Poll error: $e');
// Polling error - will retry on next interval
}
}
@ -330,16 +323,13 @@ class _ChatScreenState extends State<ChatScreen> {
if (!sentViaWebSocket) {
// Fallback to HTTP
debugPrint('[Chat] WebSocket not available, using HTTP fallback');
final authData = await AuthStorage.loadAuth();
final userId = authData?.userId;
if (userId == null || userId == 0) {
throw StateError('Not logged in. Please sign in again.');
throw StateError('Please sign in to send messages');
}
debugPrint('[Chat] Sending HTTP message: taskId=${widget.taskId}, userId=$userId');
await Api.sendChatMessage(
taskId: widget.taskId,
message: text,
@ -352,11 +342,11 @@ class _ChatScreenState extends State<ChatScreen> {
await _loadMessages();
}
} catch (e) {
debugPrint('Error sending message: $e');
if (mounted) {
final message = e.toString().replaceAll('StateError: ', '');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to send: $e'),
content: Text('Failed to send: $message'),
backgroundColor: Colors.red,
),
);

View file

@ -1,4 +1,3 @@
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:provider/provider.dart";
@ -95,7 +94,7 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
servicePointId: _servicePointId!,
);
} catch (e) {
debugPrint('[Menu] Error checking active chat: $e');
// Continue without active chat
}
if (!mounted) return;
@ -341,43 +340,27 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
_categorySortOrder.clear();
_categoryNames.clear();
print('[MenuBrowse] _organizeItems: ${_allItems.length} total items');
// First pass: identify category items (root items where itemId == categoryId)
// These are the category headers themselves, NOT menu items
final categoryItemIds = <int>{};
for (final item in _allItems) {
if (item.isRootItem && item.itemId == item.categoryId) {
categoryItemIds.add(item.itemId);
// Just register the category key (empty list for now)
_itemsByCategory.putIfAbsent(item.itemId, () => []);
// Store the sort order and name for this category
_categorySortOrder[item.itemId] = item.sortOrder;
_categoryNames[item.itemId] = item.name;
print('[MenuBrowse] Category found: ${item.name} (ID=${item.itemId}, sortOrder=${item.sortOrder})');
}
}
print('[MenuBrowse] Found ${categoryItemIds.length} categories: $categoryItemIds');
// Second pass: organize menu items and modifiers
for (final item in _allItems) {
// Skip inactive items
if (!item.isActive) continue;
// Skip category header items (they're not menu items to display)
if (categoryItemIds.contains(item.itemId)) continue;
// Check if parent is a category
if (categoryItemIds.contains(item.parentItemId)) {
// Direct child of a category = menu item (goes in _itemsByCategory)
_itemsByCategory.putIfAbsent(item.parentItemId, () => []).add(item);
print('[MenuBrowse] Menu item: ${item.name} -> category ${item.parentItemId}');
} else {
// Child of a menu item = modifier (goes in _itemsByParent)
if (item.itemId != item.parentItemId) {
_itemsByParent.putIfAbsent(item.parentItemId, () => []).add(item);
print('[MenuBrowse] Modifier: ${item.name} -> parent ${item.parentItemId}');
}
}
}
@ -391,11 +374,6 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
for (final list in _itemsByParent.values) {
list.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
}
// Debug: print final counts
for (final entry in _itemsByCategory.entries) {
print('[MenuBrowse] Category ${entry.key}: ${entry.value.length} items');
}
}
List<int> _getUniqueCategoryIds() {
@ -556,11 +534,6 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
final categoryName = _categoryNames[categoryId] ?? "Category $categoryId";
final isExpanded = _expandedCategoryId == categoryId;
// Debug: Print which items are being shown for which category
if (items.isNotEmpty) {
print('[MenuBrowse] DISPLAY: Category "$categoryName" (ID=$categoryId) showing items: ${items.map((i) => i.name).join(", ")}');
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -1067,7 +1040,6 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
);
// Add all selected modifiers recursively
print('[MenuBrowse] Adding ${selectedModifierIds.length} modifiers to root item OrderLineItemID=${rootLineItem.orderLineItemId}');
await _addModifiersRecursively(
cart.orderId,
rootLineItem.orderLineItemId,
@ -1076,9 +1048,7 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
);
// Refresh cart to get final state
print('[MenuBrowse] Refreshing cart to get final state');
cart = await Api.getCart(orderId: cart.orderId);
print('[MenuBrowse] Final cart has ${cart.lineItems.length} total line items');
appState.updateCartItemCount(cart.itemCount);
@ -1129,8 +1099,6 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
Set<int> selectedItemIds,
) async {
final children = _itemsByParent[parentItemId] ?? [];
print('[MenuBrowse] _addModifiersRecursively: parentItemId=$parentItemId has ${children.length} children');
print('[MenuBrowse] selectedItemIds passed in: $selectedItemIds');
for (final child in children) {
final isSelected = selectedItemIds.contains(child.itemId);
@ -1138,13 +1106,7 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
final hasGrandchildren = grandchildren.isNotEmpty;
final hasSelectedDescendants = _hasSelectedDescendants(child.itemId, selectedItemIds);
print('[MenuBrowse] Child ${child.name} (ItemID=${child.itemId}): selected=$isSelected, hasChildren=$hasGrandchildren, hasSelectedDescendants=$hasSelectedDescendants');
// Only add this item if it's explicitly selected
// Container items (parents) should only be added if they themselves are in selectedItemIds
// This prevents default items from being submitted when the user hasn't modified them
if (isSelected) {
print('[MenuBrowse] ADDING selected item ${child.name} with ParentOrderLineItemID=$parentOrderLineItemId');
final cart = await Api.setLineItem(
orderId: orderId,
parentOrderLineItemId: parentOrderLineItemId,
@ -1152,13 +1114,11 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
isSelected: true,
);
// Find the OrderLineItemID of this item we just added
final childLineItem = cart.lineItems.lastWhere(
(li) => li.itemId == child.itemId && li.parentOrderLineItemId == parentOrderLineItemId && !li.isDeleted,
orElse: () => throw StateError('Child line item not found for ItemID=${child.itemId}'),
orElse: () => throw StateError('Failed to add item'),
);
// Recursively add children with this item as the new parent
if (hasGrandchildren) {
await _addModifiersRecursively(
orderId,
@ -1168,9 +1128,6 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
);
}
} else if (hasSelectedDescendants) {
// This item itself is not selected, but it has selected descendants
// We need to add it as a container to maintain hierarchy
print('[MenuBrowse] ADDING container item ${child.name} (has selected descendants) with ParentOrderLineItemID=$parentOrderLineItemId');
final cart = await Api.setLineItem(
orderId: orderId,
parentOrderLineItemId: parentOrderLineItemId,
@ -1178,21 +1135,17 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
isSelected: true,
);
// Find the OrderLineItemID of this item we just added
final childLineItem = cart.lineItems.lastWhere(
(li) => li.itemId == child.itemId && li.parentOrderLineItemId == parentOrderLineItemId && !li.isDeleted,
orElse: () => throw StateError('Child line item not found for ItemID=${child.itemId}'),
orElse: () => throw StateError('Failed to add item'),
);
// Recursively add children with this item as the new parent
await _addModifiersRecursively(
orderId,
childLineItem.orderLineItemId,
child.itemId,
selectedItemIds,
);
} else {
print('[MenuBrowse] SKIPPING ${child.name} (not selected, no selected descendants)');
}
}
}
@ -1334,31 +1287,17 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> {
// Filter out default items in groups that user never modified
final itemsToSubmit = <int>{};
print('[Customization] ========== FILTERING LOGIC ==========');
print('[Customization] All selected items: $_selectedItemIds');
print('[Customization] Default items: $_defaultItemIds');
print('[Customization] User-modified groups: $_userModifiedGroups');
for (final itemId in _selectedItemIds) {
// Find which parent group this item belongs to
final parentId = _findParentId(itemId);
final isDefault = _defaultItemIds.contains(itemId);
final groupWasModified = parentId != null && _userModifiedGroups.contains(parentId);
print('[Customization] Item $itemId: isDefault=$isDefault, parentId=$parentId, groupWasModified=$groupWasModified');
// Include if: not a default, OR user modified this group
if (!isDefault || groupWasModified) {
print('[Customization] -> INCLUDED (not default or group was modified)');
itemsToSubmit.add(itemId);
} else {
print('[Customization] -> EXCLUDED (is default and group was not modified)');
}
}
print('[Customization] Final items to submit: $itemsToSubmit');
print('[Customization] =====================================');
widget.onAdd(itemsToSubmit);
}
}

View file

@ -1,303 +0,0 @@
// lib/screens/order_home_screen.dart
import "package:flutter/material.dart";
import "../services/api.dart";
class OrderHomeScreen extends StatefulWidget {
// OPTIONAL so routes that call `const OrderHomeScreen()` compile.
final int? businessId;
final int? servicePointId;
final int? userId;
const OrderHomeScreen({
super.key,
this.businessId,
this.servicePointId,
this.userId,
});
@override
State<OrderHomeScreen> createState() => _OrderHomeScreenState();
}
class _OrderHomeScreenState extends State<OrderHomeScreen> {
bool _busy = false;
String? _error;
int? _businessId;
int? _servicePointId;
int? _userId;
int? _orderId;
int? _asIntNullable(dynamic v) {
if (v == null) return null;
if (v is int) return v;
if (v is double) return v.toInt();
if (v is String) {
final s = v.trim();
if (s.isEmpty) return null;
return int.tryParse(s);
}
return null;
}
void _loadIdsFromWidgetAndRoute() {
int? b = widget.businessId;
int? sp = widget.servicePointId;
int? u = widget.userId;
// If not provided via constructor, attempt to read from route arguments.
final args = ModalRoute.of(context)?.settings.arguments;
if ((b == null || sp == null || u == null) && args is Map) {
// Prefer exact key spellings that match labels.
b ??= _asIntNullable(args["BusinessID"]);
sp ??= _asIntNullable(args["ServicePointID"]);
u ??= _asIntNullable(args["UserID"]);
// Fallback keys (common Flutter style)
b ??= _asIntNullable(args["businessId"]);
sp ??= _asIntNullable(args["servicePointId"]);
u ??= _asIntNullable(args["userId"]);
}
// Normalize "0" to null (so we don't show misleading zeros).
if (b == 0) b = null;
if (sp == 0) sp = null;
if (u == 0) u = null;
final changed = (b != _businessId) || (sp != _servicePointId) || (u != _userId);
if (changed) {
setState(() {
_businessId = b;
_servicePointId = sp;
_userId = u;
});
}
}
@override
void initState() {
super.initState();
// cannot read ModalRoute here reliably; do it in didChangeDependencies.
_businessId = widget.businessId == 0 ? null : widget.businessId;
_servicePointId = widget.servicePointId == 0 ? null : widget.servicePointId;
_userId = widget.userId == 0 ? null : widget.userId;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadIdsFromWidgetAndRoute();
}
@override
void didUpdateWidget(covariant OrderHomeScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.businessId != widget.businessId ||
oldWidget.servicePointId != widget.servicePointId ||
oldWidget.userId != widget.userId) {
_loadIdsFromWidgetAndRoute();
}
}
Future<void> _run(Future<void> Function() fn) async {
if (_busy) return;
setState(() {
_busy = true;
_error = null;
});
try {
await fn();
} catch (e) {
setState(() => _error = e.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
bool get _hasAllIds => _businessId != null && _servicePointId != null && _userId != null;
Future<void> _createOrLoadCart() async {
if (!_hasAllIds) {
setState(() {
_error = "Missing IDs. BusinessID / ServicePointID / UserID were not provided to this screen.";
});
return;
}
await _run(() async {
final cartData = await Api.getOrCreateCart(
userId: _userId!,
businessId: _businessId!,
servicePointId: _servicePointId!,
orderTypeId: 1, // MVP default: dine-in
);
final oid = _asIntNullable(cartData is Map ? cartData["OrderID"] : null);
setState(() => _orderId = oid);
});
}
Future<void> _refreshCart() async {
if (_orderId == null) return;
await _run(() async {
await Api.getCart(orderId: _orderId!);
});
}
Future<void> _addDemoItem(int itemId) async {
if (_orderId == null) {
setState(() => _error = "No cart yet. Tap 'Create/Load Cart' first.");
return;
}
await _run(() async {
await Api.setLineItem(
orderId: _orderId!,
parentOrderLineItemId: 0,
itemId: itemId,
qty: 1,
selectedChildItemIds: const <int>[],
);
});
}
Future<void> _submit() async {
if (_orderId == null) {
setState(() => _error = "No cart yet.");
return;
}
await _run(() async {
await Api.submitOrder(orderId: _orderId!);
});
}
Widget _idRow(String label, int? value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
),
Expanded(
child: Text(
value == null ? "(missing)" : value.toString(),
textAlign: TextAlign.right,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final orderId = _orderId;
return Scaffold(
appBar: AppBar(title: const Text("Order (Compile-Only MVP)")),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
"This screen exists only to compile cleanly against\n"
"Api.dart.\n"
"No menu, no models, no polish.",
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
_idRow("BusinessID", _businessId),
_idRow("ServicePointID", _servicePointId),
_idRow("UserID", _userId),
],
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _busy ? null : _createOrLoadCart,
child: const Text("Create/Load Cart"),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: (_busy || orderId == null) ? null : _refreshCart,
child: const Text("Refresh Cart"),
),
),
],
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Cart OrderID: ${orderId ?? "(none)"}"),
const SizedBox(height: 10),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
ElevatedButton(
onPressed: _busy ? null : () => _addDemoItem(101),
child: const Text("Add Demo Item 101"),
),
ElevatedButton(
onPressed: _busy ? null : () => _addDemoItem(102),
child: const Text("Add Demo Item 102"),
),
ElevatedButton(
onPressed: _busy ? null : () => _addDemoItem(103),
child: const Text("Add Demo Item 103"),
),
],
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: (_busy || orderId == null) ? null : _submit,
child: const Text("Submit Order"),
),
],
),
),
),
const SizedBox(height: 16),
if (_busy) ...[
const Center(child: CircularProgressIndicator()),
const SizedBox(height: 16),
],
if (_error != null)
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(_error!),
),
),
],
),
);
}
}

View file

@ -325,13 +325,11 @@ class Api {
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)) {
@ -581,7 +579,6 @@ class Api {
);
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(
@ -877,9 +874,7 @@ class Api {
/// Get user's avatar URL
static Future<AvatarInfo> 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)) {
@ -892,28 +887,14 @@ class Api {
);
}
/// Debug: Check token status with server
static Future<void> 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<String> 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;
}
@ -925,8 +906,6 @@ class Api {
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");
@ -941,9 +920,7 @@ class Api {
/// Get order history for current user
static Future<OrderHistoryResponse> 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)) {

View file

@ -61,7 +61,6 @@ class ChatService {
);
_socket!.onConnect((_) {
print('[ChatService] Connected to WebSocket');
_isConnected = true;
// Join the chat room
@ -73,7 +72,6 @@ class ChatService {
});
_socket!.on('joined', (data) {
print('[ChatService] Joined chat room: $data');
_eventController.add(ChatEvent(
type: ChatEventType.joined,
data: data,
@ -84,7 +82,6 @@ class ChatService {
});
_socket!.on('error', (data) {
print('[ChatService] Error: $data');
_eventController.add(ChatEvent(
type: ChatEventType.error,
message: data['message'] ?? 'Unknown error',
@ -95,7 +92,6 @@ class ChatService {
});
_socket!.on('new-message', (data) {
print('[ChatService] New message: $data');
final message = ChatMessage.fromJson(data as Map<String, dynamic>);
_messageController.add(message);
});
@ -109,7 +105,6 @@ class ChatService {
});
_socket!.on('user-joined', (data) {
print('[ChatService] User joined: $data');
_eventController.add(ChatEvent(
type: ChatEventType.userJoined,
data: data,
@ -117,7 +112,6 @@ class ChatService {
});
_socket!.on('user-left', (data) {
print('[ChatService] User left: $data');
_eventController.add(ChatEvent(
type: ChatEventType.userLeft,
data: data,
@ -125,7 +119,6 @@ class ChatService {
});
_socket!.on('chat-ended', (data) {
print('[ChatService] Chat ended: $data');
_eventController.add(ChatEvent(
type: ChatEventType.chatEnded,
message: data['message'] ?? 'Chat has ended',
@ -133,7 +126,6 @@ class ChatService {
});
_socket!.onDisconnect((_) {
print('[ChatService] Disconnected from WebSocket');
_isConnected = false;
_eventController.add(ChatEvent(
type: ChatEventType.disconnected,
@ -141,7 +133,6 @@ class ChatService {
});
_socket!.onConnectError((error) {
print('[ChatService] Connection error: $error');
_isConnected = false;
if (!completer.isCompleted) {
completer.complete(false);
@ -153,13 +144,9 @@ class ChatService {
// Timeout after 10 seconds
return completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () {
print('[ChatService] Connection timeout');
return false;
},
onTimeout: () => false,
);
} catch (e) {
print('[ChatService] Connection exception: $e');
return false;
}
}
@ -168,7 +155,6 @@ class ChatService {
/// Returns true if message was sent, false if not connected
bool sendMessage(String text) {
if (_socket == null || !_isConnected || _currentTaskId == null) {
print('[ChatService] Cannot send - not connected');
return false;
}

View file

@ -1,30 +1,11 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
// Basic widget test placeholder
// TODO: Add proper widget tests
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:payfrit_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
testWidgets('App smoke test', (WidgetTester tester) async {
// Placeholder test - proper tests should be added
expect(true, isTrue);
});
}