From ce55bf125b3fee75cef987507a5bf18f0f964116 Mon Sep 17 00:00:00 2001 From: Sneha Date: Thu, 9 Apr 2026 12:20:49 +0530 Subject: [PATCH] changes --- lib/common/settings.dart | 103 ++ lib/orders/all_orders.dart | 1 + lib/orders/assign_driver.dart | 544 +++++++++- lib/orders/assign_selection_screen.dart | 521 +++++++++ lib/orders/collect_money.dart | 224 ++++ lib/orders/delivery_updates.dart | 1305 ++++++++++------------- lib/orders/orders_model.dart | 104 ++ lib/orders/otp_start_unloading.dart | 381 +++++++ lib/orders/unloading_completed_otp.dart | 328 ++++++ lib/orders/unloading_inprogress.dart | 120 +++ pubspec.lock | 16 + pubspec.yaml | 1 + 12 files changed, 2927 insertions(+), 721 deletions(-) create mode 100644 lib/orders/assign_selection_screen.dart create mode 100644 lib/orders/collect_money.dart create mode 100644 lib/orders/otp_start_unloading.dart create mode 100644 lib/orders/unloading_completed_otp.dart create mode 100644 lib/orders/unloading_inprogress.dart diff --git a/lib/common/settings.dart b/lib/common/settings.dart index dafe760..2af799f 100644 --- a/lib/common/settings.dart +++ b/lib/common/settings.dart @@ -197,6 +197,8 @@ class AppSettings{ static String acceptRequestUrl = host +"friend-request/accept"; static String rejectRequestUrl = host +"friend-request/reject"; static String getDriverTripsUrl = host +"getdeliveryboybookings"; + static String verifyUnloadStartOTPUrl = host + 'deliveryboystartandstop'; + static String amountpaidByCustomerUrl = host + 'amountpaidByCustomer'; static int driverAvailableCount = 0; @@ -1820,6 +1822,50 @@ class AppSettings{ } + static Future verifyUnloadStartOTP(String bookingId, payload) async { + final uri = Uri.parse(verifyUnloadStartOTPUrl+'/'+bookingId); + var response = await http.put(uri, body: jsonEncode(payload), headers: await buildRequestHeaders()); + if (response.statusCode == 200) { + return response.body; + } else if (response.statusCode == 401) { + bool status = await AppSettings.resetToken(); + if (status) { + response = await await http.put(uri, body: jsonEncode(payload), headers: await buildRequestHeaders()); + if (response.statusCode == 200) { + return response.body; + } else { + return ''; + } + } else { + return ''; + } + } else { + return ''; + } + } + + static Future amountpaidByCustomer(String bookingId, payload) async { + final uri = Uri.parse(amountpaidByCustomerUrl+'/'+bookingId); + var response = await http.put(uri, body: jsonEncode(payload), headers: await buildRequestHeaders()); + if (response.statusCode == 200) { + return response.body; + } else if (response.statusCode == 401) { + bool status = await AppSettings.resetToken(); + if (status) { + response = await await http.put(uri, body: jsonEncode(payload), headers: await buildRequestHeaders()); + if (response.statusCode == 200) { + return response.body; + } else { + return ''; + } + } else { + return ''; + } + } else { + return ''; + } + } + /*Apis ends here*/ //save data local @@ -2170,4 +2216,61 @@ class AppSettings{ ); } + static appBarWithNotificationIcon(String buildingName,String typeOfWater,String capacity,context){ + buildingName = buildingName ?? ''; + typeOfWater = typeOfWater ?? ''; + capacity = capacity ?? ''; + + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + scrolledUnderElevation: 0, + titleSpacing: 0, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(buildingName, + style: fontTextStyle(16, const Color(0XFF2A2A2A), FontWeight.w600)), + const SizedBox(height: 2), + Text('$typeOfWater | $capacity', + style: fontTextStyle(12, const Color(0XFF2A2A2A), FontWeight.w400)), + ], + ), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + leading: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Padding( + padding: + const EdgeInsets.fromLTRB(8, 8, 8, 8), // Add padding if needed + child: Image.asset( + 'images/backbutton_appbar.png', // Replace with your image path + fit: BoxFit.contain, + color: Color(0XFF2A2A2A), + height: 24, + width: 24, + ), + ), + ), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', + height: 20, + width: 20,// Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + ); + } + } \ No newline at end of file diff --git a/lib/orders/all_orders.dart b/lib/orders/all_orders.dart index d8302d3..7b57e96 100644 --- a/lib/orders/all_orders.dart +++ b/lib/orders/all_orders.dart @@ -1265,6 +1265,7 @@ class _OrderCardState extends State{ MaterialPageRoute( builder:(context)=> DeliveryUpdatesPage( + details:widget.order, orderId:widget.order.dbId, initialStatus:widget.order.status ) diff --git a/lib/orders/assign_driver.dart b/lib/orders/assign_driver.dart index 7af2fd8..1c0d694 100644 --- a/lib/orders/assign_driver.dart +++ b/lib/orders/assign_driver.dart @@ -8,6 +8,8 @@ import '../resources/tankers_model.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'assign_selection_screen.dart'; + class AssignDriverScreen extends StatefulWidget { var order; var status; @@ -1425,6 +1427,26 @@ class _AssignDriverScreenState extends State { onPressed: () async { _showAssignTankerBottomSheet(); }, + /* onPressed: () async { + + bool? result = await Navigator.push( + cont + ext, + MaterialPageRoute( + builder: (_) => AssignTankerScreen( + order: widget.order, + driversList: driversList, + tankersList: tankersList, + sourceLocationsList: sourceLocationsList, + ), + ), + ); + + if(result == true){ + Navigator.pop(context,true); + } + + },*/ child: Text( "Assign Tanker", style: fontTextStyle( @@ -1437,7 +1459,527 @@ class _AssignDriverScreenState extends State { ); } - /// 🔹 Helper widget for rows + /// 🔹 Helper widget for rowsimport 'dart:convert'; + // import 'package:flutter/material.dart'; + // import 'package:intl/intl.dart'; + // import 'package:supplier_new/common/settings.dart'; + // import '../resources/drivers_model.dart'; + // import '../resources/tankers_model.dart'; + // import '../resources/source_loctaions_model.dart'; + // + // class AssignTankerScreen extends StatefulWidget { + // + // final order; + // final List driversList; + // final List tankersList; + // final List sourceLocationsList; + // + // AssignTankerScreen({ + // this.order, + // required this.driversList, + // required this.tankersList, + // required this.sourceLocationsList, + // }); + // + // @override + // State createState() => + // _AssignTankerScreenState(); + // } + // + // class _AssignTankerScreenState + // extends State { + // + // int? selectedTankerIndex; + // int? selectedDriverIndex; + // int? selectedSourceIndex; + // + // int _capToLiters(dynamic cap){ + // + // if(cap==null)return -1; + // + // if(cap is num){ + // return cap.round(); + // } + // + // final s=cap + // .toString() + // .toLowerCase() + // .replaceAll(',','') + // .trim(); + // + // final match= + // RegExp(r'(\d+(\.\d+)?)') + // .firstMatch(s); + // + // if(match==null)return -1; + // + // final n= + // double.tryParse(match.group(1)!)??-1; + // + // if(n<0)return -1; + // + // if(s.contains('kl')){ + // return (n*1000).round(); + // } + // + // return n.round(); + // + // } + // + // DateTime parseOrderDateTime(){ + // + // DateTime d= + // DateFormat("dd-MMM-yyyy") + // .parse(widget.order.date); + // + // DateTime t= + // DateFormat("hh:mm a") + // .parse(widget.order.time); + // + // return DateTime( + // d.year, + // d.month, + // d.day, + // t.hour, + // t.minute + // ); + // + // } + // + // bool isTankerBlocked( + // TankersModel tanker){ + // + // if(tanker.blocked_dates==null || + // tanker.blocked_dates.isEmpty){ + // return false; + // } + // + // DateTime orderDT= + // parseOrderDateTime(); + // + // for(var slot + // in tanker.blocked_dates){ + // + // if(slot['date']!= + // widget.order.date){ + // continue; + // } + // + // String timeRange= + // slot['time']; + // + // List parts= + // timeRange.split("to"); + // + // if(parts.length!=2){ + // continue; + // } + // + // DateTime start= + // DateFormat("hh:mm a") + // .parse(parts[0].trim()); + // + // DateTime end= + // DateFormat("hh:mm a") + // .parse(parts[1].trim()); + // + // DateTime startDT= + // DateTime( + // orderDT.year, + // orderDT.month, + // orderDT.day, + // start.hour, + // start.minute + // ); + // + // DateTime endDT= + // DateTime( + // orderDT.year, + // orderDT.month, + // orderDT.day, + // end.hour, + // end.minute + // ); + // + // if(orderDT.isAfter(startDT) + // && + // orderDT.isBefore(endDT)){ + // return true; + // } + // + // if(orderDT==startDT || + // orderDT==endDT){ + // return true; + // } + // + // } + // + // return false; + // + // } + // + // String? getDriverBlockedTime( + // DriversModel driver){ + // + // if(driver.blocked_dates==null || + // driver.blocked_dates.isEmpty){ + // return null; + // } + // + // DateTime orderDT= + // parseOrderDateTime(); + // + // for(var slot + // in driver.blocked_dates){ + // + // if(slot['date']!= + // widget.order.date){ + // continue; + // } + // + // String timeRange= + // slot['time']; + // + // List parts= + // timeRange.split("to"); + // + // if(parts.length!=2){ + // continue; + // } + // + // DateTime start= + // DateFormat("hh:mm a") + // .parse(parts[0].trim()); + // + // DateTime end= + // DateFormat("hh:mm a") + // .parse(parts[1].trim()); + // + // DateTime startDT= + // DateTime( + // orderDT.year, + // orderDT.month, + // orderDT.day, + // start.hour, + // start.minute + // ); + // + // DateTime endDT= + // DateTime( + // orderDT.year, + // orderDT.month, + // orderDT.day, + // end.hour, + // end.minute + // ); + // + // if(orderDT.isAfter(startDT) + // && + // orderDT.isBefore(endDT)){ + // return timeRange; + // } + // + // if(orderDT==startDT || + // orderDT==endDT){ + // return timeRange; + // } + // + // } + // + // return null; + // + // } + // + // @override + // Widget build(BuildContext context){ + // + // return Scaffold( + // + // backgroundColor: Colors.white, + // + // appBar: AppBar( + // + // title: + // Text("Assign Tanker"), + // + // ), + // + // body: + // + // Column( + // + // children:[ + // + // Expanded( + // + // child: + // + // SingleChildScrollView( + // + // padding: + // EdgeInsets.all(16), + // + // child: + // + // Column( + // + // crossAxisAlignment: + // CrossAxisAlignment.start, + // + // children:[ + // + // /// ORDER CARD + // + // Container( + // + // padding: + // EdgeInsets.all(12), + // + // decoration: + // BoxDecoration( + // + // borderRadius: + // BorderRadius.circular(12), + // + // border: + // Border.all( + // color: + // Color(0XFFC9C2F0) + // ), + // + // ), + // + // child: + // + // Row( + // + // children:[ + // + // Column( + // + // crossAxisAlignment: + // CrossAxisAlignment.start, + // + // children:[ + // + // Text( + // widget.order.building_name + // ), + // + // Text( + // widget.order.displayAddress + // ), + // + // ], + // + // ), + // + // Spacer(), + // + // Text( + // "${widget.order.distanceInKm} Km" + // ), + // + // ], + // + // ), + // + // ), + // + // SizedBox(height:20), + // + // /// TANKERS + // + // Text("SELECT TANKER"), + // + // SizedBox(height:10), + // + // ...widget.tankersList + // .where((t)=> + // + // _capToLiters(t.capacity)== + // _capToLiters(widget.order.capacity) + // + // ).toList() + // .asMap() + // .entries + // .map((entry){ + // + // int idx= + // entry.key; + // + // var d= + // entry.value; + // + // bool blocked= + // isTankerBlocked(d); + // + // return GestureDetector( + // + // onTap:blocked?null:(){ + // + // setState(() { + // + // selectedTankerIndex= + // idx; + // + // selectedDriverIndex= + // null; + // + // }); + // + // }, + // + // child: + // + // Card( + // + // child: + // + // Padding( + // + // padding: + // EdgeInsets.all(12), + // + // child: + // + // Text( + // d.tanker_name + // ), + // + // ), + // + // ), + // + // ); + // + // }).toList(), + // + // ], + // + // ), + // + // ), + // + // ), + // + // /// ASSIGN BUTTON + // + // Padding( + // + // padding: + // EdgeInsets.all(16), + // + // child: + // + // SizedBox( + // + // width:double.infinity, + // + // child: + // + // ElevatedButton( + // + // style: + // ElevatedButton.styleFrom( + // + // backgroundColor: + // Color(0XFF8270DB) + // + // ), + // + // onPressed:()async{ + // + // if(selectedTankerIndex==null){ + // + // AppSettings + // .longFailedToast( + // "Select tanker" + // ); + // + // return; + // + // } + // + // final selectedTanker= + // widget.tankersList[ + // selectedTankerIndex! + // ]; + // + // final selectedDriver= + // selectedDriverIndex!=null + // ? + // widget.driversList[ + // selectedDriverIndex! + // ] + // : + // null; + // + // final selectedSource= + // selectedSourceIndex!=null + // ? + // widget.sourceLocationsList[ + // selectedSourceIndex! + // ] + // : + // null; + // + // var payload={}; + // + // payload["tankerName"]= + // selectedTanker.tanker_name; + // + // payload["delivery_agent"]= + // selectedDriver?.driver_name; + // + // payload["delivery_agent_mobile"]= + // selectedDriver?.phone_number; + // + // payload["water_source_location"]= + // selectedSource?.source_name; + // + // bool status= + // await AppSettings + // .assignTanker( + // payload, + // widget.order.dbId + // ); + // + // if(status){ + // + // AppSettings + // .longSuccessToast( + // "Assigned" + // ); + // + // Navigator.pop( + // context, + // true + // ); + // + // } + // + // }, + // + // child: + // + // Text("Assign"), + // + // ), + // + // ), + // + // ) + // + // ], + // + // ), + // + // ); + // + // } + // + // } Widget _detailRow(String title, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 0), diff --git a/lib/orders/assign_selection_screen.dart b/lib/orders/assign_selection_screen.dart new file mode 100644 index 0000000..5f0f633 --- /dev/null +++ b/lib/orders/assign_selection_screen.dart @@ -0,0 +1,521 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:supplier_new/common/settings.dart'; +import '../resources/drivers_model.dart'; +import '../resources/tankers_model.dart'; +import '../resources/source_loctaions_model.dart'; + +class AssignTankerScreen extends StatefulWidget { + + final order; + final List driversList; + final List tankersList; + final List sourceLocationsList; + + AssignTankerScreen({ + this.order, + required this.driversList, + required this.tankersList, + required this.sourceLocationsList, + }); + + @override + State createState() => + _AssignTankerScreenState(); +} + +class _AssignTankerScreenState + extends State { + + int? selectedTankerIndex; + int? selectedDriverIndex; + int? selectedSourceIndex; + + int _capToLiters(dynamic cap){ + + if(cap==null)return -1; + + if(cap is num){ + return cap.round(); + } + + final s=cap + .toString() + .toLowerCase() + .replaceAll(',','') + .trim(); + + final match= + RegExp(r'(\d+(\.\d+)?)') + .firstMatch(s); + + if(match==null)return -1; + + final n= + double.tryParse(match.group(1)!)??-1; + + if(n<0)return -1; + + if(s.contains('kl')){ + return (n*1000).round(); + } + + return n.round(); + + } + + DateTime parseOrderDateTime(){ + + DateTime d= + DateFormat("dd-MMM-yyyy") + .parse(widget.order.date); + + DateTime t= + DateFormat("hh:mm a") + .parse(widget.order.time); + + return DateTime( + d.year, + d.month, + d.day, + t.hour, + t.minute + ); + + } + + bool isTankerBlocked( + TankersModel tanker){ + + if(tanker.blocked_dates==null || + tanker.blocked_dates.isEmpty){ + return false; + } + + DateTime orderDT= + parseOrderDateTime(); + + for(var slot + in tanker.blocked_dates){ + + if(slot['date']!= + widget.order.date){ + continue; + } + + String timeRange= + slot['time']; + + List parts= + timeRange.split("to"); + + if(parts.length!=2){ + continue; + } + + DateTime start= + DateFormat("hh:mm a") + .parse(parts[0].trim()); + + DateTime end= + DateFormat("hh:mm a") + .parse(parts[1].trim()); + + DateTime startDT= + DateTime( + orderDT.year, + orderDT.month, + orderDT.day, + start.hour, + start.minute + ); + + DateTime endDT= + DateTime( + orderDT.year, + orderDT.month, + orderDT.day, + end.hour, + end.minute + ); + + if(orderDT.isAfter(startDT) + && + orderDT.isBefore(endDT)){ + return true; + } + + if(orderDT==startDT || + orderDT==endDT){ + return true; + } + + } + + return false; + + } + + String? getDriverBlockedTime( + DriversModel driver){ + + if(driver.blocked_dates==null || + driver.blocked_dates.isEmpty){ + return null; + } + + DateTime orderDT= + parseOrderDateTime(); + + for(var slot + in driver.blocked_dates){ + + if(slot['date']!= + widget.order.date){ + continue; + } + + String timeRange= + slot['time']; + + List parts= + timeRange.split("to"); + + if(parts.length!=2){ + continue; + } + + DateTime start= + DateFormat("hh:mm a") + .parse(parts[0].trim()); + + DateTime end= + DateFormat("hh:mm a") + .parse(parts[1].trim()); + + DateTime startDT= + DateTime( + orderDT.year, + orderDT.month, + orderDT.day, + start.hour, + start.minute + ); + + DateTime endDT= + DateTime( + orderDT.year, + orderDT.month, + orderDT.day, + end.hour, + end.minute + ); + + if(orderDT.isAfter(startDT) + && + orderDT.isBefore(endDT)){ + return timeRange; + } + + if(orderDT==startDT || + orderDT==endDT){ + return timeRange; + } + + } + + return null; + + } + + @override + Widget build(BuildContext context){ + + return Scaffold( + + backgroundColor: Colors.white, + + appBar: AppBar( + + title: + Text("Assign Tanker"), + + ), + + body: + + Column( + + children:[ + + Expanded( + + child: + + SingleChildScrollView( + + padding: + EdgeInsets.all(16), + + child: + + Column( + + crossAxisAlignment: + CrossAxisAlignment.start, + + children:[ + + /// ORDER CARD + + Container( + + padding: + EdgeInsets.all(12), + + decoration: + BoxDecoration( + + borderRadius: + BorderRadius.circular(12), + + border: + Border.all( + color: + Color(0XFFC9C2F0) + ), + + ), + + child: + + Row( + + children:[ + + Column( + + crossAxisAlignment: + CrossAxisAlignment.start, + + children:[ + + Text( + widget.order.building_name + ), + + Text( + widget.order.displayAddress + ), + + ], + + ), + + Spacer(), + + Text( + "${widget.order.distanceInKm} Km" + ), + + ], + + ), + + ), + + SizedBox(height:20), + + /// TANKERS + + Text("SELECT TANKER"), + + SizedBox(height:10), + + ...widget.tankersList + .where((t)=> + + _capToLiters(t.capacity)== + _capToLiters(widget.order.capacity) + + ).toList() + .asMap() + .entries + .map((entry){ + + int idx= + entry.key; + + var d= + entry.value; + + bool blocked= + isTankerBlocked(d); + + return GestureDetector( + + onTap:blocked?null:(){ + + setState(() { + + selectedTankerIndex= + idx; + + selectedDriverIndex= + null; + + }); + + }, + + child: + + Card( + + child: + + Padding( + + padding: + EdgeInsets.all(12), + + child: + + Text( + d.tanker_name + ), + + ), + + ), + + ); + + }).toList(), + + ], + + ), + + ), + + ), + + /// ASSIGN BUTTON + + Padding( + + padding: + EdgeInsets.all(16), + + child: + + SizedBox( + + width:double.infinity, + + child: + + ElevatedButton( + + style: + ElevatedButton.styleFrom( + + backgroundColor: + Color(0XFF8270DB) + + ), + + onPressed:()async{ + + if(selectedTankerIndex==null){ + + AppSettings + .longFailedToast( + "Select tanker" + ); + + return; + + } + + final selectedTanker= + widget.tankersList[ + selectedTankerIndex! + ]; + + final selectedDriver= + selectedDriverIndex!=null + ? + widget.driversList[ + selectedDriverIndex! + ] + : + null; + + final selectedSource= + selectedSourceIndex!=null + ? + widget.sourceLocationsList[ + selectedSourceIndex! + ] + : + null; + + var payload={}; + + payload["tankerName"]= + selectedTanker.tanker_name; + + payload["delivery_agent"]= + selectedDriver?.driver_name; + + payload["delivery_agent_mobile"]= + selectedDriver?.phone_number; + + payload["water_source_location"]= + selectedSource?.source_name; + + bool status= + await AppSettings + .assignTanker( + payload, + widget.order.dbId + ); + + if(status){ + + AppSettings + .longSuccessToast( + "Assigned" + ); + + Navigator.pop( + context, + true + ); + + } + + }, + + child: + + Text("Assign"), + + ), + + ), + + ) + + ], + + ), + + ); + + } + +} \ No newline at end of file diff --git a/lib/orders/collect_money.dart b/lib/orders/collect_money.dart new file mode 100644 index 0000000..6f0b78b --- /dev/null +++ b/lib/orders/collect_money.dart @@ -0,0 +1,224 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import '../common/settings.dart'; +import 'all_orders.dart'; + +class CollectMoney extends StatefulWidget { + var details; + + CollectMoney({this.details}); + + @override + State createState() => _CollectMoneyState(); +} + +class _CollectMoneyState extends State { + String collectAmountInRupees = ''; + + @override + void initState() { + super.initState(); + + // Safe parsing of amount_due (avoid NaN) + String raw = widget.details.amount_due?.toString() ?? ""; + double? parsed = double.tryParse(raw); + + if (parsed == null || parsed.isNaN) { + collectAmountInRupees = "0"; + } else { + collectAmountInRupees = parsed.toStringAsFixed(0); + } + } + + String formatDeliveredDate() { + final now = DateTime.now(); + + // Month names + const months = [ + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" + ]; + + String day = now.day.toString().padLeft(2, '0'); + String month = months[now.month - 1]; + String year = now.year.toString(); + + int hour12 = now.hour > 12 ? now.hour - 12 : (now.hour == 0 ? 12 : now.hour); + String hour = hour12.toString(); + String minute = now.minute.toString().padLeft(2, '0'); + String ampm = now.hour >= 12 ? "PM" : "AM"; + + return "$day-$month-$year $hour:$minute $ampm"; + } + + @override + Widget build(BuildContext context) { + // -------------------------------------------- + // 🔥 UPI QR GENERATION + // -------------------------------------------- + String upiId = widget.details.supplier_upi_id ?? ""; + String supplierName = + Uri.encodeComponent(widget.details.supplier_name ?? "Supplier"); + + String qrData = + "upi://pay?pa=$upiId&pn=$supplierName&am=$collectAmountInRupees&cu=INR&mam=1"; + + // -------------------------------------------- + + return + WillPopScope( + onWillPop: () async { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => AllOrders(navigationFrom:"")), + (route) => false, + ); + return false; + }, + + child: Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: AppSettings.appBarWithNotificationIcon( + widget.details.building_name, + widget.details.type_of_water, + widget.details.capacity, + context, + ), + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), + child: Align( + alignment: const Alignment(0, -0.25), + child: Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 520), + padding: const EdgeInsets.fromLTRB(16, 18, 16, 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFEDEDED)), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + offset: Offset(0, 4)), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 4), + Text( + 'Collect money', + style: fontTextStyle( + 16, const Color(0xFF7E7F80), FontWeight.w500), + ), + const SizedBox(height: 6), + + // AMOUNT + Text( + '₹$collectAmountInRupees', + style: fontTextStyle( + 24, const Color(0xFF343637), FontWeight.w600), + ), + + const SizedBox(height: 16), + + // -------------------------------------------- + // 🔥 QR Code Box + // -------------------------------------------- + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE0E0E0)), + ), + padding: const EdgeInsets.all(12), + child: QrImageView( + data: qrData, + size: 200, + gapless: true, + backgroundColor: Colors.white, + ), + ), + + const SizedBox(height: 18), + + // CONTINUE BUTTON + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: () async { + AppSettings.preLoaderDialog(context); + + bool isOnline = + await AppSettings.internetConnectivity(); + if (!isOnline) { + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast( + "Please Check Internet"); + return; + } + + var payload = { + "amount_paid": widget.details.amount_paid, + "payment_mode": widget.details.payment_mode, + "orderStatus": "completed", + "deliveredDate": formatDeliveredDate(), + }; + + // 🔥 Call API + var response = await AppSettings.amountpaidByCustomer( + widget.details.bookingid, payload); + + Navigator.of(context, rootNavigator: true) + .pop(); // CLOSE LOADER + + if (response.isNotEmpty) { + // Decode JSON + var json = jsonDecode(response); + + if (json["status_code"] == 200) { + + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => AllOrders(navigationFrom:"")), + (route) => false, + ); + + return; + } else {} + AppSettings.longFailedToast( + json["msg"] ?? "Invalid OTP"); + return; + } + // TODO: Navigate home + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: Text( + 'Continue to Home', + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + )); + } +} diff --git a/lib/orders/delivery_updates.dart b/lib/orders/delivery_updates.dart index a2e6cf9..d835755 100644 --- a/lib/orders/delivery_updates.dart +++ b/lib/orders/delivery_updates.dart @@ -1,21 +1,24 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; - import 'package:supplier_new/common/settings.dart'; +import 'otp_start_unloading.dart'; +import 'unloading_inprogress.dart'; +import 'collect_money.dart'; class DeliveryUpdatesPage extends StatefulWidget { + var details; final String orderId; final String initialStatus; - const DeliveryUpdatesPage({ + DeliveryUpdatesPage({ super.key, + this.details, required this.orderId, required this.initialStatus, }); @@ -25,853 +28,715 @@ class DeliveryUpdatesPage extends StatefulWidget { } class _DeliveryUpdatesPageState extends State { + GoogleMapController? _mapController; Timer? _timer; final PolylinePoints _polylinePoints = PolylinePoints(); - /// ---------------- CHANGE THIS ---------------- - /// Put your Google Maps API key here - /// Need Directions API + Distance Matrix API enabled - static const String googleApiKey = 'AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM'; - /// -------------------------------------------- + static const String googleApiKey = "YOUR_GOOGLE_KEY"; - bool isLoading = true; - bool mapReady = false; + bool isLoading=true; + bool mapReady=false; - String currentStatus = ''; - String etaText = '--'; - String distanceText = '--'; - String durationText = '--'; + String currentStatus=''; + String etaText='--'; + String distanceText='--'; + String durationText='--'; - String driverName = ''; - String driverPhone = ''; - String tankerName = ''; - String orderAddress = ''; + String driverName=''; + String driverPhone=''; + String tankerName=''; + String orderAddress=''; double? driverLat; double? driverLng; + double? destinationLat; double? destinationLng; - Set markers = {}; - Set polylines = {}; + Set markers={}; + Set polylines={}; - List routePoints = []; + List routePoints=[]; + LatLng? _lastDriverPosition; + bool firstCameraMove=true; @override - void initState() { + void initState(){ + super.initState(); - currentStatus = _beautifyStatus(widget.initialStatus); - _loadTracking(showLoader: true); - _timer = Timer.periodic(const Duration(seconds: 5), (_) { - _loadTracking(showLoader: false); - }); + currentStatus=_beautifyStatus(widget.initialStatus); + + _loadTracking(showLoader:true); + + _timer=Timer.periodic( + Duration(seconds:5), + (_){ + _loadTracking(); + }); + } @override - void dispose() { + void dispose(){ + _timer?.cancel(); _mapController?.dispose(); + super.dispose(); } - Future _loadTracking({bool showLoader = false}) async { - if (showLoader && mounted) { - setState(() { - isLoading = true; - }); - } + Future _loadTracking({bool showLoader=false}) async{ - try { - /// =========================== - /// IMPORTANT: - /// Change this endpoint path to your real backend route - /// Example: - /// ${AppSettings.host}trackOrder/${widget.orderId} - /// ${AppSettings.host}getOrderTracking/${widget.orderId} - /// =========================== - final uri = Uri.parse( - '${AppSettings.host}trackOrder/${widget.orderId}', + try{ + + final uri=Uri.parse( + '${AppSettings.host}trackOrder/${widget.orderId}' ); - final response = await http.get(uri); + final response=await http.get(uri); - if (response.statusCode < 200 || response.statusCode >= 300) { - throw Exception('Tracking API failed: ${response.statusCode}'); - } + final decoded=jsonDecode(response.body); - final decoded = jsonDecode(response.body); - - /// Supports either: - /// { status:true, data:{...} } - /// OR direct flat response - final data = decoded is Map && decoded['data'] is Map - ? decoded['data'] as Map - : decoded as Map; - - driverLat = _toDouble( - data['driverLat'] ?? - data['driver_lat'] ?? - data['deliveryBoyLat'] ?? - data['deliveryboyLat'] ?? - data['lat'], - ); + final data=decoded["data"]??decoded; - driverLng = _toDouble( - data['driverLng'] ?? - data['driver_lng'] ?? - data['deliveryBoyLng'] ?? - data['deliveryboyLng'] ?? - data['lng'] ?? - data['long'], - ); + driverLat=_toDouble(data["driverLat"]); + driverLng=_toDouble(data["driverLng"]); - destinationLat = _toDouble( - data['destinationLat'] ?? - data['destination_lat'] ?? - data['orderLat'] ?? - data['customerLat'] ?? - data['dropLat'], - ); + destinationLat=_toDouble(data["destinationLat"]); + destinationLng=_toDouble(data["destinationLng"]); + + driverName=(data["driverName"]??"").toString(); + + driverPhone=(data["driverPhone"]??"").toString(); + + tankerName=(data["tankerName"]??"").toString(); - destinationLng = _toDouble( - data['destinationLng'] ?? - data['destination_lng'] ?? - data['orderLng'] ?? - data['customerLng'] ?? - data['dropLng'], + orderAddress=(data["address"]??"").toString(); + + currentStatus=_beautifyStatus( + (data["status"]??widget.initialStatus).toString() ); - driverName = (data['driverName'] ?? - data['delivery_agent_name'] ?? - data['deliveryBoyName'] ?? - '') - .toString(); - - driverPhone = (data['driverPhone'] ?? - data['delivery_agent_mobile'] ?? - data['deliveryBoyPhone'] ?? - '') - .toString(); - - tankerName = (data['tankerName'] ?? data['tanker_name'] ?? '').toString(); - - orderAddress = (data['destinationAddress'] ?? - data['displayAddress'] ?? - data['address'] ?? - data['customerAddress'] ?? - '') - .toString(); - - final apiStatus = - (data['status'] ?? data['orderStatus'] ?? widget.initialStatus) - .toString(); - currentStatus = _beautifyStatus(apiStatus); - - final apiEta = - (data['eta'] ?? data['etaText'] ?? data['estimatedTime'] ?? '') - .toString() - .trim(); - - final apiDistance = - (data['distance'] ?? data['distanceText'] ?? '').toString().trim(); - - if (apiEta.isNotEmpty) etaText = apiEta; - if (apiDistance.isNotEmpty) distanceText = apiDistance; - - if (driverLat != null && - driverLng != null && - destinationLat != null && - destinationLng != null) { + if(driverLat!=null && + driverLng!=null && + destinationLat!=null && + destinationLng!=null){ + await _buildMarkers(); - await _fetchGoogleRouteAndEta(); + if(currentStatus.toLowerCase().contains("out for delivery")){ + + await _fetchGoogleRouteAndEta(); + + } await _moveCameraToBounds(); - } - if (mounted) { - setState(() { - isLoading = false; - }); - } - } catch (e) { - debugPrint('Tracking error: $e'); - if (mounted) { - setState(() { - isLoading = false; - }); } + + setState(() { + isLoading=false; + }); + + } + catch(e){ + + setState(() { + isLoading=false; + }); + } + + } + + double _calculateDistanceKm( + double startLat, + double startLng, + double endLat, + double endLng){ + + const double earthRadius=6371; + + final dLat=_degToRad(endLat-startLat); + final dLng=_degToRad(endLng-startLng); + + final a= + sin(dLat/2)*sin(dLat/2)+ + cos(_degToRad(startLat))* + cos(_degToRad(endLat))* + sin(dLng/2)* + sin(dLng/2); + + final c=2*atan2(sqrt(a),sqrt(1-a)); + + return earthRadius*c; + } + + double _degToRad(double deg){ + return deg*pi/180; } - Future _buildMarkers() async { - if (driverLat == null || - driverLng == null || - destinationLat == null || - destinationLng == null) { + Future _buildMarkers() async{ + + if(driverLat==null || + driverLng==null || + destinationLat==null || + destinationLng==null){ return; } - markers = { + LatLng newDriverPos= + LatLng(driverLat!,driverLng!); + + /// CHECK MOVEMENT DISTANCE + bool moved=true; + + if(_lastDriverPosition!=null){ + + double diff= + _calculateDistanceKm( + _lastDriverPosition!.latitude, + _lastDriverPosition!.longitude, + newDriverPos.latitude, + newDriverPos.longitude + ); + + /// UPDATE ROUTE ONLY IF DRIVER MOVED > 30m + moved=diff>0.03; + + } + + _lastDriverPosition=newDriverPos; + + markers={ + Marker( - markerId: const MarkerId('driver'), - position: LatLng(driverLat!, driverLng!), - infoWindow: InfoWindow( - title: driverName.isNotEmpty ? driverName : 'Driver', - snippet: tankerName.isNotEmpty ? tankerName : 'Live location', - ), - icon: BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueAzure, - ), + markerId:MarkerId("driver"), + position:newDriverPos, + rotation:0, + icon:BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueAzure + ) ), + Marker( - markerId: const MarkerId('destination'), - position: LatLng(destinationLat!, destinationLng!), - infoWindow: InfoWindow( - title: 'Destination', - snippet: orderAddress.isNotEmpty ? orderAddress : 'Order location', - ), - icon: BitmapDescriptor.defaultMarkerWithHue( - BitmapDescriptor.hueRed, - ), - ), + markerId:MarkerId("destination"), + position:LatLng(destinationLat!,destinationLng!), + icon:BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueRed + ) + ) + }; - } - Future _fetchGoogleRouteAndEta() async { - if (driverLat == null || - driverLng == null || - destinationLat == null || - destinationLng == null) { - return; - } + /// FOLLOW DRIVER CAMERA + if(_mapController!=null){ - try { - final origin = '${driverLat!},${driverLng!}'; - final destination = '${destinationLat!},${destinationLng!}'; - - final directionsUrl = Uri.parse( - 'https://maps.googleapis.com/maps/api/directions/json' - '?origin=$origin' - '&destination=$destination' - '&departure_time=now' - '&traffic_model=best_guess' - '&mode=driving' - '&key=$googleApiKey', - ); + if(firstCameraMove){ - final directionsResponse = await http.get(directionsUrl); - final directionsData = jsonDecode(directionsResponse.body); - - if (directionsData['routes'] != null && - (directionsData['routes'] as List).isNotEmpty) { - final route = directionsData['routes'][0]; - final overviewPolyline = - route['overview_polyline']?['points']?.toString() ?? ''; - - final legs = route['legs'] as List?; - if (legs != null && legs.isNotEmpty) { - final leg = legs[0]; - final distanceObj = leg['distance']; - final durationObj = leg['duration']; - final durationTrafficObj = leg['duration_in_traffic']; - - if (distanceObj != null && (distanceText == '--' || distanceText.isEmpty)) { - distanceText = (distanceObj['text'] ?? '--').toString(); - } - - if (durationObj != null) { - durationText = (durationObj['text'] ?? '--').toString(); - } - - if (durationTrafficObj != null) { - etaText = (durationTrafficObj['text'] ?? '--').toString(); - } else if (durationObj != null && (etaText == '--' || etaText.isEmpty)) { - etaText = (durationObj['text'] ?? '--').toString(); - } - } + _mapController!.animateCamera( + + CameraUpdate.newLatLngZoom( + newDriverPos, + 15 + ) + + ); + + firstCameraMove=false; + + } + else{ + + _mapController!.animateCamera( + + CameraUpdate.newLatLng( + newDriverPos + ) + + ); - final result = _polylinePoints.decodePolyline(overviewPolyline); - routePoints = result - .map((p) => LatLng(p.latitude, p.longitude)) - .toList(); - - polylines = { - Polyline( - polylineId: const PolylineId('delivery_route'), - points: routePoints, - width: 5, - color: const Color(0XFF8270DB), - ), - }; - } else { - _setStraightLinePolyline(); - _setApproxEtaIfNeeded(); } - } catch (e) { - debugPrint('Directions error: $e'); - _setStraightLinePolyline(); - _setApproxEtaIfNeeded(); - } - } - void _setStraightLinePolyline() { - if (driverLat == null || - driverLng == null || - destinationLat == null || - destinationLng == null) { - return; } - polylines = { - Polyline( - polylineId: const PolylineId('delivery_route_fallback'), - points: [ - LatLng(driverLat!, driverLng!), - LatLng(destinationLat!, destinationLng!), - ], - width: 5, - color: const Color(0XFF8270DB), - ), - }; } - void _setApproxEtaIfNeeded() { - if (driverLat == null || - driverLng == null || - destinationLat == null || - destinationLng == null) { + Future _fetchGoogleRouteAndEta() async{ + + if(driverLat==null || + driverLng==null || + destinationLat==null || + destinationLng==null){ return; } - final km = _calculateDistanceKm( - driverLat!, - driverLng!, - destinationLat!, - destinationLng!, + final origin='${driverLat!},${driverLng!}'; + final destination='${destinationLat!},${destinationLng!}'; + + final url=Uri.parse( + + "https://maps.googleapis.com/maps/api/directions/json?" + "origin=$origin" + "&destination=$destination" + "&departure_time=now" + "&traffic_model=best_guess" + "&mode=driving" + "&key=$googleApiKey" ); - if (distanceText == '--' || distanceText.isEmpty) { - distanceText = '${km.toStringAsFixed(1)} km'; - } + try{ + + final res=await http.get(url); + + final data=jsonDecode(res.body); + + if(data["routes"]==null || data["routes"].isEmpty)return; + + final route=data["routes"][0]; + + final leg=route["legs"][0]; + + /// DISTANCE + distanceText= + (leg["distance"]["text"]??"--").toString(); + + /// DURATION + durationText= + (leg["duration"]["text"]??"--").toString(); + + /// ETA WITH TRAFFIC + if(leg["duration_in_traffic"]!=null){ + + etaText= + leg["duration_in_traffic"]["text"].toString(); + + } + else{ + + etaText=durationText; + + } + + /// ROUTE POLYLINE + final poly= + route["overview_polyline"]["points"]; + + final decoded= + _polylinePoints.decodePolyline(poly); + + routePoints= + decoded.map((e)=> + LatLng(e.latitude,e.longitude) + ).toList(); + + polylines={ + + Polyline( + polylineId:PolylineId("route"), + points:routePoints, + width:5, + color:Color(0XFF8270DB) + ) + + }; - /// Rough city driving estimate at 25 km/h - final mins = ((km / 25) * 60).ceil().clamp(1, 999); - if (etaText == '--' || etaText.isEmpty) { - etaText = '$mins mins'; } - if (durationText == '--' || durationText.isEmpty) { - durationText = '$mins mins'; + catch(e){ + + print("Route error $e"); + } + } - Future _moveCameraToBounds() async { - if (!mapReady || _mapController == null) return; - if (driverLat == null || - driverLng == null || - destinationLat == null || - destinationLng == null) { - return; - } + Future _moveCameraToBounds() async{ + + if(_mapController==null)return; + + LatLngBounds bounds=LatLngBounds( + + southwest:LatLng( + min(driverLat!,destinationLat!), + min(driverLng!,destinationLng!) + ), + + northeast:LatLng( + max(driverLat!,destinationLat!), + max(driverLng!,destinationLng!) + ) - final southwest = LatLng( - min(driverLat!, destinationLat!), - min(driverLng!, destinationLng!), - ); - final northeast = LatLng( - max(driverLat!, destinationLat!), - max(driverLng!, destinationLng!), ); - final bounds = LatLngBounds( - southwest: southwest, - northeast: northeast, + _mapController!.animateCamera( + CameraUpdate.newLatLngBounds(bounds,70) ); - try { - await _mapController!.animateCamera( - CameraUpdate.newLatLngBounds(bounds, 70), - ); - } catch (_) { - await Future.delayed(const Duration(milliseconds: 300)); - try { - await _mapController!.animateCamera( - CameraUpdate.newLatLngBounds(bounds, 70), - ); - } catch (_) {} - } } - double _calculateDistanceKm( - double startLat, - double startLng, - double endLat, - double endLng, - ) { - const double earthRadius = 6371; + double? _toDouble(val){ - final dLat = _degToRad(endLat - startLat); - final dLng = _degToRad(endLng - startLng); + if(val==null)return null; - final a = sin(dLat / 2) * sin(dLat / 2) + - cos(_degToRad(startLat)) * - cos(_degToRad(endLat)) * - sin(dLng / 2) * - sin(dLng / 2); + return double.tryParse(val.toString()); - final c = 2 * atan2(sqrt(a), sqrt(1 - a)); - return earthRadius * c; } - double _degToRad(double deg) => deg * pi / 180; + String _beautifyStatus(String s){ + + return s.replaceAll("_"," ").toUpperCase(); - double? _toDouble(dynamic val) { - if (val == null) return null; - if (val is double) return val; - if (val is int) return val.toDouble(); - return double.tryParse(val.toString()); } - String _beautifyStatus(String s) { - final value = s.trim().toLowerCase(); + /// STATUS FLOW BUTTON + Widget _buildActionButton(){ + + String status=currentStatus + .replaceAll("_"," ") + .toLowerCase() + .trim(); + + if(status.contains("arrived")){ + + return _actionButton( + + "Start Unloading", + + (){ + + Navigator.push( + context, + MaterialPageRoute( + builder:(_)=>UnloadArrivalScreen( + details:widget.details + ) + ) + ); + + }); - if (value == 'advance_paid' || value == 'accepted') { - return 'Pending'; } - if (value == 'deliveryboy_assigned' || value == 'tanker_assigned') { - return 'Assigned'; + if(status.contains("arrived")){ + etaText="Reached destination"; } - if (value == 'delivered') { - return 'Completed'; + + if(status.contains("unloading started")){ + + return _actionButton( + + "Unloading Progress", + + (){ + + Navigator.push( + context, + MaterialPageRoute( + builder:(_)=>UnloadingInProgressScreen( + details:widget.details + ) + ) + ); + + }); + } - return value - .replaceAll('_', ' ') - .split(' ') - .map((e) => e.isEmpty - ? e - : '${e[0].toUpperCase()}${e.substring(1)}') - .join(' '); - } + if(status.contains("unloading stopped")){ + + return _actionButton( + + "Collect Payment", + + (){ + + Navigator.push( + context, + MaterialPageRoute( + builder:(_)=>CollectMoney( + details:widget.details + ) + ) + ); + + }); - int _statusStepIndex() { - final raw = widget.initialStatus.trim().toLowerCase(); - final live = currentStatus.trim().toLowerCase().replaceAll(' ', '_'); - final s = live.isNotEmpty ? live : raw; - - switch (s) { - case 'accepted': - case 'advance_paid': - return 0; - case 'deliveryboy_assigned': - case 'tanker_assigned': - return 1; - case 'pickup_started': - return 2; - case 'start_loading': - case 'loading_completed': - return 3; - case 'out_for_delivery': - case 'arrived': - return 4; - case 'unloading_started': - case 'unloading_stopped': - case 'payment_pending': - return 5; - case 'delivered': - return 6; - default: - return 0; } - } - Future _callDriver() async { - if (driverPhone.trim().isEmpty) return; + if(status.contains("payment pending")){ + + return _actionButton( + + "Complete Delivery", + + (){ + + Navigator.push( + context, + MaterialPageRoute( + builder:(_)=>CollectMoney( + details:widget.details + ) + ) + ); + + }); - final uri = Uri.parse('tel:$driverPhone'); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); } + + return SizedBox(); + } - Widget _buildTopMapCard() { - final initialTarget = LatLng( - driverLat ?? 17.385, - driverLng ?? 78.486, - ); + Widget _actionButton( + String text, + VoidCallback onTap){ - return Expanded( - child: Stack( - children: [ - GoogleMap( - initialCameraPosition: CameraPosition( - target: initialTarget, - zoom: 14, - ), - myLocationButtonEnabled: false, - zoomControlsEnabled: false, - compassEnabled: true, - trafficEnabled: true, - markers: markers, - polylines: polylines, - onMapCreated: (controller) async { - _mapController = controller; - mapReady = true; - await _moveCameraToBounds(); - }, - ), + return SizedBox( - Positioned( - top: 14, - left: 14, - right: 14, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 3), - ) - ], - ), - child: Row( - children: [ - _miniMetric('ETA', etaText), - const SizedBox(width: 10), - _miniMetric('Distance', distanceText), - const SizedBox(width: 10), - _miniMetric('Status', currentStatus), - ], - ), - ), - ), + width:double.infinity, + + child:ElevatedButton( + + style:ElevatedButton.styleFrom( + + backgroundColor:Color(0XFF8270DB), + + padding:EdgeInsets.symmetric(vertical:15), + + shape:RoundedRectangleBorder( + borderRadius:BorderRadius.circular(20) + ) - Positioned( - right: 14, - bottom: 14, - child: FloatingActionButton( - heroTag: 'fit_map_btn', - backgroundColor: const Color(0XFF8270DB), - onPressed: _moveCameraToBounds, - child: const Icon(Icons.center_focus_strong, color: Colors.white), - ), ), - ], + + onPressed:onTap, + + child:Text( + text, + style:TextStyle( + color:Colors.white, + fontSize:16, + fontWeight:FontWeight.w600 + ) + ) + ), + ); + } - Widget _miniMetric(String title, String value) { - return Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - decoration: BoxDecoration( - color: const Color(0XFFF7F7FB), - borderRadius: BorderRadius.circular(14), - ), - child: Column( - children: [ - Text( - title, - style: const TextStyle( - fontSize: 11, - color: Color(0XFF777777), - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 6), - Text( - value.isEmpty ? '--' : value, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 12, - color: Color(0XFF2D2E30), - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ); + Future _callDriver() async{ + + if(widget.details.delivery_agent_mobile.isEmpty)return; + + final uri=Uri.parse("tel:${widget.details.delivery_agent_mobile}"); + + await launchUrl(uri); + } - Widget _buildBottomPanel() { - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(16, 16, 16, 18), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(26)), + @override + Widget build(BuildContext context){ + + return Scaffold( + + backgroundColor:Color(0XFFF2F2F2), + + appBar:AppBar( + title:Text("Track Delivery"), + backgroundColor:Colors.white, ), - child: SingleChildScrollView( - child: Column( - children: [ - Container( - width: 44, - height: 5, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(20), - ), - ), - const SizedBox(height: 16), - - Row( - children: [ - CircleAvatar( - radius: 24, - backgroundColor: const Color(0XFFEEE9FF), - child: const Icon( - Icons.person, - color: Color(0XFF8270DB), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - driverName.isNotEmpty ? driverName : 'Driver not assigned', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Color(0XFF2D2E30), - ), - ), - const SizedBox(height: 4), - Text( - tankerName.isNotEmpty - ? tankerName - : 'Delivery partner', - style: const TextStyle( - fontSize: 13, - color: Color(0XFF6A6B6D), - fontWeight: FontWeight.w500, - ), + + body:isLoading? + + Center(child:CircularProgressIndicator()) + + : + + Column( + + children:[ + + Expanded( + + child:GoogleMap( + + initialCameraPosition: + + CameraPosition( + + target:LatLng( + driverLat??17.385, + driverLng??78.486 ), - ], + + zoom:14 + ), - ), - InkWell( - onTap: driverPhone.trim().isEmpty ? null : _callDriver, - borderRadius: BorderRadius.circular(16), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 10, - ), - decoration: BoxDecoration( - color: driverPhone.trim().isEmpty - ? Colors.grey.shade300 - : const Color(0XFF8270DB), - borderRadius: BorderRadius.circular(16), + + markers:markers, + + polylines:polylines, + + onMapCreated:(c){ + + _mapController=c; + + mapReady=true; + + } + + ) + + ), + + Container( + + padding:EdgeInsets.all(16), + + decoration:BoxDecoration( + + color:Colors.white, + + borderRadius:BorderRadius.vertical( + top:Radius.circular(25) + ) + + ), + + child:Column( + + children:[ + + Row( + + children:[ + + Expanded( + + child:Text( + "${widget.details.delivery_agent_name}", + style:TextStyle( + fontSize:16, + fontWeight:FontWeight.bold + ) + ) + ), - child: const Row( + + IconButton( + + onPressed:_callDriver, + + icon:Icon(Icons.call) + + ) + + ], + + ), + + SizedBox(height:10), + Text( + "Status : $currentStatus" + ), + SizedBox(height:8), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.call, color: Colors.white, size: 16), - SizedBox(width: 6), + Text( - 'Call', + "ETA", style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, + fontSize:12, + color:Colors.grey ), ), + + Text( + etaText, + style: TextStyle( + fontSize:14, + fontWeight:FontWeight.bold + ), + ) + ], ), - ), - ) - ], - ), - const SizedBox(height: 16), - - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: const Color(0XFFF7F7FB), - borderRadius: BorderRadius.circular(18), - ), - child: Column( - children: [ - _infoRow('Order ID', widget.orderId), - const SizedBox(height: 10), - _infoRow('Current Status', currentStatus), - const SizedBox(height: 10), - _infoRow('ETA to Destination', etaText), - const SizedBox(height: 10), - _infoRow('Remaining Distance', distanceText), - if (orderAddress.trim().isNotEmpty) ...[ - const SizedBox(height: 10), - _infoRow('Destination', orderAddress), - ], - ], - ), - ), + Column( + children: [ - const SizedBox(height: 18), - _buildTimeline(), - ], - ), - ), - ); - } + Text( + "Distance", + style: TextStyle( + fontSize:12, + color:Colors.grey + ), + ), - Widget _infoRow(String title, String value) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 130, - child: Text( - title, - style: const TextStyle( - fontSize: 13, - color: Color(0XFF6B6C6E), - fontWeight: FontWeight.w500, - ), - ), - ), - Expanded( - child: Text( - value.isEmpty ? '--' : value, - style: const TextStyle( - fontSize: 13, - color: Color(0XFF222222), - fontWeight: FontWeight.w700, - ), - ), - ), - ], - ); - } + Text( + distanceText, + style: TextStyle( + fontSize:14, + fontWeight:FontWeight.bold + ), + ) - Widget _buildTimeline() { - final currentIndex = _statusStepIndex(); - - final steps = [ - 'Order Confirmed', - 'Driver Assigned', - 'Pickup Started', - 'Loading', - 'Out for Delivery', - 'Unloading', - 'Completed', - ]; - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: const Color(0XFFF7F7FB), - borderRadius: BorderRadius.circular(18), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Delivery Progress', - style: TextStyle( - fontSize: 15, - color: Color(0XFF2D2E30), - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 14), - ...List.generate(steps.length, (index) { - final done = index <= currentIndex; - final isLast = index == steps.length - 1; - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - children: [ - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: done - ? const Color(0XFF8270DB) - : Colors.grey.shade300, - shape: BoxShape.circle, - ), - child: done - ? const Icon(Icons.check, - size: 12, color: Colors.white) - : null, + ], ), - if (!isLast) - Container( - width: 2, - height: 28, - color: done - ? const Color(0XFF8270DB) - : Colors.grey.shade300, - ), + + Column( + children: [ + + Text( + "Duration", + style: TextStyle( + fontSize:12, + color:Colors.grey + ), + ), + + Text( + durationText, + style: TextStyle( + fontSize:14, + fontWeight:FontWeight.bold + ), + ) + + ], + ) + ], ), - const SizedBox(width: 12), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 1), - child: Text( - steps[index], - style: TextStyle( - fontSize: 14, - fontWeight: done ? FontWeight.w700 : FontWeight.w500, - color: done - ? const Color(0XFF2D2E30) - : const Color(0XFF8A8B8D), - ), - ), - ), - ), + + SizedBox(height:15), + SizedBox(height:20), + + _buildActionButton(), + + SizedBox(height:10), + ], - ); - }), - ], - ), - ); - } - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0XFFF2F2F2), - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.white, - iconTheme: const IconThemeData(color: Color(0XFF2D2E30)), - title: const Text( - 'Track Delivery', - style: TextStyle( - color: Color(0XFF2D2E30), - fontSize: 18, - fontWeight: FontWeight.w700, - ), - ), - ), - body: isLoading - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - _buildTopMapCard(), - SizedBox( - height: MediaQuery.of(context).size.height * 0.40, - child: _buildBottomPanel(), - ), + ), + + ) + ], + ), + ); + } + } \ No newline at end of file diff --git a/lib/orders/orders_model.dart b/lib/orders/orders_model.dart index 6eadcbc..75e8ffa 100644 --- a/lib/orders/orders_model.dart +++ b/lib/orders/orders_model.dart @@ -27,6 +27,30 @@ class OrdersModel { String water_source_location=''; String remarks=''; + String unload_start_time = ''; + + String bookingid = ''; + String tank_name = ''; + String tankLocation = ''; + + List extraStatus = []; + + double? sourceLat; + double? sourceLng; + double? buildingLat; + double? buildingLng; + String delivery_agent_mobile=''; + String amount_due = ''; + String amount_paid = ''; + String supplier_upi_id = '9000950877@ybl'; + String supplier_name = ''; + String payment_mode = ''; + String tanker_status = ''; + String tanker_availability = ''; + String start_time = ''; + String stop_time = ''; + DateTime? orderDate; + OrdersModel(); factory OrdersModel.fromJson(Map json){ @@ -49,6 +73,8 @@ class OrdersModel { rtvm.tanker_name = json['tankerName'] ?? ''; rtvm.water_source_location = json['water_source_location'] ?? ''; rtvm.remarks = json['remarks'] ?? ''; + rtvm.delivery_agent_mobile=json['delivery_agent_mobile']??''; + // Split and trim List parts = rtvm.address.split(',').map((e) => e.trim()).toList(); @@ -72,6 +98,84 @@ class OrdersModel { (rtvm.distanceInMeters / 1000).toStringAsFixed(2), ); + + rtvm.tank_name = json['tankName'] ?? ''; + rtvm.tankLocation = json['tankLocation'] ?? ''; + + rtvm.amount_due = json['amount_due'] ?? ''; + rtvm.amount_paid = json['amount_paid'] ?? ''; + rtvm.supplier_name = json['supplierName'] ?? ''; + rtvm.payment_mode = json['payment_mode'] ?? ''; + rtvm.start_time = json['start_time'] ?? ''; + rtvm.stop_time = json['stop_time'] ?? ''; + + if (rtvm.date.isNotEmpty) { + final parts = rtvm.date.split('-'); + + if (parts.length == 3) { + final day = int.tryParse(parts[0]) ?? 1; + final year = int.tryParse(parts[2]) ?? 1970; + + final monthStr = parts[1].toLowerCase(); + + const monthMap = { + 'jan': 1, + 'feb': 2, + 'mar': 3, + 'apr': 4, + 'may': 5, + 'jun': 6, + 'jul': 7, + 'aug': 8, + 'sep': 9, + 'oct': 10, + 'nov': 11, + 'dec': 12, + }; + + final month = monthMap[monthStr]; + + if (month != null) { + rtvm.orderDate = DateTime(year, month, day); + } + } + } + + // ---------------- EXTRA DATA ---------------- + if (json['extra'] != null && json['extra'] is List) { + for (var item in json['extra']) { + if (item['status'] != null && item['status'] is List) { + List statusList = + List.from(item['status'].map((e) => e.toString())); + + rtvm.extraStatus = statusList; + + if (statusList.length >= 1) { + rtvm.tanker_status = statusList[0].toLowerCase(); + } + + if (statusList.length >= 2) { + rtvm.tanker_availability = statusList[1].toLowerCase(); + } + } + + if (item['source_location'] != null) { + rtvm.sourceLat = + (item['source_location']['latitude'] ?? 0).toDouble(); + rtvm.sourceLng = + (item['source_location']['longitude'] ?? 0).toDouble(); + } + + if (item['building_location'] != null) { + rtvm.buildingLat = + (item['building_location']['latitude'] ?? 0).toDouble(); + rtvm.buildingLng = + (item['building_location']['longitude'] ?? 0).toDouble(); + } + } + } + + return rtvm; } Map toJson() => { diff --git a/lib/orders/otp_start_unloading.dart b/lib/orders/otp_start_unloading.dart new file mode 100644 index 0000000..fa762ab --- /dev/null +++ b/lib/orders/otp_start_unloading.dart @@ -0,0 +1,381 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:supplier_new/orders/unloading_inprogress.dart'; + +import '../common/settings.dart'; +import 'all_orders.dart'; + +class UnloadArrivalScreen extends StatefulWidget { + + var details; + + UnloadArrivalScreen({ + this.details + }); + @override + State createState() => _UnloadArrivalScreenState(); +} + +class _UnloadArrivalScreenState extends State with TickerProviderStateMixin{ + final _c1 = TextEditingController(); + final _c2 = TextEditingController(); + final _c3 = TextEditingController(); + final _c4 = TextEditingController(); + + final _f1 = FocusNode(); + final _f2 = FocusNode(); + final _f3 = FocusNode(); + final _f4 = FocusNode(); + + bool get _otpReady => + _c1.text.isNotEmpty && + _c2.text.isNotEmpty && + _c3.text.isNotEmpty && + _c4.text.isNotEmpty; + + late AnimationController _shakeController; + late Animation _offsetAnimation; + bool _isOtpComplete = false; + + void _checkOtpFilled() { + setState(() { + _isOtpComplete = + _c1.text.isNotEmpty && + _c2.text.isNotEmpty && + _c3.text.isNotEmpty && + _c4.text.isNotEmpty; + }); + } + + @override + void dispose() { + _c1.dispose(); + _c2.dispose(); + _c3.dispose(); + _c4.dispose(); + _f1.dispose(); + _f2.dispose(); + _f3.dispose(); + _f4.dispose(); + _shakeController.dispose(); + super.dispose(); + } + @override + void initState() { + super.initState(); + + _shakeController = AnimationController( + duration: const Duration(milliseconds: 450), + vsync: this, + ); + + _offsetAnimation = Tween(begin: 0.0, end: 15.0) + .chain(CurveTween(curve: Curves.elasticIn)) + .animate(_shakeController); + } + + + void _onDigit({ + required String value, + required FocusNode current, + FocusNode? next, + FocusNode? prev, + }) { + if (value.length == 1 && next != null) { + next.requestFocus(); + } else if (value.isEmpty && prev != null) { + prev.requestFocus(); + } + setState(() {}); + } + + InputDecoration _otpDecoration(bool focused) => InputDecoration( + counterText: '', + contentPadding: const EdgeInsets.symmetric(vertical: 14), + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFE0E0E0)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.black, width: 1.2), + ), + ); + + @override + Widget build(BuildContext context) { + return + WillPopScope( + onWillPop: () async { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => AllOrders(navigationFrom:"")), + (route) => false, + ); + return false;}, + child:Scaffold( + backgroundColor: Colors.white, + appBar: AppSettings.appBarWithNotificationIcon(widget.details.building_name, widget.details.type_of_water, widget.details.capacity, context), + + body: Stack( + children: [ + // Map background + Positioned.fill( + child: Image.asset( + 'images/google_maps.png', // make sure this exists & is declared in pubspec.yaml + fit: BoxFit.cover, + ), + ), + + // Success text + Positioned( + left: 16, + right: 16, + top: 8, + child: Padding( + padding: const EdgeInsets.only(top: 24, left: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('You have reached!', + style: fontTextStyle( + 20, const Color(0xFF101214), FontWeight.w600)), + const SizedBox(height: 4), + Text('Hurray! You are on-time.', + style: fontTextStyle( + 16, const Color(0xFF0A9E04), FontWeight.w500)), + ], + ), + ), + ), + + // Center card (nudged up) + Align( + alignment: const Alignment(0, -0.6), + child: Container( + width: MediaQuery.of(context).size.width * 0.86, + padding: const EdgeInsets.fromLTRB(16, 18, 16, 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 10, + offset: Offset(0, 4)) + ], + ), + child: widget.details.tank_name!=''?Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(widget.details.tank_name!=''?"Unload Water in":'The user hasn’t selected a tank. Please ask the user to select one.', + textAlign: TextAlign.center, + style: fontTextStyle( + 16, const Color(0xFF939495), FontWeight.w500)), + const SizedBox(height: 4), + Visibility( + visible: widget.details.tank_name!='', + child: Text(widget.details.tank_name, + style: fontTextStyle( + 20, const Color(0xFF101214), FontWeight.w600)),), + + const SizedBox(height: 14), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Enter OTP to Start Unloading', + style: fontTextStyle(14, const Color(0xFF646566), + FontWeight.w500)), + const SizedBox(width: 6), + Icon(Icons.help_outline, + size: 16, color: Colors.grey.shade600), + ], + ), + + const SizedBox(height: 14), + + // OTP boxes + AnimatedBuilder( + animation: _shakeController, + builder: (context, child) { + return Transform.translate( + offset: Offset(_offsetAnimation.value, 0), + child: child, + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _OtpBox( + controller: _c1, + focusNode: _f1, + onChanged: (v){ + _onDigit(value: v, current: _f1, next: _f2); + _checkOtpFilled(); + }, + //onChanged: (v) => _onDigit(value: v, current: _f1, next: _f2), + decoration: _otpDecoration(_f1.hasFocus), + ), + _OtpBox( + controller: _c2, + focusNode: _f2, + onChanged: (v){ + _onDigit(value: v, current: _f2, next: _f3, prev: _f1); + _checkOtpFilled(); + }, + decoration: _otpDecoration(_f2.hasFocus), + ), + _OtpBox( + controller: _c3, + focusNode: _f3, + onChanged: (v){ + _onDigit(value: v, current: _f3, next: _f4, prev: _f2); + _checkOtpFilled(); + }, + decoration: _otpDecoration(_f3.hasFocus), + ), + _OtpBox( + controller: _c4, + focusNode: _f4, + onChanged: (v){ + _onDigit(value: v, current: _f4, prev: _f3); + _checkOtpFilled(); + }, + decoration: _otpDecoration(_f4.hasFocus), + ), + ], + ), + ), + + + const SizedBox(height: 16), + + // Continue button -> navigate to UnloadingInProgressScreen + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _isOtpComplete ? () async { + if (!_otpReady) return; + + AppSettings.preLoaderDialog(context); + + bool isOnline = await AppSettings.internetConnectivity(); + if (!isOnline) { + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast("Please Check Internet"); + return; + } + + // Combine OTP + final otp = "${_c1.text}${_c2.text}${_c3.text}${_c4.text}"; + + var payload = { + "action": "start", + "percentage": "100", + "otp": otp, + }; + + // 🔥 Call API + var response = await AppSettings.verifyUnloadStartOTP( + widget.details.bookingid, + payload + ); + + Navigator.of(context, rootNavigator: true).pop(); // CLOSE LOADER + + if (response.isNotEmpty) { + // Decode JSON + var json = jsonDecode(response); + + if (json["status_code"] == 200) { + // OTP VERIFIED → NAVIGATE + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 100)); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => UnloadingInProgressScreen(details: widget.details)), + (route) => false, + ); + } else { + + _shakeController.forward(from: 0); // 🔥 SHAKES OTP BOXES + Future.delayed(const Duration(milliseconds: 300), () { + _c1.clear(); + _c2.clear(); + _c3.clear(); + _c4.clear(); + + _f1.requestFocus(); // Move focus back to first box + _checkOtpFilled(); // Disable button again + }); + AppSettings.longFailedToast(json["msg"] ?? "Invalid OTP"); + return; + } + } else { + AppSettings.longFailedToast("Something went wrong"); + } + } : null, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.resolveWith((_) => Colors.black), + foregroundColor: + MaterialStateProperty.resolveWith((_) => Colors.white), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28)), + ), + ), + child: Text('Continue', + style: fontTextStyle( + 16, const Color(0xFFFFFFFF), FontWeight.w400)), + ), + ), + ], + ): + Text(widget.details.tank_name!=''?"Unload Water in":'The user hasn’t selected a tank. Please ask the user to select one.', + textAlign: TextAlign.center, + style: fontTextStyle( + 16, const Color(0xFF939495), FontWeight.w500)), + ), + ), + ], + ), + )); + } +} + +class _OtpBox extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + final ValueChanged onChanged; + final InputDecoration decoration; + + const _OtpBox({ + required this.controller, + required this.focusNode, + required this.onChanged, + required this.decoration, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 54, + child: TextField( + controller: controller, + focusNode: focusNode, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: decoration, + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/orders/unloading_completed_otp.dart b/lib/orders/unloading_completed_otp.dart new file mode 100644 index 0000000..223e4d6 --- /dev/null +++ b/lib/orders/unloading_completed_otp.dart @@ -0,0 +1,328 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../common/settings.dart'; +import 'collect_money.dart'; + +TextStyle fts(double s, Color c, FontWeight w) => + GoogleFonts.inter(fontSize: s, color: c, fontWeight: w); + +class UnloadingCompleteScreen extends StatefulWidget { + var details; + + UnloadingCompleteScreen({ + this.details, + }); + + @override + State createState() => _UnloadingCompleteScreenState(); +} + +class _UnloadingCompleteScreenState extends State with TickerProviderStateMixin{ + final _c1 = TextEditingController(); + final _c2 = TextEditingController(); + final _c3 = TextEditingController(); + final _c4 = TextEditingController(); + + final _f1 = FocusNode(); + final _f2 = FocusNode(); + final _f3 = FocusNode(); + final _f4 = FocusNode(); + + bool get _otpReady => + _c1.text.isNotEmpty && _c2.text.isNotEmpty && _c3.text.isNotEmpty && _c4.text.isNotEmpty; + + late AnimationController _shakeController; + late Animation _offsetAnimation; + bool _isOtpComplete = false; + + void _checkOtpFilled() { + setState(() { + _isOtpComplete = + _c1.text.isNotEmpty && + _c2.text.isNotEmpty && + _c3.text.isNotEmpty && + _c4.text.isNotEmpty; + }); + } + + @override + void initState() { + super.initState(); + + _shakeController = AnimationController( + duration: const Duration(milliseconds: 450), + vsync: this, + ); + + _offsetAnimation = Tween(begin: 0.0, end: 15.0) + .chain(CurveTween(curve: Curves.elasticIn)) + .animate(_shakeController); + } + + @override + void dispose() { + _c1.dispose(); + _c2.dispose(); + _c3.dispose(); + _c4.dispose(); + _f1.dispose(); + _f2.dispose(); + _f3.dispose(); + _f4.dispose(); + _shakeController.dispose(); + super.dispose(); + } + + void _onDigit({ + required String value, + required FocusNode current, + FocusNode? next, + FocusNode? prev, + }) { + if (value.length == 1 && next != null) { + next.requestFocus(); + } else if (value.isEmpty && prev != null) { + prev.requestFocus(); + } + setState(() {}); + } + + InputDecoration _otpDecoration(bool focused) => InputDecoration( + counterText: '', + contentPadding: const EdgeInsets.symmetric(vertical: 14), + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Color(0xFFE0E0E0)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.black, width: 1.2), + ), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppSettings.appBarWithNotificationIcon(widget.details.building_name, widget.details.type_of_water, widget.details.capacity, context), + body: SafeArea( + child: Align( + alignment: const Alignment(0, -0.25), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + width: double.infinity, + constraints: const BoxConstraints(maxWidth: 520), + padding: const EdgeInsets.fromLTRB(16, 18, 16, 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))], + border: Border.all(color: const Color(0xFFEDEDED)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Green check + Container( + width: 28, + height: 28, + decoration: const BoxDecoration(color: Color(0xFF2FAE22), shape: BoxShape.circle), + child: const Icon(Icons.check, color: Colors.white, size: 18), + ), + const SizedBox(height: 12), + + Text('Unloading Complete', style: fts(20, const Color(0xFF101214), FontWeight.w700)), + const SizedBox(height: 12), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Enter OTP to finish delivery', style: fts(12, const Color(0xFF7E7F80), FontWeight.w500)), + const SizedBox(width: 6), + Icon(Icons.help_outline, size: 16, color: Colors.grey.shade600), + ], + ), + const SizedBox(height: 14), + + // OTP row + AnimatedBuilder( + animation: _shakeController, + builder: (context, child) { + return Transform.translate( + offset: Offset(_offsetAnimation.value, 0), + child: child, + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _OtpBox( + controller: _c1, + focusNode: _f1, + onChanged: (v){ + _onDigit(value: v, current: _f1, next: _f2); + _checkOtpFilled(); + }, + //onChanged: (v) => _onDigit(value: v, current: _f1, next: _f2), + decoration: _otpDecoration(_f1.hasFocus), + ), + _OtpBox( + controller: _c2, + focusNode: _f2, + onChanged: (v){ + _onDigit(value: v, current: _f2, next: _f3, prev: _f1); + _checkOtpFilled(); + }, + decoration: _otpDecoration(_f2.hasFocus), + ), + _OtpBox( + controller: _c3, + focusNode: _f3, + onChanged: (v){ + _onDigit(value: v, current: _f3, next: _f4, prev: _f2); + _checkOtpFilled(); + }, + decoration: _otpDecoration(_f3.hasFocus), + ), + _OtpBox( + controller: _c4, + focusNode: _f4, + onChanged: (v){ + _onDigit(value: v, current: _f4, prev: _f3); + _checkOtpFilled(); + }, + decoration: _otpDecoration(_f4.hasFocus), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Continue button -> go to CollectMoneyScreen + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _isOtpComplete ? () async { + if (!_otpReady) return; + + AppSettings.preLoaderDialog(context); + + bool isOnline = await AppSettings.internetConnectivity(); + if (!isOnline) { + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast("Please Check Internet"); + return; + } + + // Combine OTP + final otp = "${_c1.text}${_c2.text}${_c3.text}${_c4.text}"; + + var payload = { + "action": "stop", + "percentage": "100", + "otp": otp, + }; + + // 🔥 Call API + var response = await AppSettings.verifyUnloadStartOTP( + widget.details.bookingid, + payload + ); + + Navigator.of(context, rootNavigator: true).pop(); // CLOSE LOADER + + if (response.isNotEmpty) { + // Decode JSON + var json = jsonDecode(response); + + if (json["status_code"] == 200) { + // OTP VERIFIED → NAVIGATE + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 100)); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => CollectMoney(details:widget.details)), + ); + } else { + // OTP WRONG + /*FocusScope.of(context).unfocus(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UnloadingInProgressScreen(details: widget.details), + ), + );*/ + _shakeController.forward(from: 0); // 🔥 SHAKES OTP BOXES + Future.delayed(const Duration(milliseconds: 300), () { + _c1.clear(); + _c2.clear(); + _c3.clear(); + _c4.clear(); + + _f1.requestFocus(); // Move focus back to first box + _checkOtpFilled(); // Disable button again + }); + AppSettings.longFailedToast(json["msg"] ?? "Invalid OTP"); + return; + } + } else { + AppSettings.longFailedToast("Something went wrong"); + } + } : null, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((_) => Colors.black), // always black + foregroundColor: MaterialStateProperty.resolveWith((_) => Colors.white), + shape: MaterialStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + ), + child: Text('Continue', style: fts(14, Colors.white, FontWeight.w600)), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _OtpBox extends StatelessWidget { + final TextEditingController controller; + final FocusNode focusNode; + final ValueChanged onChanged; + final InputDecoration decoration; + + const _OtpBox({ + required this.controller, + required this.focusNode, + required this.onChanged, + required this.decoration, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 54, + child: TextField( + controller: controller, + focusNode: focusNode, + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: decoration, + onChanged: onChanged, + ), + ); + } +} diff --git a/lib/orders/unloading_inprogress.dart b/lib/orders/unloading_inprogress.dart new file mode 100644 index 0000000..5b186ce --- /dev/null +++ b/lib/orders/unloading_inprogress.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:supplier_new/orders/unloading_completed_otp.dart'; + +import '../common/settings.dart'; +import 'all_orders.dart'; + +TextStyle fts(double s, Color c, FontWeight w) => + GoogleFonts.inter(fontSize: s, color: c, fontWeight: w); + +class UnloadingInProgressScreen extends StatefulWidget { + final Duration? startFrom; + var details; + + UnloadingInProgressScreen({ + this.details, + this.startFrom + }); + /// If you want to start from a preset time (e.g., 12 minutes), pass Duration(minutes: 12) + + + @override + State createState() => _UnloadingInProgressScreenState(); +} + +class _UnloadingInProgressScreenState extends State { + late Duration _elapsed; + Timer? _timer; + + @override + void initState() { + super.initState(); + _elapsed = widget.startFrom ?? Duration.zero; + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() => _elapsed += const Duration(seconds: 1)); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + String _fmt(Duration d) { + final h = d.inHours.toString().padLeft(2, '0'); + final m = (d.inMinutes % 60).toString().padLeft(2, '0'); + final s = (d.inSeconds % 60).toString().padLeft(2, '0'); + return '$h:$m:$s'; + } + + void _onComplete() { + _timer?.cancel(); + // Optionally show a toast/snackbar, then navigate + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text('Unloading marked complete at ${_fmt(_elapsed)}')), + // ); + + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => UnloadingCompleteScreen(details: widget.details,)), + ); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => AllOrders(navigationFrom:"")), + (route) => false, + ); + return false; + }, + + child:Scaffold( + backgroundColor: Colors.white, + appBar: AppSettings.appBarWithNotificationIcon(widget.details.building_name, widget.details.type_of_water, widget.details.capacity, context), + + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Unloading in-progress', + style: fontTextStyle(24, const Color(0xFF000000), FontWeight.w500)), + const SizedBox(height: 12), + Text( + _fmt(_elapsed), + style: fontTextStyle(40, const Color(0xFF000000), FontWeight.w700), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _onComplete, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: Text('Unloading Complete', + style: fontTextStyle(14, const Color(0xFFFFFFFF), FontWeight.w400)), + ), + ), + ], + ), + ), + ), + ), + )); + } +} diff --git a/pubspec.lock b/pubspec.lock index a684d14..bca0ec7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -896,6 +896,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" sanitize_html: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c85bf74..9072a4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: photo_view: ^0.15.0 url_launcher: ^6.1.9 sms_autofill: ^2.4.0 + qr_flutter: ^4.1.0 dev_dependencies: flutter_test: