payfrit-app/lib/screens/restaurant_select_screen.dart
John Mizerek 0a8c12c1d3 Enhance UI with Material Design 3 and fix cart quantity handling
UI Improvements:
- Menu items displayed as attractive cards with icons and better typography
- Restaurant selection upgraded to card-based layout with shadows
- Animated pulsing beacon scanner with gradient effect
- Enhanced item customization sheet with drag handle and pill-style pricing
- Category headers with highlighted background and borders
- Business and service point names now shown in app bar

Persistent Login:
- Created AuthStorage service for credential persistence using SharedPreferences
- Auto-restore authentication on app launch
- Seamless login flow: scan → browse → login on cart add
- Users stay logged in across app restarts

Cart Functionality Fixes:
- Fixed duplicate item handling: now properly increments quantity
- Prevented adding inactive items by skipping unselected modifiers
- Fixed self-referential items (item cannot be its own child)
- Added debug logging for cart state tracking
- Success messages now show accurate item counts

Technical Improvements:
- AppState tracks business/service point names for display
- Beacon scanner passes location names through navigation
- Quantity calculation checks existing cart items before adding
- Better null safety with firstOrNull pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 09:40:23 -08:00

246 lines
7.9 KiB
Dart

// lib/screens/restaurant_select_screen.dart
import "package:flutter/material.dart";
import "package:provider/provider.dart";
import "../app/app_router.dart";
import "../app/app_state.dart";
import "../models/restaurant.dart";
import "../models/service_point.dart";
import "../services/api.dart";
class RestaurantSelectScreen extends StatefulWidget {
const RestaurantSelectScreen({super.key});
@override
State<RestaurantSelectScreen> createState() => _RestaurantSelectScreenState();
}
class _RestaurantSelectScreenState extends State<RestaurantSelectScreen> {
late Future<List<Restaurant>> _future;
String? _debugLastRaw;
int? _debugLastStatus;
@override
void initState() {
super.initState();
_future = _load();
}
Future<List<Restaurant>> _load() async {
final raw = await Api.listRestaurantsRaw();
_debugLastRaw = raw.rawBody;
_debugLastStatus = raw.statusCode;
return Api.listRestaurants();
}
Future<void> _selectBusinessAndContinue(Restaurant r) async {
final appState = context.read<AppState>();
// You MUST have a userId for ordering.
final userId = appState.userId;
if (userId == null || userId <= 0) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Missing UserID (not logged in).")),
);
return;
}
// Set selected business
appState.setBusiness(r.businessId);
// Go pick service point, and WAIT for a selection.
final sp = await Navigator.of(context).pushNamed(
AppRoutes.servicePointSelect,
arguments: {
"BusinessID": r.businessId,
"UserID": userId,
},
);
if (!mounted) return;
if (sp is ServicePoint) {
// Store selection in AppState
appState.setServicePoint(sp.servicePointId);
// Navigate to Menu Browse
Navigator.of(context).pushNamed(
AppRoutes.menuBrowse,
arguments: {
"BusinessID": r.businessId,
"ServicePointID": sp.servicePointId,
"UserID": userId,
},
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Select Business"),
),
body: FutureBuilder<List<Restaurant>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return _ErrorPane(
title: "Businesses Load Failed",
message: snapshot.error.toString(),
statusCode: _debugLastStatus,
raw: _debugLastRaw,
onRetry: () => setState(() => _future = _load()),
);
}
final items = snapshot.data ?? const <Restaurant>[];
if (items.isEmpty) {
return _ErrorPane(
title: "No Businesses Returned",
message: "The API returned an empty list.",
statusCode: _debugLastStatus,
raw: _debugLastRaw,
onRetry: () => setState(() => _future = _load()),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, i) {
final r = items[i];
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.surface,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _selectBusinessAndContinue(r),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.store,
size: 32,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
r.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
"Tap to view menu",
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
],
),
),
),
),
);
},
);
},
),
);
}
}
class _ErrorPane extends StatelessWidget {
final String title;
final String message;
final int? statusCode;
final String? raw;
final VoidCallback onRetry;
const _ErrorPane({
required this.title,
required this.message,
required this.statusCode,
required this.raw,
required this.onRetry,
});
@override
Widget build(BuildContext context) {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 10),
Text(message),
const SizedBox(height: 14),
if (statusCode != null) Text("HTTP: $statusCode"),
if (raw != null && raw!.trim().isNotEmpty) ...[
const SizedBox(height: 10),
const Text("Raw response:"),
const SizedBox(height: 6),
Text(raw!),
],
const SizedBox(height: 14),
FilledButton(
onPressed: onRetry,
child: const Text("Retry"),
),
],
),
),
),
),
),
);
}
}