// lib/screens/order_track_screen.dart import 'dart:math'; import 'package:flutter/material.dart'; import '../services/dio_client.dart'; import '../services/order_service.dart'; import 'order_screen.dart'; class OrderTrackScreen extends StatefulWidget { final String orderId; const OrderTrackScreen({super.key, required this.orderId}); @override State createState() => _OrderTrackScreenState(); } class _OrderTrackScreenState extends State with TickerProviderStateMixin { bool loading = true; Map? shipment; Map trackData = {}; late final AnimationController progressController; late final AnimationController shipController; late final AnimationController timelineController; @override void initState() { super.initState(); progressController = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), ); shipController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1600), )..repeat(reverse: true); timelineController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), ); WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); } @override void dispose() { progressController.dispose(); shipController.dispose(); timelineController.dispose(); super.dispose(); } // ---------------- LOAD DATA ---------------- Future _loadData() async { if (!mounted) return; setState(() => loading = true); try { final service = OrderService(DioClient.getInstance(context)); final results = await Future.wait([ service.getShipment(widget.orderId) .catchError((_) => {"success": false}), service.trackOrder(widget.orderId) .catchError((_) => {"success": false}), ]); final shipRes = results[0] as Map; final trackRes = results[1] as Map; shipment = shipRes["success"] == true ? Map.from(shipRes["shipment"] ?? {}) : null; trackData = trackRes["success"] == true ? Map.from(trackRes["track"] ?? {}) : {}; } catch (_) {} final target = _computeProgress(); if (mounted) { try { await progressController.animateTo( target, curve: Curves.easeInOutCubic, ); // Animate timeline timelineController.forward(from: 0); } catch (_) {} } if (mounted) setState(() => loading = false); } // ---------------- PROGRESS LOGIC ---------------- double _computeProgress() { final status = (trackData["shipment_status"] ?? "").toString().toLowerCase(); if (status.contains("delivered")) return 1.0; if (status.contains("dispatched")) return 0.85; if (status.contains("transit")) return 0.65; if (status.contains("loading")) return 0.40; if (status.contains("pending")) return 0.25; return 0.05; } String _fmt(dynamic v) { if (v == null) return "-"; try { final d = DateTime.parse(v.toString()).toLocal(); return "${d.day}/${d.month}/${d.year}"; } catch (_) { return v.toString(); } } // ---------------- SHIPMENT STEPS DATA ---------------- final List> _shipmentSteps = [ { 'title': 'Shipment Ready', 'status_key': 'shipment_ready', 'icon': Icons.inventory, }, { 'title': 'Export Custom', 'status_key': 'export_custom', 'icon': Icons.account_balance, }, { 'title': 'International Transit', 'status_key': 'international_transit', 'icon': Icons.flight, }, { 'title': 'Arrived at India', 'status_key': 'arrived_india', 'icon': Icons.flag, }, { 'title': 'Import Custom', 'status_key': 'import_custom', 'icon': Icons.account_balance_outlined, }, { 'title': 'Warehouse', 'status_key': 'warehouse', 'icon': Icons.warehouse, }, { 'title': 'Domestic Distribution', 'status_key': 'domestic_distribution', 'icon': Icons.local_shipping, }, { 'title': 'Out for Delivery', 'status_key': 'out_for_delivery', 'icon': Icons.delivery_dining, }, { 'title': 'Delivered', 'status_key': 'delivered', 'icon': Icons.verified, }, ]; // ---------------- STATUS MAPPING ---------------- Map _statusToIndex = { 'shipment_ready': 0, 'export_custom': 1, 'international_transit': 2, 'arrived_india': 3, 'import_custom': 4, 'warehouse': 5, 'domestic_distribution': 6, 'out_for_delivery': 7, 'delivered': 8, }; int _getCurrentStepIndex() { final status = (trackData["shipment_status"] ?? "").toString().toLowerCase(); // Try to match exact status key first for (var entry in _statusToIndex.entries) { if (status.contains(entry.key)) { return entry.value; } } // Fallback mappings if (status.contains("delivered")) return 8; if (status.contains("dispatch") || status.contains("out for delivery")) return 7; if (status.contains("distribution")) return 6; if (status.contains("warehouse")) return 5; if (status.contains("import")) return 4; if (status.contains("arrived") || status.contains("india")) return 3; if (status.contains("transit")) return 2; if (status.contains("export")) return 1; if (status.contains("ready") || status.contains("pending")) return 0; return 0; // Default to first step } // ---------------- UI BUILD ---------------- @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; final scale = (width / 430).clamp(0.75, 1.25); return Scaffold( appBar: AppBar( title: const Text("Shipment & Tracking"), elevation: 0.8, ), body: loading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: EdgeInsets.all(16 * scale), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _headerCard(scale), SizedBox(height: 16 * scale), _shipmentSummary(scale), SizedBox(height: 20 * scale), _trackingStatus(scale), SizedBox(height: 16 * scale), // Replace horizontal progress bar with vertical timeline _shipmentTimeline(scale), ], ), ), ); } // ---------------- HEADER ---------------- Widget _headerCard(double scale) { return Container( padding: EdgeInsets.all(14 * scale), decoration: _boxDecoration(scale), child: Row( children: [ Container( padding: EdgeInsets.all(10 * scale), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12 * scale), ), child: Icon(Icons.local_shipping, color: Colors.blue, size: 28 * scale), ), SizedBox(width: 12 * scale), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Order #${trackData['order_id'] ?? '-'}", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16 * scale), ), SizedBox(height: 6 * scale), Text( trackData["shipment_status"] ?? "-", style: TextStyle(color: Colors.black54, fontSize: 13 * scale), ), ], ) ], ), ); } // ---------------- SHIPMENT SUMMARY ---------------- Widget _shipmentSummary(double scale) { if (shipment == null) { return _simpleCard(scale, "Shipment not created yet"); } return Container( padding: EdgeInsets.all(16 * scale), decoration: _boxDecoration(scale), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Shipment Summary", style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)), SizedBox(height: 12 * scale), Container( padding: EdgeInsets.all(12 * scale), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12 * scale), ), child: Row( children: [ Icon(Icons.qr_code_2, color: Colors.blue, size: 22 * scale), SizedBox(width: 10 * scale), Text("Shipment ID: ${shipment!['shipment_id']}", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15 * scale)), ], ), ), SizedBox(height: 14 * scale), _twoCol( "Status", formatStatusLabel(shipment!['status']), scale, ), _twoCol("Shipment Date", _fmt(shipment!['shipment_date']), scale), _twoCol("Origin", shipment!['origin'], scale), _twoCol("Destination", shipment!['destination'], scale), ], ), ); } Widget _simpleCard(double scale, String text) { return Container( padding: EdgeInsets.all(14 * scale), decoration: _boxDecoration(scale), child: Text(text, style: TextStyle(fontSize: 15 * scale, color: Colors.grey.shade700)), ); } Widget _twoCol(String title, dynamic value, double scale) { return Padding( padding: EdgeInsets.symmetric(vertical: 6 * scale), child: Row( children: [ Expanded( child: Text(title, style: TextStyle(color: Colors.grey, fontSize: 13 * scale)), ), Expanded( child: Text( value?.toString() ?? "-", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13 * scale), ), ), ], ), ); } // ---------------- TRACKING STATUS ---------------- Widget _trackingStatus(double scale) { final delivered = _computeProgress() >= 1.0; return Container( padding: EdgeInsets.symmetric(vertical: 10 * scale, horizontal: 14 * scale), decoration: BoxDecoration( gradient: delivered ? const LinearGradient(colors: [Colors.green, Colors.lightGreen]) : const LinearGradient(colors: [Colors.blue, Colors.purple]), borderRadius: BorderRadius.circular(40 * scale), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( delivered ? Icons.verified : Icons.local_shipping, color: Colors.white, size: 16 * scale, ), SizedBox(width: 8 * scale), Text( formatStatusLabel(trackData["shipment_status"]), style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13 * scale, ), ), ], ), ); } // ---------------- VERTICAL SHIPMENT TIMELINE ---------------- Widget _shipmentTimeline(double scale) { final currentStepIndex = _getCurrentStepIndex(); return Container( padding: EdgeInsets.all(16 * scale), decoration: _boxDecoration(scale), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.timeline, color: Colors.blue, size: 20 * scale), SizedBox(width: 8 * scale), Text( "Shipment Progress", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 17 * scale, ), ), ], ), SizedBox(height: 8 * scale), Text( "Tracking your package in real-time", style: TextStyle( color: Colors.grey.shade600, fontSize: 13 * scale, ), ), SizedBox(height: 16 * scale), // Timeline Container AnimatedBuilder( animation: timelineController, builder: (context, child) { return Opacity( opacity: timelineController.value, child: Transform.translate( offset: Offset(0, 20 * (1 - timelineController.value)), child: child, ), ); }, child: _buildTimeline(scale, currentStepIndex), ), ], ), ); } Widget _buildTimeline(double scale, int currentStepIndex) { return Container( padding: EdgeInsets.only(left: 8 * scale), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: _shipmentSteps.length, itemBuilder: (context, index) { final step = _shipmentSteps[index]; final isCompleted = index < currentStepIndex; final isCurrent = index == currentStepIndex; final isLast = index == _shipmentSteps.length - 1; return _buildTimelineStep( scale: scale, step: step, index: index, isCompleted: isCompleted, isCurrent: isCurrent, isLast: isLast, currentStepIndex: currentStepIndex, ); }, ), ); } Widget _buildTimelineStep({ required double scale, required Map step, required int index, required bool isCompleted, required bool isCurrent, required bool isLast, required int currentStepIndex, }) { return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Timeline Column (Line + Icon) Column( children: [ // Top connector line (except for first item) if (index > 0) Container( width: 2 * scale, height: 24 * scale, color: isCompleted ? Colors.green : Colors.grey.shade300, ), // Step Icon with animation _buildStepIcon(scale, isCompleted, isCurrent, step['icon']), // Bottom connector line (except for last item) if (!isLast) Container( width: 2 * scale, height: 24 * scale, color: index < currentStepIndex ? Colors.green : Colors.grey.shade300, ), ], ), SizedBox(width: 16 * scale), // Step Content Expanded( child: Container( margin: EdgeInsets.only(bottom: 20 * scale), padding: EdgeInsets.all(14 * scale), decoration: BoxDecoration( color: isCurrent ? Colors.blue.shade50 : Colors.transparent, borderRadius: BorderRadius.circular(12 * scale), border: isCurrent ? Border.all(color: Colors.blue.shade200, width: 1.5 * scale) : null, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( step['title'], style: TextStyle( fontSize: 15 * scale, fontWeight: FontWeight.w600, color: isCurrent ? Colors.blue.shade800 : isCompleted ? Colors.green.shade800 : Colors.grey.shade800, ), ), ), if (isCompleted) Icon(Icons.check_circle, color: Colors.green, size: 18 * scale ), ], ), SizedBox(height: 4 * scale), // Optional timestamp - you can customize this with actual timestamps from API _buildStepTimestamp(scale, step, isCompleted, isCurrent, index), ], ), ), ), ], ), ); } Widget _buildStepIcon(double scale, bool isCompleted, bool isCurrent, IconData iconData) { // Animation for current step if (isCurrent) { return AnimatedBuilder( animation: shipController, builder: (context, child) { return Container( width: 32 * scale * (1 + 0.2 * shipController.value), height: 32 * scale * (1 + 0.2 * shipController.value), decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.blue.shade50, border: Border.all( color: Colors.blue, width: 2 * scale, ), ), child: Icon( iconData, color: Colors.blue, size: 18 * scale, ), ); }, ); } // Completed step if (isCompleted) { return Container( width: 32 * scale, height: 32 * scale, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.green.shade50, border: Border.all( color: Colors.green, width: 2 * scale, ), ), child: Icon( Icons.check, color: Colors.green, size: 18 * scale, ), ); } // Upcoming step return Container( width: 32 * scale, height: 32 * scale, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.grey.shade100, border: Border.all( color: Colors.grey.shade400, width: 2 * scale, ), ), child: Icon( iconData, color: Colors.grey.shade600, size: 18 * scale, ), ); } Widget _buildStepTimestamp(double scale, Map step, bool isCompleted, bool isCurrent, int index) { // You can replace this with actual timestamps from your API final now = DateTime.now(); final estimatedTime = now.add(Duration(days: index)); String statusText = isCurrent ? "In progress" : isCompleted ? "Completed" : "Estimated ${estimatedTime.day}/${estimatedTime.month}"; Color statusColor = isCurrent ? Colors.blue.shade600 : isCompleted ? Colors.green.shade600 : Colors.grey.shade600; return Row( children: [ Icon( isCurrent ? Icons.access_time : Icons.calendar_today, color: statusColor, size: 14 * scale, ), SizedBox(width: 4 * scale), Text( statusText, style: TextStyle( fontSize: 12 * scale, color: statusColor, fontWeight: FontWeight.w500, ), ), ], ); } BoxDecoration _boxDecoration(double scale) { return BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12 * scale), boxShadow: [ BoxShadow( color: Colors.black12.withOpacity(0.05), blurRadius: 10 * scale, offset: Offset(0, 4 * scale), ) ], ); } }