changes in order track file

This commit is contained in:
divya abdar
2025-12-25 10:26:47 +05:30
9 changed files with 659 additions and 410 deletions

View File

@@ -16,6 +16,12 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
Map order = {};
final Map<String, bool> _expanded = {};
bool confirming = false;
bool get isOrderPlaced => order['status'] == 'order_placed';
bool get isConfirmed => order['status'] != 'order_placed';
@override
void initState() {
super.initState();
@@ -65,6 +71,10 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
_itemsSection(items, scale),
SizedBox(height: 18 * scale),
_totalsSection(scale),
SizedBox(height: 24 * scale),
_confirmOrderButton(scale),
],
),
),
@@ -72,6 +82,69 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
);
}
Widget _confirmOrderButton(double scale) {
final isPlaced = order['status'] == 'order_placed';
return SizedBox(
width: double.infinity,
height: 52 * scale,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isPlaced ? Colors.orange : Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14 * scale),
),
),
onPressed: (!isPlaced || confirming)
? null
: () async {
setState(() => confirming = true);
final service =
OrderService(DioClient.getInstance(context));
final res =
await service.confirmOrder(order['order_id']);
confirming = false;
if (res['success'] == true) {
setState(() {
order['status'] = 'order_confirmed';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Order confirmed successfully'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(res['message'] ?? 'Failed to confirm order'),
backgroundColor: Colors.red,
),
);
}
setState(() {});
},
child: confirming
? const CircularProgressIndicator(color: Colors.white)
: Text(
isPlaced ? 'Confirm Order' : 'Order Confirmed',
style: TextStyle(
fontSize: 16 * scale,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
// -----------------------------
// SUMMARY CARD
// -----------------------------
@@ -267,7 +340,7 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
Text("Totalss",
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
_totalRow("Total Qty", order['ttl_qty'], scale),

View File

@@ -32,64 +32,24 @@ class _OrdersScreenState extends State<OrdersScreen> {
}
final screenWidth = MediaQuery.of(context).size.width;
final scale = (screenWidth / 420).clamp(0.85, 1.15);
final scale = (screenWidth / 420).clamp(0.85, 1.1);
final filteredOrders = provider.orders.where((o) {
final q = searchQuery.toLowerCase();
return o["order_id"].toString().toLowerCase().contains(q) ||
o["status"].toString().toLowerCase().contains(q) ||
o["description"].toString().toLowerCase().contains(q) ||
o["amount"].toString().toLowerCase().contains(q);
o["description"].toString().toLowerCase().contains(q);
}).toList();
return Column(
children: [
// SEARCH BAR
Container(
margin: EdgeInsets.fromLTRB(16 * scale, 16 * scale, 16 * scale, 10 * scale),
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.12),
blurRadius: 10 * scale,
spreadRadius: 1 * scale,
offset: Offset(0, 4 * scale),
),
],
),
child: Row(
children: [
Icon(Icons.search, size: 22 * scale, color: Colors.grey.shade700),
SizedBox(width: 10 * scale),
Expanded(
child: TextField(
onChanged: (value) => setState(() => searchQuery = value),
style: TextStyle(fontSize: 15 * scale),
decoration: InputDecoration(
hintText: "Search orders...",
hintStyle: TextStyle(
fontSize: 14 * scale,
color: Colors.grey.shade600,
),
border: InputBorder.none,
),
),
),
],
),
),
// ORDER LIST
_searchBar(scale),
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16 * scale),
padding: EdgeInsets.all(14 * scale),
itemCount: filteredOrders.length,
itemBuilder: (context, i) {
final order = filteredOrders[i];
return _orderCard(order, scale);
return _orderCard(filteredOrders[i], scale);
},
),
),
@@ -97,106 +57,175 @@ class _OrdersScreenState extends State<OrdersScreen> {
);
}
Widget _searchBar(double scale) {
return Container(
margin: EdgeInsets.fromLTRB(14 * scale, 14 * scale, 14 * scale, 8 * scale),
padding: EdgeInsets.symmetric(horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14 * scale),
boxShadow: [
BoxShadow(
color: Colors.black12.withOpacity(0.1),
blurRadius: 8 * scale,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
Icon(Icons.search, size: 20 * scale, color: Colors.grey.shade700),
SizedBox(width: 8 * scale),
Expanded(
child: TextField(
onChanged: (v) => setState(() => searchQuery = v),
decoration: const InputDecoration(
hintText: "Search orders...",
border: InputBorder.none,
),
),
),
],
),
);
}
Widget _orderCard(Map<String, dynamic> o, double scale) {
final progress = getProgress(o['status']);
final badgeColor = getStatusColor(o['status']);
final percent = (progress * 100).toInt();
return Card(
elevation: 3 * scale,
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
color: Colors.white,
margin: EdgeInsets.only(bottom: 16 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale),
borderRadius: BorderRadius.circular(14),
),
child: Padding(
padding: EdgeInsets.all(16 * scale),
padding: EdgeInsets.all(12 * scale), // 👈 tighter padding
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// HEADER
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Order #${o['order_id']}",
style: TextStyle(
fontSize: 18 * scale,
fontWeight: FontWeight.bold,
),
),
Container(
padding: EdgeInsets.symmetric(
horizontal: 12 * scale,
vertical: 6 * scale,
),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(12 * scale),
),
child: Text(
o['status'],
style: TextStyle(
color: badgeColor,
fontWeight: FontWeight.bold,
fontSize: 13 * scale,
),
fontSize: 15 * scale, // 👈 slightly smaller
fontWeight: FontWeight.w600,
),
),
getStatusBadge(o['status'], scale),
],
),
SizedBox(height: 10 * scale),
Text(o['description'], style: TextStyle(fontSize: 14 * scale)),
SizedBox(height: 5 * scale),
SizedBox(height: 6 * scale),
Text(
o['description'],
style: TextStyle(fontSize: 13 * scale),
),
SizedBox(height: 4 * scale),
Text(
"${o['amount']}",
style: TextStyle(
fontSize: 16 * scale,
fontSize: 15 * scale,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 18 * scale),
_AnimatedProgressBar(progress: progress, scale: scale),
SizedBox(height: 18 * scale),
SizedBox(height: 12 * scale),
// PROGRESS HEADER
Align(
alignment: Alignment.centerRight,
child: Text(
"$percent%",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 11 * scale,
color: Colors.grey.shade700,
),
),
),
_AnimatedProgressBar(progress: progress, scale: scale),
SizedBox(height: 6 * scale),
// PROGRESS LABELS
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text("Shipment Ready",
style: TextStyle(fontSize: 10, color: Colors.grey)),
Text("Import Custom",
style: TextStyle(fontSize: 10, color: Colors.grey)),
Text("Delivered",
style: TextStyle(fontSize: 10, color: Colors.grey)),
],
),
SizedBox(height: 12 * scale),
// ACTIONS
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_btn(Icons.visibility, "View", Colors.green.shade800,
Colors.green.shade50, () => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice", Colors.orange.shade800,
Colors.orange.shade50, () => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track", Colors.blue.shade800,
Colors.blue.shade50, () => _openTrack(o['order_id']), scale),
_btn(Icons.visibility, "View",
Colors.green.shade700, Colors.green.shade50,
() => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice",
Colors.orange.shade700, Colors.orange.shade50,
() => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track",
Colors.blue.shade700, Colors.blue.shade50,
() => _openTrack(o['order_id']), scale),
],
)
),
],
),
),
);
}
Widget _btn(IconData icon, String text, Color fg, Color bg,
VoidCallback onTap, double scale) {
Widget _btn(
IconData icon,
String text,
Color fg,
Color bg,
VoidCallback onTap,
double scale,
) {
return InkWell(
borderRadius: BorderRadius.circular(12 * scale),
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20 * scale, vertical: 12 * scale),
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12 * scale)),
constraints: BoxConstraints(
minWidth: 115 * scale, // 👈 makes button wider
minHeight: 36 * scale, // 👈 makes button taller
),
padding: EdgeInsets.symmetric(
horizontal: 16 * scale, // slightly more horizontal space
vertical: 8 * scale,
),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 👈 centers content
children: [
Icon(icon, size: 18 * scale, color: fg),
SizedBox(width: 8 * scale),
Icon(icon, size: 16 * scale, color: fg),
SizedBox(width: 6 * scale),
Text(
text,
style: TextStyle(
color: fg,
fontWeight: FontWeight.w600,
fontSize: 14 * scale,
fontSize: 12 * scale,
),
),
],
@@ -205,20 +234,44 @@ class _OrdersScreenState extends State<OrdersScreen> {
);
}
void _openOrderDetails(String id) {
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
// ================= STATUS BADGE =================
Widget getStatusBadge(String? status, double scale) {
final config = statusConfig(status);
final isDomestic = (status ?? '').toLowerCase().contains("domestic");
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: config.bg,
borderRadius: BorderRadius.circular(12),
),
child: Text(
isDomestic ? "Domestic\nDistribution" : (status ?? "Unknown"),
textAlign: TextAlign.center,
maxLines: isDomestic ? 2 : 1,
style: TextStyle(
color: config.fg,
fontWeight: FontWeight.w600,
fontSize: 11,
height: 1.2,
),
),
);
}
void _openInvoice(String id) {
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)));
}
void _openOrderDetails(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
void _openTrack(String id) {
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)));
}
void _openInvoice(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)));
void _openTrack(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)));
}
// ================= PROGRESS BAR =================
@@ -231,28 +284,24 @@ class _AnimatedProgressBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return LayoutBuilder(builder: (context, c) {
return Stack(
children: [
Container(
height: 10 * scale,
height: 8, // 👈 thinner bar
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale),
borderRadius: BorderRadius.circular(20),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 650),
curve: Curves.easeInOut,
height: 10 * scale,
width: constraints.maxWidth * progress,
duration: const Duration(milliseconds: 600),
height: 8,
width: c.maxWidth * progress,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * scale),
borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient(
colors: [
Color(0xFF4F8CFF),
Color(0xFF8A4DFF),
],
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
),
),
),
@@ -262,36 +311,58 @@ class _AnimatedProgressBar extends StatelessWidget {
}
}
// ================= FIXED STATUS LOGIC =================
// ================= STATUS LOGIC =================
double getProgress(String? status) {
final s = (status ?? '')
.toLowerCase()
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.trim();
if (s == "pending") return 0.25;
if (s == "loading") return 0.40;
if (s.contains("transit")) return 0.65;
if (s == "dispatched") return 0.85;
if (s == "delivered") return 1.0;
final s = (status ?? '').toLowerCase();
if (s.contains("shipment ready")) return 0.05;
if (s.contains("export")) return 0.10;
if (s.contains("international")) return 0.20;
if (s.contains("arrived")) return 0.30;
if (s.contains("import")) return 0.40;
if (s.contains("warehouse")) return 0.50;
if (s.contains("domestic")) return 0.60;
if (s.contains("out for")) return 0.90;
if (s.contains("delivered")) return 1.0;
return 0.05;
}
Color getStatusColor(String? status) {
final s = (status ?? '')
.toLowerCase()
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.trim();
_StatusConfig statusConfig(String? status) {
final s = (status ?? '').toLowerCase();
if (s == "pending") return Colors.orange;
if (s == "loading") return Colors.amber.shade800;
if (s.contains("transit")) return Colors.red;
if (s == "dispatched") return Colors.blue.shade700;
if (s == "delivered") return Colors.green.shade700;
if (s.contains("shipment ready")) {
return _StatusConfig(Colors.blue.shade800, Colors.blue.shade50);
}
if (s.contains("export")) {
return _StatusConfig(Colors.purple.shade800, Colors.purple.shade50);
}
if (s.contains("international")) {
return _StatusConfig(Colors.red.shade800, Colors.red.shade50);
}
if (s.contains("arrived")) {
return _StatusConfig(Colors.orange.shade800, Colors.orange.shade50);
}
if (s.contains("import")) {
return _StatusConfig(Colors.teal.shade800, Colors.teal.shade50);
}
if (s.contains("warehouse")) {
return _StatusConfig(Colors.brown.shade800, Colors.brown.shade50);
}
if (s.contains("domestic")) {
return _StatusConfig(Colors.indigo.shade800, Colors.indigo.shade50);
}
if (s.contains("out for")) {
return _StatusConfig(Colors.pink.shade800, Colors.pink.shade50);
}
if (s.contains("delivered")) {
return _StatusConfig(Colors.green.shade800, Colors.green.shade50);
}
return Colors.black54;
return _StatusConfig(Colors.grey.shade800, Colors.grey.shade300);
}
class _StatusConfig {
final Color fg;
final Color bg;
_StatusConfig(this.fg, this.bg);
}

View File

@@ -21,6 +21,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
late final AnimationController progressController;
late final AnimationController shipController;
late final AnimationController timelineController;
@override
void initState() {
@@ -36,6 +37,11 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
duration: const Duration(milliseconds: 1600),
)..repeat(reverse: true);
timelineController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
}
@@ -43,6 +49,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
void dispose() {
progressController.dispose();
shipController.dispose();
timelineController.dispose();
super.dispose();
}
@@ -56,19 +63,19 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
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}),
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 DATA -------------------
shipment = shipRes["success"] == true
? Map<String, dynamic>.from(shipRes["shipment"] ?? {})
: null;
/// ------------------- TRACKING DATA -------------------
trackData = trackRes["success"] == true
? Map<String, dynamic>.from(trackRes["track"] ?? {})
: {};
@@ -82,6 +89,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
target,
curve: Curves.easeInOutCubic,
);
// Animate timeline
timelineController.forward(from: 0);
} catch (_) {}
}
@@ -90,9 +99,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
// ---------------- PROGRESS LOGIC ----------------
double _computeProgress() {
final status = (trackData["shipment_status"] ?? "")
.toString()
.toLowerCase();
final status =
(trackData["shipment_status"] ?? "").toString().toLowerCase();
if (status.contains("delivered")) return 1.0;
if (status.contains("dispatched")) return 0.85;
@@ -100,22 +108,9 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
if (status.contains("loading")) return 0.40;
if (status.contains("pending")) return 0.25;
if (_hasTimestamp("delivered_at")) return 1.0;
if (_hasTimestamp("dispatched_at")) return 0.85;
if (_hasTimestamp("in_transit_at")) return 0.65;
if (_hasTimestamp("loading_at")) return 0.40;
if (_hasTimestamp("pending_at")) return 0.25;
return 0.05;
}
bool _hasTimestamp(String key) {
final v = trackData[key];
if (v == null) return false;
final s = v.toString().trim().toLowerCase();
return s.isNotEmpty && s != "null";
}
String _fmt(dynamic v) {
if (v == null) return "-";
try {
@@ -126,6 +121,92 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
}
}
// ---------------- SHIPMENT STEPS DATA ----------------
final List<Map<String, dynamic>> _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<String, int> _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) {
@@ -148,21 +229,13 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
SizedBox(height: 16 * scale),
_shipmentSummary(scale),
SizedBox(height: 16 * scale),
_shipmentTotals(scale),
SizedBox(height: 16 * scale),
_shipmentItems(scale), // USING SHIPMENT API ONLY
SizedBox(height: 20 * scale),
_trackingStatus(scale),
SizedBox(height: 16 * scale),
_progressBar(scale, width),
SizedBox(height: 16 * scale),
_detailsCard(scale),
// Replace horizontal progress bar with vertical timeline
_shipmentTimeline(scale),
],
),
),
@@ -223,7 +296,6 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale),
// Shipment ID
Container(
padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration(
@@ -282,142 +354,9 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
);
}
// ---------------- SHIPMENT TOTALS ----------------
Widget _shipmentTotals(double scale) {
if (shipment == null) return const SizedBox.shrink();
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Totals",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 10 * scale),
_twoCol("Total CTN", shipment!['total_ctn'], scale),
_twoCol("Total Qty", shipment!['total_qty'], scale),
_twoCol("Total Amount", shipment!['total_amount'], scale),
_twoCol("Total CBM", shipment!['total_cbm'], scale),
_twoCol("Total KG", shipment!['total_kg'], scale),
],
),
);
}
// ---------------- SHIPMENT ITEMS (FROM SHIPMENT API ONLY) ----------------
Widget _shipmentItems(double scale) {
if (shipment == null) return const SizedBox.shrink();
final items = (shipment!['items'] as List?) ?? [];
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Items",
style:
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 14 * scale),
...items.map((item) {
final orderId = item["order_id"]?.toString() ?? "-";
final qty = item["total_ttl_qty"]?.toString() ?? "-";
final cbm = item["total_ttl_cbm"]?.toString() ?? "-";
final kg = item["total_ttl_kg"]?.toString() ?? "-";
final amount = item["total_amount"]?.toString() ?? "-";
return Card(
elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14 * scale),
),
child: Padding(
padding: EdgeInsets.all(14 * scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: EdgeInsets.all(8 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12 * scale),
),
child: Icon(Icons.inventory_2,
color: Colors.blue, size: 20 * scale),
),
SizedBox(width: 12 * scale),
Text(
"Order ID: $orderId",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15 * scale),
),
],
),
SizedBox(height: 12 * scale),
if (item["mark_no"] != null)
Text("Mark No: ${item["mark_no"]}",
style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 6 * scale),
Text("Quantity: $qty",
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14 * scale)),
Text("CBM: $cbm", style: TextStyle(fontSize: 13 * scale)),
Text("KG: $kg", style: TextStyle(fontSize: 13 * scale)),
SizedBox(height: 10 * scale),
Container(
padding: EdgeInsets.symmetric(
vertical: 8 * scale, horizontal: 12 * scale),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10 * scale),
border: Border.all(color: Colors.blue, width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.currency_rupee,
color: Colors.blue),
Text(
"Amount: ₹$amount",
style: TextStyle(
fontSize: 15 * scale,
fontWeight: FontWeight.bold,
color: Colors.blue),
),
],
),
),
],
),
),
);
}).toList(),
],
),
);
}
// ---------------- TRACKING STATUS ----------------
Widget _trackingStatus(double scale) {
final p = _computeProgress();
final delivered = p >= 1.0;
final delivered = _computeProgress() >= 1.0;
return Container(
padding:
@@ -438,9 +377,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
),
SizedBox(width: 8 * scale),
Text(
(trackData["shipment_status"] ?? "-")
.toString()
.toUpperCase(),
(trackData["shipment_status"] ?? "-").toString().toUpperCase(),
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
@@ -451,129 +388,282 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
);
}
// ---------------- PROGRESS BAR ----------------
Widget _progressBar(double scale, double width) {
final usableWidth = width - (48 * scale);
// ---------------- VERTICAL SHIPMENT TIMELINE ----------------
Widget _shipmentTimeline(double scale) {
final currentStepIndex = _getCurrentStepIndex();
return Container(
padding: EdgeInsets.all(12 * scale),
padding: EdgeInsets.all(16 * scale),
decoration: _boxDecoration(scale),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Shipment Progress",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
SizedBox(height: 12 * scale),
Stack(
alignment: Alignment.centerLeft,
Row(
children: [
// Background
Container(
width: usableWidth,
height: 10 * scale,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale)),
),
// Progress Bar
AnimatedBuilder(
animation: progressController,
builder: (_, __) {
final w = usableWidth *
progressController.value.clamp(0.0, 1.0);
return Container(
width: w,
height: 10 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)]),
borderRadius: BorderRadius.circular(20 * scale),
),
);
},
),
// Moving Truck Icon
AnimatedBuilder(
animation: shipController,
builder: (_, __) {
final progress = progressController.value.clamp(0.0, 1.0);
final bob = sin(shipController.value * 2 * pi) * (4 * scale);
return Positioned(
left: (usableWidth - 26 * scale) * progress,
top: -6 * scale + bob,
child: Container(
width: 26 * scale,
height: 26 * scale,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
),
borderRadius: BorderRadius.circular(8 * scale),
),
child: Icon(
Icons.local_shipping,
size: 16 * scale,
color: Colors.white,
),
),
);
},
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: 10 * scale),
AnimatedBuilder(
animation: progressController,
builder: (_, __) => Text(
"${(progressController.value * 100).toInt()}% Completed",
style: TextStyle(color: Colors.black54, fontSize: 13 * 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),
),
],
),
);
}
// ---------------- DETAILS CARD ----------------
Widget _detailsCard(double scale) {
Widget _buildTimeline(double scale, int currentStepIndex) {
return Container(
padding: EdgeInsets.all(14 * scale),
decoration: _boxDecoration(scale),
child: Column(
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<String, dynamic> step,
required int index,
required bool isCompleted,
required bool isCurrent,
required bool isLast,
required int currentStepIndex,
}) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_detailsRow("Order ID", trackData["order_id"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Status", trackData["shipment_status"], scale),
SizedBox(height: 10 * scale),
_detailsRow("Date", _fmt(trackData["shipment_date"]), scale),
// 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 _detailsRow(String title, dynamic value, double scale) {
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<String, dynamic> 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: [
Expanded(
child: Text(title,
style: TextStyle(
fontWeight: FontWeight.w700, fontSize: 14 * scale))),
Expanded(
child: Text(value?.toString() ?? "-",
textAlign: TextAlign.right,
style: TextStyle(color: Colors.black87, fontSize: 14 * scale))),
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,
),
),
],
);
}
// ---------------- BOX DECORATION ----------------
BoxDecoration _boxDecoration(double scale) {
return BoxDecoration(
color: Colors.white,
@@ -587,4 +677,4 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
],
);
}
}
}