- Add LoginScreen with form validation and error handling - Add Api.login() method with LoginResponse model - Add login route to AppRouter - Update SplashScreen to check auth status and route to login if needed - Store auth token in Api service for authenticated requests - Fix restaurant selection to work with authenticated users
303 lines
8.2 KiB
Dart
303 lines
8.2 KiB
Dart
// 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!),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|