payfrit-app/lib/screens/restaurant_select_screen.dart
John Mizerek 9995eb2ff7 feat: implement full recursive menu customization system
- Add MenuItem model with hierarchical structure support
- Implement recursive menu browsing with infinite depth support
- Add ExpansionTile for collapsible modifier sections
- Implement radio/checkbox logic based on ItemMaxNumSelectionReq
- Add automatic pre-selection for ItemIsCheckedByDefault items
- Implement validation for ItemRequiresChildSelection and max limits
- Add recursive price calculation across all depth levels
- Support intelligent selection behavior (radio groups, parent/child deselection)
- Add proper error messaging for validation failures
- Connect menu items API endpoint
- Update navigation flow to menu browse after service point selection
2025-12-29 10:32:31 -08:00

184 lines
5.2 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.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, i) {
final r = items[i];
return ListTile(
title: Text(r.name),
trailing: const Icon(Icons.chevron_right),
onTap: () => _selectBusinessAndContinue(r),
);
},
);
},
),
);
}
}
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"),
),
],
),
),
),
),
),
);
}
}