feat: standard error handling, web bug fixes, and mock data setup
This commit is contained in:
@@ -1,28 +1,157 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import 'package:doormile/shared/models/models.dart';
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/utils/snackbar_utils.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
|
||||
class LiveTrackingScreen extends StatelessWidget {
|
||||
class LiveTrackingScreen extends StatefulWidget {
|
||||
final Order? order;
|
||||
const LiveTrackingScreen({super.key, this.order});
|
||||
|
||||
@override
|
||||
State<LiveTrackingScreen> createState() => _LiveTrackingScreenState();
|
||||
}
|
||||
|
||||
class _LiveTrackingScreenState extends State<LiveTrackingScreen> {
|
||||
static const _defaultCenter = LatLng(11.02, 76.95);
|
||||
|
||||
WebSocketChannel? _channel;
|
||||
GoogleMapController? _mapController;
|
||||
|
||||
LatLng _milerPosition = _defaultCenter;
|
||||
Set<Marker> _markers = {};
|
||||
int _etaSeconds = 0;
|
||||
String _milerName = 'Assigning Miler...';
|
||||
String _status = 'In transit';
|
||||
Timer? _etaTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connectWebSocket();
|
||||
}
|
||||
|
||||
void _connectWebSocket() {
|
||||
final bookingId = widget.order?.id;
|
||||
if (bookingId == null || bookingId.isEmpty) return;
|
||||
try {
|
||||
_channel = WebSocketChannel.connect(
|
||||
Uri.parse('ws://api.doormile.com/ws/bookings/$bookingId/track'),
|
||||
);
|
||||
_channel!.stream.listen(
|
||||
_onMessage,
|
||||
onError: (e) => debugPrint('WS error: $e'),
|
||||
onDone: () => debugPrint('WS closed'),
|
||||
cancelOnError: false,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('WS connect error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _onMessage(dynamic raw) {
|
||||
final Map<String, dynamic> data;
|
||||
try {
|
||||
data = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint('WS parse error: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
final lat = (data['lat'] as num?)?.toDouble();
|
||||
final lon = (data['lon'] as num?)?.toDouble();
|
||||
final etaMinutes = (data['eta_minutes'] as num?)?.toInt();
|
||||
final milerName = data['miler_name'] as String?;
|
||||
final status = data['status'] as String?;
|
||||
bool positionChanged = false;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
if (milerName != null && milerName.isNotEmpty) {
|
||||
_milerName = milerName;
|
||||
}
|
||||
if (status != null && status.isNotEmpty) {
|
||||
_status = status;
|
||||
}
|
||||
if (lat != null && lon != null && (lat != 0.0 || lon != 0.0)) {
|
||||
_milerPosition = LatLng(lat, lon);
|
||||
positionChanged = true;
|
||||
_markers = {
|
||||
Marker(
|
||||
markerId: const MarkerId('miler'),
|
||||
position: _milerPosition,
|
||||
infoWindow: InfoWindow(title: _milerName),
|
||||
),
|
||||
};
|
||||
}
|
||||
if (etaMinutes != null) {
|
||||
_etaSeconds = etaMinutes * 60;
|
||||
}
|
||||
});
|
||||
|
||||
if (etaMinutes != null) _restartEtaTimer();
|
||||
|
||||
if (positionChanged) {
|
||||
_mapController?.animateCamera(
|
||||
CameraUpdate.newLatLng(_milerPosition),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _restartEtaTimer() {
|
||||
_etaTimer?.cancel();
|
||||
if (_etaSeconds <= 0) return;
|
||||
_etaTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
if (_etaSeconds > 0) _etaSeconds--;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
String get _etaLabel {
|
||||
if (_etaSeconds > 0) {
|
||||
return '${(_etaSeconds / 60).ceil()} min';
|
||||
}
|
||||
return widget.order?.eta ?? '—';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_etaTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
_mapController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final id = order?.id ?? 'DM-CHN-BLR-7741';
|
||||
final eta = order?.eta ?? '2:30 PM';
|
||||
final id = widget.order?.id ?? 'DM-CHN-BLR-7741';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surface,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Faux map
|
||||
Positioned.fill(
|
||||
child: CustomPaint(painter: _MapPainter()),
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: const CameraPosition(
|
||||
target: _defaultCenter,
|
||||
zoom: 14,
|
||||
),
|
||||
markers: _markers,
|
||||
onMapCreated: (controller) => _mapController = controller,
|
||||
myLocationButtonEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
),
|
||||
),
|
||||
// Top bar
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -46,7 +175,6 @@ class LiveTrackingScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom detail sheet
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
@@ -81,14 +209,14 @@ class LiveTrackingScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
StatusChip(
|
||||
label: 'In transit',
|
||||
label: _status,
|
||||
color: AppColors.tertiary,
|
||||
bg: AppColors.tertiaryFixed.withValues(alpha: 0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('ETA $eta',
|
||||
Text('ETA $_etaLabel',
|
||||
style: AppTheme.labelMd.copyWith(color: AppColors.primary)),
|
||||
const SizedBox(height: 16),
|
||||
const _MileStepper(activeIndex: 2),
|
||||
@@ -139,7 +267,7 @@ class LiveTrackingScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Rahul Sharma',
|
||||
Text(_milerName,
|
||||
style: AppTheme.labelMd.copyWith(fontWeight: FontWeight.w700)),
|
||||
Row(
|
||||
children: [
|
||||
@@ -195,7 +323,14 @@ class LiveTrackingScreen extends StatelessWidget {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => Navigator.pushNamed(context, '/chat', arguments: 'Rahul Sharma'),
|
||||
onPressed: () => Navigator.pushNamed(
|
||||
context,
|
||||
'/chat',
|
||||
arguments: {
|
||||
'milerName': _milerName,
|
||||
'bookingId': widget.order?.id ?? '',
|
||||
},
|
||||
),
|
||||
icon: const Icon(Icons.chat_bubble_outline, size: 18),
|
||||
label: const Text('Chat'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -216,13 +351,7 @@ class LiveTrackingScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _toast(BuildContext context, String msg) {
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(
|
||||
content: Text(msg),
|
||||
backgroundColor: AppColors.inverseSurface,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
SnackbarUtils.showError(context, msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +403,9 @@ class _MileStepper extends StatelessWidget {
|
||||
height: 3,
|
||||
color: i == _stops.length - 1
|
||||
? Colors.transparent
|
||||
: (i < activeIndex ? _stops[i + 1].$2 : AppColors.surfaceContainerHigh),
|
||||
: (i < activeIndex
|
||||
? _stops[i + 1].$2
|
||||
: AppColors.surfaceContainerHigh),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -292,71 +423,3 @@ class _MileStepper extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight stylised street map with a dashed delivery route.
|
||||
class _MapPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final bg = Paint()..color = const Color(0xFFEDEAE9);
|
||||
canvas.drawRect(Offset.zero & size, bg);
|
||||
|
||||
final road = Paint()
|
||||
..color = Colors.white
|
||||
..strokeWidth = 10;
|
||||
final thin = Paint()
|
||||
..color = const Color(0xFFE0DCDB)
|
||||
..strokeWidth = 4;
|
||||
|
||||
for (double x = 30; x < size.width; x += 70) {
|
||||
canvas.drawLine(Offset(x, 0), Offset(x, size.height), thin);
|
||||
}
|
||||
for (double y = 80; y < size.height; y += 70) {
|
||||
canvas.drawLine(Offset(0, y), Offset(size.width, y), thin);
|
||||
}
|
||||
canvas.drawLine(Offset(0, size.height * 0.35),
|
||||
Offset(size.width, size.height * 0.42), road);
|
||||
canvas.drawLine(Offset(size.width * 0.4, 0),
|
||||
Offset(size.width * 0.5, size.height * 0.7), road);
|
||||
|
||||
// Dashed route
|
||||
final routePaint = Paint()
|
||||
..color = AppColors.primary
|
||||
..strokeWidth = 4
|
||||
..style = PaintingStyle.stroke;
|
||||
final path = Path()
|
||||
..moveTo(size.width * 0.2, size.height * 0.6)
|
||||
..quadraticBezierTo(size.width * 0.35, size.height * 0.3,
|
||||
size.width * 0.55, size.height * 0.35)
|
||||
..quadraticBezierTo(size.width * 0.75, size.height * 0.4,
|
||||
size.width * 0.7, size.height * 0.18);
|
||||
_drawDashed(canvas, path, routePaint);
|
||||
|
||||
// Origin & destination markers
|
||||
final dot = Paint()..color = AppColors.primary;
|
||||
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 7, dot);
|
||||
canvas.drawCircle(Offset(size.width * 0.2, size.height * 0.6), 3,
|
||||
Paint()..color = Colors.white);
|
||||
canvas.drawCircle(Offset(size.width * 0.7, size.height * 0.18), 9,
|
||||
Paint()..color = AppColors.primary);
|
||||
// Vehicle marker (mid route)
|
||||
final mid = Offset(size.width * 0.55, size.height * 0.35);
|
||||
canvas.drawCircle(mid, 16, Paint()..color = AppColors.primary.withValues(alpha: 0.2));
|
||||
canvas.drawCircle(mid, 9, Paint()..color = AppColors.tertiary);
|
||||
}
|
||||
|
||||
void _drawDashed(Canvas canvas, Path path, Paint paint) {
|
||||
const dash = 8.0;
|
||||
const gap = 6.0;
|
||||
for (final metric in path.computeMetrics()) {
|
||||
double dist = 0;
|
||||
while (dist < metric.length) {
|
||||
canvas.drawPath(
|
||||
metric.extractPath(dist, dist + dash), paint);
|
||||
dist += dash + gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user