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

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,8 @@
// class ApiConfig {
// static const String baseUrl = "http://103.248.30.24:3030/api";
// static const String fileBaseUrl = "http://103.248.30.24:3030/";
// }
class ApiConfig { class ApiConfig {
static const String baseUrl = "http://192.168.0.105:8000/api"; static const String baseUrl = "http://10.202.215.164:8000/api";
static const String fileBaseUrl = "http://192.168.0.105:8000/"; static const String fileBaseUrl = "http://10.202.215.164:8000/";
} }

View File

@@ -6,7 +6,7 @@ class AppConfig {
static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png"; static const String logoUrlEmulator = "http://10.0.2.2:8000/images/kent_logo2.png";
// For Physical Device (Replace with your actual PC local IP) // For Physical Device (Replace with your actual PC local IP)
static const String logoUrlDevice = "http://192.168.0.105:8000/images/kent_logo2.png"; static const String logoUrlDevice = "http://10.202.215.164:8000/images/kent_logo2.png";
// Which one to use? // Which one to use?
static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE static const String logoUrl = logoUrlDevice; // CHANGE THIS WHEN TESTING ON REAL DEVICE

View File

@@ -16,6 +16,12 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
Map order = {}; Map order = {};
final Map<String, bool> _expanded = {}; final Map<String, bool> _expanded = {};
bool confirming = false;
bool get isOrderPlaced => order['status'] == 'order_placed';
bool get isConfirmed => order['status'] != 'order_placed';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -65,6 +71,10 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
_itemsSection(items, scale), _itemsSection(items, scale),
SizedBox(height: 18 * scale), SizedBox(height: 18 * scale),
_totalsSection(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 // SUMMARY CARD
// ----------------------------- // -----------------------------
@@ -267,7 +340,7 @@ class _OrderDetailScreenState extends State<OrderDetailScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Totals", Text("Totalss",
style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)), style: TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale), SizedBox(height: 12 * scale),
_totalRow("Total Qty", order['ttl_qty'], 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 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 filteredOrders = provider.orders.where((o) {
final q = searchQuery.toLowerCase(); final q = searchQuery.toLowerCase();
return o["order_id"].toString().toLowerCase().contains(q) || return o["order_id"].toString().toLowerCase().contains(q) ||
o["status"].toString().toLowerCase().contains(q) || o["status"].toString().toLowerCase().contains(q) ||
o["description"].toString().toLowerCase().contains(q) || o["description"].toString().toLowerCase().contains(q);
o["amount"].toString().toLowerCase().contains(q);
}).toList(); }).toList();
return Column( return Column(
children: [ children: [
// SEARCH BAR _searchBar(scale),
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
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.all(16 * scale), padding: EdgeInsets.all(14 * scale),
itemCount: filteredOrders.length, itemCount: filteredOrders.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final order = filteredOrders[i]; return _orderCard(filteredOrders[i], scale);
return _orderCard(order, 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) { Widget _orderCard(Map<String, dynamic> o, double scale) {
final progress = getProgress(o['status']); final progress = getProgress(o['status']);
final badgeColor = getStatusColor(o['status']); final percent = (progress * 100).toInt();
return Card( return Card(
elevation: 3 * scale, elevation: 2,
margin: EdgeInsets.only(bottom: 12 * scale),
color: Colors.white, color: Colors.white,
margin: EdgeInsets.only(bottom: 16 * scale),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16 * scale), borderRadius: BorderRadius.circular(14),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(16 * scale), padding: EdgeInsets.all(12 * scale), // 👈 tighter padding
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// HEADER
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
"Order #${o['order_id']}", "Order #${o['order_id']}",
style: TextStyle( style: TextStyle(
fontSize: 18 * scale, fontSize: 15 * scale, // 👈 slightly smaller
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
),
),
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,
),
), ),
), ),
getStatusBadge(o['status'], scale),
], ],
), ),
SizedBox(height: 10 * scale), SizedBox(height: 6 * scale),
Text(o['description'], style: TextStyle(fontSize: 14 * scale)), Text(
SizedBox(height: 5 * scale), o['description'],
style: TextStyle(fontSize: 13 * scale),
),
SizedBox(height: 4 * scale),
Text( Text(
"${o['amount']}", "${o['amount']}",
style: TextStyle( style: TextStyle(
fontSize: 16 * scale, fontSize: 15 * scale,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox(height: 18 * scale), SizedBox(height: 12 * scale),
_AnimatedProgressBar(progress: progress, scale: scale),
SizedBox(height: 18 * 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_btn(Icons.visibility, "View", Colors.green.shade800, _btn(Icons.visibility, "View",
Colors.green.shade50, () => _openOrderDetails(o['order_id']), scale), Colors.green.shade700, Colors.green.shade50,
() => _openOrderDetails(o['order_id']), scale),
_btn(Icons.receipt_long, "Invoice", Colors.orange.shade800, _btn(Icons.receipt_long, "Invoice",
Colors.orange.shade50, () => _openInvoice(o['order_id']), scale), Colors.orange.shade700, Colors.orange.shade50,
() => _openInvoice(o['order_id']), scale),
_btn(Icons.local_shipping, "Track", Colors.blue.shade800, _btn(Icons.local_shipping, "Track",
Colors.blue.shade50, () => _openTrack(o['order_id']), scale), Colors.blue.shade700, Colors.blue.shade50,
() => _openTrack(o['order_id']), scale),
], ],
) ),
], ],
), ),
), ),
); );
} }
Widget _btn(IconData icon, String text, Color fg, Color bg, Widget _btn(
VoidCallback onTap, double scale) { IconData icon,
String text,
Color fg,
Color bg,
VoidCallback onTap,
double scale,
) {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(12 * scale),
onTap: onTap, onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 20 * scale, vertical: 12 * scale), constraints: BoxConstraints(
decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12 * scale)), 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( child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 👈 centers content
children: [ children: [
Icon(icon, size: 18 * scale, color: fg), Icon(icon, size: 16 * scale, color: fg),
SizedBox(width: 8 * scale), SizedBox(width: 6 * scale),
Text( Text(
text, text,
style: TextStyle( style: TextStyle(
color: fg, color: fg,
fontWeight: FontWeight.w600, 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, // ================= STATUS BADGE =================
MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
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) { void _openOrderDetails(String id) =>
Navigator.push(context, Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id))); MaterialPageRoute(builder: (_) => OrderDetailScreen(orderId: id)));
}
void _openTrack(String id) { void _openInvoice(String id) =>
Navigator.push(context, Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id))); MaterialPageRoute(builder: (_) => OrderInvoiceScreen(orderId: id)));
}
void _openTrack(String id) =>
Navigator.push(context,
MaterialPageRoute(builder: (_) => OrderTrackScreen(orderId: id)));
} }
// ================= PROGRESS BAR ================= // ================= PROGRESS BAR =================
@@ -231,28 +284,24 @@ class _AnimatedProgressBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, c) {
return Stack( return Stack(
children: [ children: [
Container( Container(
height: 10 * scale, height: 8, // 👈 thinner bar
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade300, color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20 * scale), borderRadius: BorderRadius.circular(20),
), ),
), ),
AnimatedContainer( AnimatedContainer(
duration: const Duration(milliseconds: 650), duration: const Duration(milliseconds: 600),
curve: Curves.easeInOut, height: 8,
height: 10 * scale, width: c.maxWidth * progress,
width: constraints.maxWidth * progress,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20 * scale), borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient( gradient: const LinearGradient(
colors: [ colors: [Color(0xFF4F8CFF), Color(0xFF8A4DFF)],
Color(0xFF4F8CFF),
Color(0xFF8A4DFF),
],
), ),
), ),
), ),
@@ -262,36 +311,58 @@ class _AnimatedProgressBar extends StatelessWidget {
} }
} }
// ================= FIXED STATUS LOGIC ================= // ================= STATUS LOGIC =================
double getProgress(String? status) { double getProgress(String? status) {
final s = (status ?? '') final s = (status ?? '').toLowerCase();
.toLowerCase() if (s.contains("shipment ready")) return 0.05;
.replaceAll('_', ' ') if (s.contains("export")) return 0.10;
.replaceAll('-', ' ') if (s.contains("international")) return 0.20;
.trim(); if (s.contains("arrived")) return 0.30;
if (s.contains("import")) return 0.40;
if (s == "pending") return 0.25; if (s.contains("warehouse")) return 0.50;
if (s == "loading") return 0.40; if (s.contains("domestic")) return 0.60;
if (s.contains("transit")) return 0.65; if (s.contains("out for")) return 0.90;
if (s == "dispatched") return 0.85; if (s.contains("delivered")) return 1.0;
if (s == "delivered") return 1.0;
return 0.05; return 0.05;
} }
Color getStatusColor(String? status) { _StatusConfig statusConfig(String? status) {
final s = (status ?? '') final s = (status ?? '').toLowerCase();
.toLowerCase()
.replaceAll('_', ' ')
.replaceAll('-', ' ')
.trim();
if (s == "pending") return Colors.orange; if (s.contains("shipment ready")) {
if (s == "loading") return Colors.amber.shade800; return _StatusConfig(Colors.blue.shade800, Colors.blue.shade50);
if (s.contains("transit")) return Colors.red; }
if (s == "dispatched") return Colors.blue.shade700; if (s.contains("export")) {
if (s == "delivered") return Colors.green.shade700; 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 progressController;
late final AnimationController shipController; late final AnimationController shipController;
late final AnimationController timelineController;
@override @override
void initState() { void initState() {
@@ -36,6 +37,11 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
duration: const Duration(milliseconds: 1600), duration: const Duration(milliseconds: 1600),
)..repeat(reverse: true); )..repeat(reverse: true);
timelineController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData()); WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
} }
@@ -43,6 +49,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
void dispose() { void dispose() {
progressController.dispose(); progressController.dispose();
shipController.dispose(); shipController.dispose();
timelineController.dispose();
super.dispose(); super.dispose();
} }
@@ -56,19 +63,19 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
final service = OrderService(DioClient.getInstance(context)); final service = OrderService(DioClient.getInstance(context));
final results = await Future.wait([ final results = await Future.wait([
service.getShipment(widget.orderId).catchError((_) => {"success": false}), service.getShipment(widget.orderId)
service.trackOrder(widget.orderId).catchError((_) => {"success": false}), .catchError((_) => {"success": false}),
service.trackOrder(widget.orderId)
.catchError((_) => {"success": false}),
]); ]);
final shipRes = results[0] as Map; final shipRes = results[0] as Map;
final trackRes = results[1] as Map; final trackRes = results[1] as Map;
/// ------------------- SHIPMENT DATA -------------------
shipment = shipRes["success"] == true shipment = shipRes["success"] == true
? Map<String, dynamic>.from(shipRes["shipment"] ?? {}) ? Map<String, dynamic>.from(shipRes["shipment"] ?? {})
: null; : null;
/// ------------------- TRACKING DATA -------------------
trackData = trackRes["success"] == true trackData = trackRes["success"] == true
? Map<String, dynamic>.from(trackRes["track"] ?? {}) ? Map<String, dynamic>.from(trackRes["track"] ?? {})
: {}; : {};
@@ -82,6 +89,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
target, target,
curve: Curves.easeInOutCubic, curve: Curves.easeInOutCubic,
); );
// Animate timeline
timelineController.forward(from: 0);
} catch (_) {} } catch (_) {}
} }
@@ -90,9 +99,8 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
// ---------------- PROGRESS LOGIC ---------------- // ---------------- PROGRESS LOGIC ----------------
double _computeProgress() { double _computeProgress() {
final status = (trackData["shipment_status"] ?? "") final status =
.toString() (trackData["shipment_status"] ?? "").toString().toLowerCase();
.toLowerCase();
if (status.contains("delivered")) return 1.0; if (status.contains("delivered")) return 1.0;
if (status.contains("dispatched")) return 0.85; 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("loading")) return 0.40;
if (status.contains("pending")) return 0.25; 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; 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) { String _fmt(dynamic v) {
if (v == null) return "-"; if (v == null) return "-";
try { 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 ---------------- // ---------------- UI BUILD ----------------
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -148,21 +229,13 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
SizedBox(height: 16 * scale), SizedBox(height: 16 * scale),
_shipmentSummary(scale), _shipmentSummary(scale),
SizedBox(height: 16 * scale),
_shipmentTotals(scale),
SizedBox(height: 16 * scale),
_shipmentItems(scale), // USING SHIPMENT API ONLY
SizedBox(height: 20 * scale), SizedBox(height: 20 * scale),
_trackingStatus(scale), _trackingStatus(scale),
SizedBox(height: 16 * scale), SizedBox(height: 16 * scale),
_progressBar(scale, width), // Replace horizontal progress bar with vertical timeline
SizedBox(height: 16 * scale), _shipmentTimeline(scale),
_detailsCard(scale),
], ],
), ),
), ),
@@ -223,7 +296,6 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)), TextStyle(fontSize: 18 * scale, fontWeight: FontWeight.bold)),
SizedBox(height: 12 * scale), SizedBox(height: 12 * scale),
// Shipment ID
Container( Container(
padding: EdgeInsets.all(12 * scale), padding: EdgeInsets.all(12 * scale),
decoration: BoxDecoration( 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 ---------------- // ---------------- TRACKING STATUS ----------------
Widget _trackingStatus(double scale) { Widget _trackingStatus(double scale) {
final p = _computeProgress(); final delivered = _computeProgress() >= 1.0;
final delivered = p >= 1.0;
return Container( return Container(
padding: padding:
@@ -438,9 +377,7 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
), ),
SizedBox(width: 8 * scale), SizedBox(width: 8 * scale),
Text( Text(
(trackData["shipment_status"] ?? "-") (trackData["shipment_status"] ?? "-").toString().toUpperCase(),
.toString()
.toUpperCase(),
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -451,129 +388,282 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
); );
} }
// ---------------- PROGRESS BAR ---------------- // ---------------- VERTICAL SHIPMENT TIMELINE ----------------
Widget _progressBar(double scale, double width) { Widget _shipmentTimeline(double scale) {
final usableWidth = width - (48 * scale); final currentStepIndex = _getCurrentStepIndex();
return Container( return Container(
padding: EdgeInsets.all(12 * scale), padding: EdgeInsets.all(16 * scale),
decoration: _boxDecoration(scale), decoration: _boxDecoration(scale),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Shipment Progress", Row(
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 15 * scale)),
SizedBox(height: 12 * scale),
Stack(
alignment: Alignment.centerLeft,
children: [ children: [
// Background Icon(Icons.timeline, color: Colors.blue, size: 20 * scale),
Container( SizedBox(width: 8 * scale),
width: usableWidth, Text(
height: 10 * scale, "Shipment Progress",
decoration: BoxDecoration( style: TextStyle(
color: Colors.grey.shade300, fontWeight: FontWeight.bold,
borderRadius: BorderRadius.circular(20 * scale)), fontSize: 17 * 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,
),
),
);
},
), ),
], ],
), ),
SizedBox(height: 8 * scale),
SizedBox(height: 10 * scale), Text(
"Tracking your package in real-time",
AnimatedBuilder( style: TextStyle(
animation: progressController, color: Colors.grey.shade600,
builder: (_, __) => Text( fontSize: 13 * scale,
"${(progressController.value * 100).toInt()}% Completed",
style: TextStyle(color: Colors.black54, 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 _buildTimeline(double scale, int currentStepIndex) {
Widget _detailsCard(double scale) {
return Container( return Container(
padding: EdgeInsets.all(14 * scale), padding: EdgeInsets.only(left: 8 * scale),
decoration: _boxDecoration(scale), child: ListView.builder(
child: Column( 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: [ children: [
_detailsRow("Order ID", trackData["order_id"], scale), // Timeline Column (Line + Icon)
SizedBox(height: 10 * scale), Column(
_detailsRow("Status", trackData["shipment_status"], scale), children: [
SizedBox(height: 10 * scale), // Top connector line (except for first item)
_detailsRow("Date", _fmt(trackData["shipment_date"]), scale), 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( return Row(
children: [ children: [
Expanded( Icon(
child: Text(title, isCurrent ? Icons.access_time : Icons.calendar_today,
style: TextStyle( color: statusColor,
fontWeight: FontWeight.w700, fontSize: 14 * scale))), size: 14 * scale,
Expanded( ),
child: Text(value?.toString() ?? "-", SizedBox(width: 4 * scale),
textAlign: TextAlign.right, Text(
style: TextStyle(color: Colors.black87, fontSize: 14 * scale))), statusText,
style: TextStyle(
fontSize: 12 * scale,
color: statusColor,
fontWeight: FontWeight.w500,
),
),
], ],
); );
} }
// ---------------- BOX DECORATION ----------------
BoxDecoration _boxDecoration(double scale) { BoxDecoration _boxDecoration(double scale) {
return BoxDecoration( return BoxDecoration(
color: Colors.white, color: Colors.white,
@@ -587,4 +677,4 @@ class _OrderTrackScreenState extends State<OrderTrackScreen>
], ],
); );
} }
} }

View File

@@ -8,6 +8,7 @@ import 'token_interceptor.dart';
class DioClient { class DioClient {
static Dio? _dio; static Dio? _dio;
// static const String baseUrl = "http://103.248.30.24:3030";
static const String baseUrl = "http://10.119.0.74:8000"; static const String baseUrl = "http://10.119.0.74:8000";
static Dio getInstance(BuildContext context) { static Dio getInstance(BuildContext context) {

View File

@@ -29,4 +29,10 @@ class OrderService {
final res = await _dio.get('/user/order/$id/track'); final res = await _dio.get('/user/order/$id/track');
return res.data; return res.data;
} }
Future<Map<String, dynamic>> confirmOrder(String orderId) async {
final res = await _dio.post('/user/orders/$orderId/confirm');
return res.data;
}
} }

View File

@@ -39,6 +39,10 @@ class ReverbSocketService {
'ws://10.119.0.74:8080/app/q5fkk5rvcnatvbgadwvl' 'ws://10.119.0.74:8080/app/q5fkk5rvcnatvbgadwvl'
'?protocol=7&client=flutter&version=1.0', '?protocol=7&client=flutter&version=1.0',
); );
// final uri = Uri.parse(
// 'ws://103.248.30.24:8080/app/q5fkk5rvcnatvbgadwvl'
// '?protocol=7&client=flutter&version=1.0',
// );
debugPrint("🔌 CONNECTING SOCKET → $uri"); debugPrint("🔌 CONNECTING SOCKET → $uri");