import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; import '../../../controller/functions/launch.dart'; import '../../../controller/home/profile/order_history_controller.dart'; import '../../widgets/my_scafold.dart'; import '../../widgets/mycircular.dart'; // ───────────────────────────────────────────────────────────────────────────── // Main Screen // ───────────────────────────────────────────────────────────────────────────── class OrderHistory extends StatelessWidget { const OrderHistory({super.key}); @override Widget build(BuildContext context) { Get.put(OrderHistoryController()); return MyScafolld( title: 'Order History'.tr, isleading: true, body: [ GetBuilder( builder: (controller) { if (controller.isloading) { return const MyCircularProgressIndicator(); } if (controller.orderHistoryListPassenger.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.route_outlined, size: 80, color: AppColor.writeColor.withOpacity(0.3)), const SizedBox(height: 16), Text('No trip history found'.tr, style: AppStyle.headTitle2), const SizedBox(height: 6), Text('Your past trips will appear here.'.tr, style: AppStyle.subtitle), ], ), ); } return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), itemCount: controller.orderHistoryListPassenger.length, separatorBuilder: (_, __) => const SizedBox(height: 14), itemBuilder: (context, index) { final ride = controller.orderHistoryListPassenger[index]; return _HistoryCard( key: ValueKey(ride['id'] ?? index), ride: ride, ); }, ); }, ), ], ); } } // ───────────────────────────────────────────────────────────────────────────── // Coordinate helpers // ───────────────────────────────────────────────────────────────────────────── LatLng _parseLatLng(String raw, LatLng fallback) { try { final parts = raw.split(','); return LatLng(double.parse(parts[0]), double.parse(parts[1])); } catch (_) { return fallback; } } const LatLng _kDamascus = LatLng(33.5, 36.3); // ───────────────────────────────────────────────────────────────────────────── // Lightweight card — NO native map in the list // ───────────────────────────────────────────────────────────────────────────── class _HistoryCard extends StatelessWidget { final Map ride; const _HistoryCard({Key? key, required this.ride}) : super(key: key); @override Widget build(BuildContext context) { final start = _parseLatLng(ride['start_location'] ?? '', _kDamascus); final end = _parseLatLng(ride['end_location'] ?? '', _kDamascus); final status = ride['status'] ?? ''; return Material( color: Colors.transparent, child: InkWell( onTap: () => _openDetail(context, ride, start, end), borderRadius: BorderRadius.circular(18), child: Ink( decoration: BoxDecoration( color: AppColor.secondaryColor, borderRadius: BorderRadius.circular(18), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.12), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Lightweight route preview (pure Flutter, zero native cost) ── ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(18), topRight: Radius.circular(18), ), child: SizedBox( height: 130, width: double.infinity, child: CustomPaint( painter: _RoutePainter(start: start, end: end), child: Align( alignment: Alignment.bottomRight, child: Padding( padding: const EdgeInsets.all(8), child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.45), borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.map_outlined, color: Colors.white, size: 13), const SizedBox(width: 4), Text('View Map'.tr, style: const TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600)), ], ), ), ), ), ), ), ), // ── Details ────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon(Icons.access_time_rounded, size: 14, color: AppColor.writeColor.withOpacity(0.5)), const SizedBox(width: 4), Text( '${ride['date']} · ${ride['time']}', style: AppStyle.subtitle.copyWith( fontSize: 12, color: AppColor.writeColor.withOpacity(0.6)), ), ], ), _StatusChip(status: status), ], ), const Divider(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Total Price'.tr, style: AppStyle.title.copyWith(fontSize: 15)), Text( '${ride['price']} ${'SYP'.tr}', style: AppStyle.headTitle.copyWith( fontSize: 20, color: AppColor.primaryColor), ), ], ), ], ), ), ], ), ), ), ); } void _openDetail(BuildContext context, Map ride, LatLng start, LatLng end) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _RideDetailSheet(ride: ride, start: start, end: end), ); } } // ───────────────────────────────────────────────────────────────────────────── // Pure-Flutter route painter — grid background + animated dashed line // ───────────────────────────────────────────────────────────────────────────── class _RoutePainter extends CustomPainter { final LatLng start; final LatLng end; const _RoutePainter({required this.start, required this.end}); @override void paint(Canvas canvas, Size size) { // Background gradient final bgPaint = Paint() ..shader = LinearGradient( colors: [ AppColor.primaryColor.withOpacity(0.08), AppColor.primaryColor.withOpacity(0.18), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), bgPaint); // Subtle grid final gridPaint = Paint() ..color = AppColor.primaryColor.withOpacity(0.06) ..strokeWidth = 1; const step = 20.0; for (double x = 0; x < size.width; x += step) { canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); } for (double y = 0; y < size.height; y += step) { canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); } // Map lat/lng to canvas — simple linear projection final minLat = math.min(start.latitude, end.latitude); final maxLat = math.max(start.latitude, end.latitude); final minLng = math.min(start.longitude, end.longitude); final maxLng = math.max(start.longitude, end.longitude); final latRange = maxLat - minLat; final lngRange = maxLng - minLng; final pad = 32.0; Offset project(LatLng p) { double x, y; if (lngRange < 0.0005) { x = size.width / 2; } else { x = pad + ((p.longitude - minLng) / lngRange) * (size.width - 2 * pad); } if (latRange < 0.0005) { y = size.height / 2; } else { // Invert y (latitude grows up, canvas grows down) y = size.height - pad - ((p.latitude - minLat) / latRange) * (size.height - 2 * pad); } return Offset(x, y); } final startPt = project(start); final endPt = project(end); // Dashed route line final linePaint = Paint() ..color = AppColor.primaryColor ..strokeWidth = 2.5 ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; _drawDashedLine(canvas, startPt, endPt, linePaint, 8, 5); // Glow behind markers final glowPaint = Paint() ..color = AppColor.primaryColor.withOpacity(0.2) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8); canvas.drawCircle(startPt, 14, glowPaint); canvas.drawCircle(endPt, 14, glowPaint); // Start dot (A) _drawMarker(canvas, startPt, AppColor.primaryColor, 'A'); // End dot (B) _drawMarker(canvas, endPt, AppColor.redColor, 'B'); } void _drawDashedLine(Canvas canvas, Offset p1, Offset p2, Paint paint, double dashLen, double gapLen) { final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy; final dist = math.sqrt(dx * dx + dy * dy); if (dist == 0) return; final ux = dx / dist; final uy = dy / dist; double traveled = 0; bool drawing = true; while (traveled < dist) { final segLen = drawing ? dashLen : gapLen; final next = math.min(traveled + segLen, dist); if (drawing) { canvas.drawLine( Offset(p1.dx + ux * traveled, p1.dy + uy * traveled), Offset(p1.dx + ux * next, p1.dy + uy * next), paint, ); } traveled = next; drawing = !drawing; } } void _drawMarker(Canvas canvas, Offset center, Color color, String label) { // Outer ring canvas.drawCircle(center, 12, Paint()..color = color.withOpacity(0.25)); // Solid circle canvas.drawCircle(center, 8, Paint()..color = color); // White inner canvas.drawCircle(center, 4, Paint()..color = Colors.white); // Label text final tp = TextPainter( text: TextSpan( text: label, style: TextStyle( color: color, fontSize: 7, fontWeight: FontWeight.w900)), textDirection: TextDirection.ltr, )..layout(); tp.paint(canvas, center - Offset(tp.width / 2, tp.height / 2)); } @override bool shouldRepaint(_RoutePainter old) => old.start != start || old.end != end; } // ───────────────────────────────────────────────────────────────────────────── // Status chip // ───────────────────────────────────────────────────────────────────────────── class _StatusChip extends StatelessWidget { final String status; const _StatusChip({required this.status}); @override Widget build(BuildContext context) { Color color; IconData icon; if (status == 'Canceled'.tr) { color = AppColor.redColor; icon = Icons.cancel_outlined; } else if (status == 'Finished'.tr) { color = AppColor.greenColor; icon = Icons.check_circle_outline; } else { color = AppColor.yellowColor; icon = Icons.hourglass_empty_rounded; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.12), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: color, size: 13), const SizedBox(width: 4), Text(status, style: AppStyle.subtitle.copyWith( color: color, fontWeight: FontWeight.bold, fontSize: 11)), ], ), ); } } // ───────────────────────────────────────────────────────────────────────────── // Detail bottom sheet — ONE MapLibre instance, only when user requests it // ───────────────────────────────────────────────────────────────────────────── class _RideDetailSheet extends StatefulWidget { final Map ride; final LatLng start; final LatLng end; const _RideDetailSheet( {required this.ride, required this.start, required this.end}); @override State<_RideDetailSheet> createState() => _RideDetailSheetState(); } class _RideDetailSheetState extends State<_RideDetailSheet> { MapLibreMapController? _mc; LatLngBounds? get _bounds { final latDiff = (widget.start.latitude - widget.end.latitude).abs(); final lngDiff = (widget.start.longitude - widget.end.longitude).abs(); if (latDiff < 0.0005 && lngDiff < 0.0005) return null; return LatLngBounds( northeast: LatLng( math.max(widget.start.latitude, widget.end.latitude), math.max(widget.start.longitude, widget.end.longitude), ), southwest: LatLng( math.min(widget.start.latitude, widget.end.latitude), math.min(widget.start.longitude, widget.end.longitude), ), ); } void _onMapCreated(MapLibreMapController c) => _mc = c; Future _onStyleLoaded() async { WidgetsBinding.instance.addPostFrameCallback((_) async { await Future.delayed(const Duration(milliseconds: 400)); if (!mounted || _mc == null) return; await _draw(); }); } Future _draw() async { final mc = _mc; if (mc == null || !mounted) return; try { final aData = await rootBundle.load('assets/images/A.png'); await mc.addImage('det_start', aData.buffer.asUint8List()); final bData = await rootBundle.load('assets/images/b.png'); await mc.addImage('det_end', bData.buffer.asUint8List()); } catch (_) {} await mc.addLine(LineOptions( geometry: [widget.start, widget.end], lineColor: '#${AppColor.primaryColor.value.toRadixString(16).substring(2)}', lineWidth: 4.0, lineOpacity: 1.0, )); await mc.addSymbol(SymbolOptions( geometry: widget.start, iconImage: 'det_start', iconSize: 0.8, iconAnchor: 'bottom', )); await mc.addSymbol(SymbolOptions( geometry: widget.end, iconImage: 'det_end', iconSize: 0.8, iconAnchor: 'bottom', )); final b = _bounds; if (b != null) { await mc.animateCamera(CameraUpdate.newLatLngBounds(b, left: 60, top: 60, right: 60, bottom: 60)); } else { await mc.animateCamera(CameraUpdate.newLatLngZoom(widget.start, 14)); } } @override void dispose() { _mc?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final ride = widget.ride; final center = LatLng( (widget.start.latitude + widget.end.latitude) / 2, (widget.start.longitude + widget.end.longitude) / 2, ); return DraggableScrollableSheet( initialChildSize: 0.88, minChildSize: 0.5, maxChildSize: 0.95, builder: (_, scrollController) => Container( decoration: BoxDecoration( color: AppColor.secondaryColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ // Handle Container( margin: const EdgeInsets.symmetric(vertical: 10), width: 40, height: 4, decoration: BoxDecoration( color: AppColor.writeColor.withOpacity(0.2), borderRadius: BorderRadius.circular(2), ), ), // Map — only ONE instance, created on demand Expanded( child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), child: MapLibreMap( styleString: 'assets/style.json', initialCameraPosition: CameraPosition(target: center, zoom: 12), onMapCreated: _onMapCreated, onStyleLoadedCallback: _onStyleLoaded, myLocationEnabled: false, trackCameraPosition: false, ), ), ), // Trip info strip Container( padding: const EdgeInsets.fromLTRB(20, 14, 20, 20), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${ride['date']} · ${ride['time']}', style: AppStyle.subtitle.copyWith( fontSize: 12, color: AppColor.writeColor.withOpacity(0.55))), const SizedBox(height: 2), Text('${ride['price']} ${'SYP'.tr}', style: AppStyle.headTitle.copyWith( fontSize: 22, color: AppColor.primaryColor)), ], ), _StatusChip(status: ride['status'] ?? ''), ], ), const SizedBox(height: 14), // Open in Google Maps SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { final url = 'https://www.google.com/maps/dir/${ride['start_location']}/${ride['end_location']}/'; showInBrowser(url); }, icon: const Icon(Icons.open_in_new, size: 16), label: Text('Open in Google Maps'.tr), style: ElevatedButton.styleFrom( backgroundColor: AppColor.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), ), ), ], ), ), ], ), ), ); } }