feat: standard error handling, web bug fixes, and mock data setup

This commit is contained in:
2026-06-26 16:06:18 +05:30
parent 401cf600e8
commit 523609796e
18 changed files with 792 additions and 164 deletions

View File

@@ -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;
}