diff --git a/lib/supplier/book_a_tanker.dart b/lib/supplier/book_a_tanker.dart new file mode 100644 index 0000000..d7eb91b --- /dev/null +++ b/lib/supplier/book_a_tanker.dart @@ -0,0 +1,1641 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:watermanagement/supplier/filter_screen.dart'; +import 'package:watermanagement/supplier/supplier_details.dart'; +import 'package:watermanagement/supplier/supplier_details_from_search.dart'; +import 'package:watermanagement/supplier/suppliers_model.dart'; + +class BookATanker extends StatefulWidget { + const BookATanker({super.key}); + + @override + State createState() => _BookATankerState(); +} + +class _BookATankerState extends State { + String? selectedWaterType; + String? selectedCapacity; + String? selectedQuantity; + String? selectedTime; + DateTime? selectedDate; + List SuppliersList = []; + bool isDataLoading = false; + bool isSearchEnabled = false; + int sentRequests = 0; + int maxRequests = 5; + DateTime? firstRequestTime; + Timer? _resetTimer; + Timer? _countdownTimer; + String remainingTime = ''; + double progress = 1.0; + int totalCooldown = 900; // 15 minutes in seconds + + RangeValues? selectedRadius; + RangeValues? selectedRating; + RangeValues? selectedPrice; + String? selectedPump; + bool filterAll = true, filterConnected = false, filterNotConnected = false; + final TextEditingController _quotedAmountController = TextEditingController(); + + + // TimeOfDay _selectedTime = (TimeOfDay.now()); + + final List waterTypes = ['Drinking Water', 'Bore water']; + final List capacities = [ + '1,000 L', + '2,000 L', + '3,000 L', + '5,000 L', + '10,000 L', + '20,000 L' + ]; + final List quantities = ['1', '2', '3']; + final List timeSlots = ['8 AM', '12 PM', '4 PM', '8 PM']; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _selectedTime = _roundToNextHour(TimeOfDay.now()); + _loadRequestState(); + // Defer the bottom sheet call until after build + WidgetsBinding.instance.addPostFrameCallback((_) { + _showLocationBottomSheet(); + }); + } + + @override + void dispose() { + _countdownTimer?.cancel(); + _quotedAmountController.dispose(); + super.dispose(); + } + + Future _loadRequestState() async { + List validTimes = await _getValidRequestTimes(); + validTimes.sort(); // Ensure chronological order + setState(() { + sentRequests = validTimes.length; + }); + + if (validTimes.isNotEmpty) { + final firstExpiry = validTimes.first.add(Duration(minutes: 15)); + final remaining = firstExpiry.difference(DateTime.now()).inSeconds; + if (remaining > 0) { + _startCountdownTimer(remaining); + } + } + } + + Future> _getValidRequestTimes() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + final rawList = prefs.getStringList('requestTimes') ?? []; + final now = DateTime.now(); + final validTimes = rawList + .map((s) => DateTime.tryParse(s)) + .whereType() + .where((t) => now.difference(t).inMinutes < 15) + .toList(); + return validTimes; + } + + Future _saveRequestTimes(List times) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setStringList('requestTimes', times.map((e) => e.toIso8601String()).toList()); + } + + void _startCountdownTimer(int totalSeconds) { + _countdownTimer?.cancel(); + totalCooldown = totalSeconds; + progress = 1.0; + + _countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) async { + final elapsed = timer.tick; + final remaining = totalSeconds - elapsed; + + if (remaining <= 0) { + timer.cancel(); + + // 🧹 Clean expired request times + List updatedTimes = await _getValidRequestTimes(); + updatedTimes.sort(); // Ensure order + await _saveRequestTimes(updatedTimes); + + setState(() { + sentRequests = updatedTimes.length; + remainingTime = ''; + progress = 0.0; + }); + + // πŸ• If more valid requests remain, restart countdown with new first one + if (updatedTimes.isNotEmpty) { + final nextExpiry = updatedTimes.first.add(Duration(minutes: 15)); + final newRemaining = nextExpiry.difference(DateTime.now()).inSeconds; + if (newRemaining > 0) { + _startCountdownTimer(newRemaining); + } + } + + } else { + final minutes = (remaining ~/ 60).toString().padLeft(2, '0'); + final seconds = (remaining % 60).toString().padLeft(2, '0'); + setState(() { + remainingTime = "$minutes:$seconds"; + progress = remaining / totalCooldown; + }); + } + }); + } + + + + + void _showLocationBottomSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, // πŸ”’ Prevents dragging + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Container( + padding: EdgeInsets.all(16), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.45, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Confirm Address?", + style: fontTextStyle(16, Color(0XFF343637), FontWeight.w600), + ), + SizedBox(height: MediaQuery.of(context).size.height * .024), + Row( + children: [ + Text( + "Your current selected address is", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w600), + ), + SizedBox(width: MediaQuery.of(context).size.width * .004), + Text( + "HOME", + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w600), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + children: [ + Image.asset( + 'images/marker-pin.png', + width: 24, + height: 24, + ), + SizedBox(width: MediaQuery.of(context).size.width * .024), + Expanded( + child: Text( + AppSettings.userAddress, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ) + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + // Optional: handle "Change" + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(width: 1, color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Change', + style: fontTextStyle(14, Color(0XFF939495), FontWeight.w600), + ), + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .010), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + /* setState(() { + _isConfirmed = true; + });*/ + }, + child: Container( + decoration: BoxDecoration( + color: Color(0XFF1D7AFC), + border: Border.all(width: 1, color: Colors.white), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Confirm', + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + + + + TimeOfDay _roundToNextHour(TimeOfDay time) { + int nextHour = (time.minute > 0) ? (time.hour + 1) % 24 : time.hour; + return TimeOfDay(hour: nextHour, minute: 0); + } + + void sendRequest() { + if (sentRequests < maxRequests) { + setState(() { + sentRequests++; + }); + } + } + + Future _selectFromDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: selectedDate ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(Duration(days: 3)), + helpText: 'Select Date', + initialEntryMode: DatePickerEntryMode.calendarOnly, + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + inputDecorationTheme: InputDecorationTheme( + border: InputBorder.none, // Removes the text field border + ), + colorScheme: ColorScheme.light( + primary: Color(0XFF1D7AFC), + onPrimary: Color(0XFFFFFFFF), // Header text color + surface: Color(0XFFFFFFFF), + onSurface: Colors.black, + secondary: Colors.pink, + ), + dividerColor: Color(0XFFF5F6F6), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Color(0XFF1D7AFC), + ), // Text color + //backgroundColor: MaterialStateProperty.all(Colors.grey[200]), // Background + textStyle: MaterialStateProperty.all( + fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + ), // Font style + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8))), // Rounded corners + ), + // Background of the dialog box + ), + textTheme: TextTheme( + bodyLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + labelLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + titleLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + headlineLarge: + fontTextStyle(20, Color(0XFF1D7AFC), FontWeight.w600), + ), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + selectedDate = picked; + + //toDate1 = null; // Reset To Date + //toDateController1.clear(); + }); + } + } + + TimeOfDay _selectedTime = TimeOfDay.now(); + String _timeRangeText = ''; + + + Future _selectTime(BuildContext context) async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime, + cancelText: 'Cancel', + confirmText: 'Confirm', + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + primaryColor: Color(0XFF1D7AFC), + timePickerTheme: TimePickerThemeData( + backgroundColor: Colors.white, + dialBackgroundColor: Colors.white, + hourMinuteTextColor: Color(0XFF1D7AFC), + dayPeriodTextColor: Color(0XFF1D7AFC), + dialTextColor: Color(0XFF1D7AFC), + dayPeriodColor: Color(0XFFC3C4C4), + hourMinuteShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Color(0XFFFFFFFF), width: 2), + ), + dayPeriodShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + dialHandColor: Color(0XFFC3C4C4), + hourMinuteColor: Color(0XFFFFFFFF), + dialTextStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + dayPeriodTextStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + hourMinuteTextStyle: + TextStyle(fontSize: 50, fontWeight: FontWeight.w600), + helpTextStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + final now = TimeOfDay.now(); + final today = DateTime.now(); + final isToday = selectedDate?.day == today.day && + selectedDate?.month == today.month && + selectedDate?.year == today.year; + + bool isValid = true; + + if (isToday) { + final nowMinutes = now.hour * 60 + now.minute; + final pickedMinutes = picked.hour * 60 + picked.minute; + isValid = pickedMinutes >= nowMinutes; + } + + if (isValid) { + setState(() { + _selectedTime = picked; + _timeRangeText = _formatTimeRange(_selectedTime); + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Please select a time after the current time.")), + ); + } + } + } + + String _formatTimeRange(TimeOfDay start) { + final endHour = (start.hour + 2) % 24; + final end = TimeOfDay(hour: endHour, minute: start.minute); + return '${_formatTime(start)} to ${_formatTime(end)}'; + } + + String _formatTime(TimeOfDay time) { + final now = DateTime.now(); + final dt = DateTime(now.year, now.month, now.day, time.hour, time.minute); + return TimeOfDay.fromDateTime(dt).format(context); + } + + Widget _beforeDataScreen() { + return Padding(padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "What’s new?", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + RichText( + text: TextSpan( + text: "Find ", + style: fontTextStyle(20, Color(0XFF343637), FontWeight.w600), + children: [ + TextSpan( + text: "exclusive offers", + style: fontTextStyle(20, Color(0XFF8270DB), FontWeight.w600), + ), + TextSpan( + text: " & best deals available for you.", + style: fontTextStyle(20, Color(0XFF343637), FontWeight.w600), + ), + ], + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .024), + Container( + height: 80, + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + Color(0xFFFFFFFF), + Color(0xFFE2DCFF) + ], // Light gradient background + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + border: Border.all(color: Color(0xFF8270DB)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AQUINTO', + style: + fontTextStyle(20, Color(0XFF8270DB), FontWeight.w800), + ), + SizedBox(height: MediaQuery.of(context).size.height * .004), + Text( + 'Your Water, Your Data, Your Control.', + style: + fontTextStyle(12, Color(0XFF8270DB), FontWeight.w500), + ), + ], + ), + /*Image.asset( + 'images/WaterTankImage.png', // Replace with your actual image asset path + height: 40, + width: 40, + fit: BoxFit.contain, + ),*/ + ], + ), + ) + ], + ),); + } + + Widget _beforeDataScreenAbove() { + return Column( + children: [ + SizedBox(height: MediaQuery.of(context).size.height * .024), + CircleAvatar(radius: 80, backgroundColor: Colors.grey.shade300), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Center( + child: Text("Book Tanker", + style: fontTextStyle(20, Color(0XFF000000), FontWeight.w600)), + ), + SizedBox(height: MediaQuery.of(context).size.height * .008), + Text( + "Book a tanker instantly or schedule it\nanytime in the next 3 days", + textAlign: TextAlign.center, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ], + ); + } + + Widget _suppliersDataScreen() { + if (isDataLoading) { + return Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ); + } + + if (SuppliersList.isEmpty) { + return Center( + child: Text( + 'No Data Available', + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + ); + } + + return Container( + color: Color(0xFFF1F1F1), // Set your desired background color + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 0, vertical: 12), + decoration: BoxDecoration( + color: Color(0xFFF1F1F1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Row( + children: [ + Image.asset( + 'images/info.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + SizedBox( + width: MediaQuery.of(context).size.width * .008, + ), + Text( + "Order requests sent", + style: fontTextStyle(14, Color(0XFF2D2E30), FontWeight.w600), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Text("$sentRequests/$maxRequests",style: fontTextStyle(14, Color(0XFF515253), FontWeight.w600),), + Spacer(), + Text("$remainingTime",style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400),), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .016, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(maxRequests, (index) { + return Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 4), + height: 3, + decoration: BoxDecoration( + color: index < sentRequests + ?Color(0XFF114690) + :Color(0XFF939495), + borderRadius: BorderRadius.circular(2), + ), + ), + ); + }), + ), + Padding( + padding: EdgeInsets.only(top: 12, right: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: (){}, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF515253)), + borderRadius: BorderRadius.circular(45), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'images/sort.png', + width: 12, + height: 12, + color: Color(0XFF515253), + fit: BoxFit.contain, + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Text( + "Sort", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF515253), + fit: BoxFit.contain, + ), + ], + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + GestureDetector( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => FilterScreen( + initialFilters: { + 'all': filterAll, + 'connected': filterConnected, + 'notConnected': filterNotConnected, + 'radiusRange': selectedRadius, + 'ratingRange': selectedRating, + 'priceRange': selectedPrice, + 'pumpChoice': selectedPump, + }, + ), + ), + ); + + if (result != null && result is Map) { + setState(() { + filterAll = result['all']; + filterConnected = result['connected']; + filterNotConnected = result['notConnected']; + selectedRadius = result['radiusRange']; + selectedRating = result['ratingRange']; + selectedPrice = result['priceRange']; + selectedPump = result['pumpChoice']; + }); + + await getAllSuppliers(); + } + }, + + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF515253)), + borderRadius: BorderRadius.circular(45), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'images/filter.png', + width: 12, + height: 12, + color: Color(0XFF515253), + fit: BoxFit.contain, + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Text( + "Filter", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ], + ), + ), + ) + ], + ), + ) + + ], + )), + ], + ), + ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: SuppliersList.length, + separatorBuilder: (context, index) => SizedBox( + height: MediaQuery.of(context).size.height * .008, + ), + itemBuilder: (BuildContext context, int index) { + //final supplier = SuppliersList[index]; + return GestureDetector( + onTap: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SupplierScreenFromSearch(details: SuppliersList[index],)), + ); + }, + child:Card( + color: Colors.white, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(12), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 25, + backgroundColor: Color(0XFFE8F2FF), + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: 50, + height: 50, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .024, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Name and Rating Row + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + SuppliersList[index].supplier_name, + style: fontTextStyle(16, + Color(0XFF2D2E30), FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ), + Row( + children: [ + Image.asset( + 'images/star.png', + width: 16, + height: 16, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * + .008, + ), + Text( + '4.2', + style: fontTextStyle(10, + Color(0XFF515253), FontWeight.w400), + ), + ], + ), + ], + ), + SizedBox( + height: + MediaQuery.of(context).size.height * .004, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Drinking | Bore Water', + style: fontTextStyle( + 12, Color(0XFF4692FD), FontWeight.w500), + ), + + Text( + SuppliersList[index].matchedPrice.toString()!="null"&&SuppliersList[index].matchedPrice.toString()!="" + ""? '\u20B9 ${AppSettings.formDouble(SuppliersList[index].matchedPrice.toString())}': '', + style: fontTextStyle(10, Color(0XFF515253), FontWeight.w400), + ), + ], + ), + SizedBox( + height: + MediaQuery.of(context).size.height * .004, + ), + + /// Address and Favourite + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '${SuppliersList[index].displayAddress} ${SuppliersList[index].distanceInMeters} Km', + style: fontTextStyle(12, + Color(0XFF515253), FontWeight.w400), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () async { + AppSettings.preLoaderDialog(context); + if (SuppliersList[index].isFavorite) { + try { + bool tankerResponse = + await AppSettings + .removeFavourites( + SuppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longSuccessToast( + 'Supplier removed from favourites'); + await getAllSuppliers(); + // await getConnectedSuppliersData(); + } else { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to remove from favourites'); + } + } catch (e) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to remove from favourites'); + } + } else { + try { + bool tankerResponse = + await AppSettings.addFavourites( + SuppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longSuccessToast( + 'Supplier added to favourites'); + await getAllSuppliers(); + //await getConnectedSuppliersData(); + //await getAllFavouritesData(); + } else { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to add favourites'); + } + } catch (e) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to add favourites'); + } + } + }, + child: Image.asset( + SuppliersList[index].isFavorite + ? 'images/heart_active.png' + : 'images/heart_outline.png', + width: 20, + height: 20, + ), + ), + ], + ), + ], + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .016, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, + color: SuppliersList[index].isRequetsedBooking?Color(0XFF1D7AFC):Color(0XFF939495), + ), + borderRadius: BorderRadius.circular( + 24, + )), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Call', + style: fontTextStyle(12, SuppliersList[index].isRequetsedBooking?Color(0XFF1D7AFC):Color(0XFF939495), + FontWeight.w600)), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .010, + ), + Expanded( + child: GestureDetector( + onTap: () async{ + if(!SuppliersList[index].isRequetsedBooking){ + requestOrderDialog(SuppliersList[index]); + } + + /* Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlaceOrder(details: connectedSuppliersList[index],)), + );*/ + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: SuppliersList[index].isRequetsedBooking?Color(0XFFDBDBDC):Color(0XFF1D7AFC), + border: Border.all( + width: 1, + color: Color(0XFFFFFFFF), + ), + borderRadius: BorderRadius.circular( + 24, + )), + alignment: Alignment.center,// + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Request Order', + style: fontTextStyle( + 12, Color(0XFFFFFFFF), FontWeight.w600)), + ), + ), + )) + ], + ) + ], + ), + ), + ) , + ) + + ; + }, + + ) + ], + ), + ); + + + } + + requestOrderDialog(var details) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: Color(0XFFFFFFFF),// Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center(child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text('ORDER DETAILS',style:fontTextStyle(16,Color(0XFF646566),FontWeight.w400)), + ), + SizedBox(height: MediaQuery.of(context).size.height * .024,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Water Type',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(selectedWaterType!,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Capacity',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(selectedCapacity!,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Quantity',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(selectedQuantity!,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Date',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(DateFormat('dd-MMM-yyyy').format(selectedDate!),style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Time',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(_timeRangeText,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Actual Price',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text( + details.matchedPrice.toString()!="null"&&details.matchedPrice.toString()!="" + ""? '\u20B9 ${AppSettings.formDouble(details.matchedPrice.toString())}': '', + style: fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Your Price',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4, // Adjust width as needed + child: TextFormField( + controller: _quotedAmountController, + keyboardType: TextInputType.number, + textCapitalization: TextCapitalization.sentences, + maxLength: 10, + decoration: textFormFieldDecoration(Icons.phone,''), + style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500), + cursorColor: Color(0XFF1D7AFC), + + //TextStyle(color: Colors.black,fontWeight: FontWeight.bold), + ) + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Text('A standard 10% advance is payable before order confirmation. The advance payment is negotiable.',style:italicFontTextStyle(12,Color(0XFF515253),FontWeight.w400)), + ], + ),), + actions: [ + Center( + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Color(0XFF1D7AFC), width: 1), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Cancel', + style: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + ), + ), + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .010,), + Expanded( + child: GestureDetector( + onTap: () async { + final now = DateTime.now(); + List validTimes = await _getValidRequestTimes(); + + if (validTimes.length >= maxRequests) { + AppSettings.longFailedToast( + "Maximum 5 requests allowed in any 15-minute window."); + return; + } + + final text = _quotedAmountController.text.trim(); + if (text.isEmpty) return; + + final quoted = int.tryParse(text); + if (quoted == null) { + AppSettings.longFailedToast("Invalid number"); + return; + } + + // βœ… SAFE matched price parsing + final matchedPriceStr = details.matchedPrice ?? ''; + final matched = int.tryParse(matchedPriceStr); + if (matched == null) { + AppSettings.longFailedToast("Invalid matched price"); + return; + } + + if (quoted > matched + 300 || quoted < matched - 300) { + AppSettings.longFailedToast( + "Enter Β±300 within matched price $matched"); + return; + } + + // βœ… SAFE required values + final supplierId = details.supplier_id ?? ''; + if (supplierId.isEmpty) { + AppSettings.longFailedToast("Supplier not available"); + return; + } + + if (selectedDate == null) { + AppSettings.longFailedToast("Please select date"); + return; + } + + AppSettings.preLoaderDialog(context); + bool isOnline = await AppSettings.internetConnectivity(); + + if (!isOnline) { + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast("Check your internet connection."); + return; + } + + // βœ… NULL-SAFE PAYLOAD + var payload = { + "customerId": AppSettings.customerId ?? '', + "type_of_water": selectedWaterType ?? '', + "capacity": selectedCapacity ?? '', + "quantity": selectedQuantity ?? '', + "date": DateFormat('dd-MMM-yyyy').format(selectedDate!), + "time": _timeRangeText ?? '', + "requested_suppliers": [ + { + "supplierId": supplierId, + "quoted_amount": quoted, + "time": DateFormat('dd-MM-yyyy HH:mm').format(now), + } + ], + }; + + debugPrint("PAYLOAD β†’ $payload"); + + bool status = await AppSettings.requestOrder(payload); + Navigator.of(context, rootNavigator: true).pop(); + + if (status) { + _quotedAmountController.clear(); + Navigator.pop(context); + + validTimes.add(now); + validTimes.sort(); + await _saveRequestTimes(validTimes); + sentRequests = validTimes.length; + + if (validTimes.isNotEmpty) { + final firstExpiry = + validTimes.first.add(const Duration(minutes: 15)); + final remaining = + firstExpiry.difference(DateTime.now()).inSeconds; + if (remaining > 0) _startCountdownTimer(remaining); + } + + setState(() {}); + await getAllSuppliers(); + AppSettings.longSuccessToast("Request Sent"); + } else { + AppSettings.longFailedToast("Request sending failed"); + } + }, + child: Container( + decoration: BoxDecoration( + color: const Color(0XFF1D7AFC), + border: Border.all(color: const Color(0XFF1D7AFC), width: 1), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Send Request', + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + ), + ), + ), + ) + ], + ), + ), + ], + ); + }); + }); + } + + + Future getAllSuppliers() async { + AppSettings.preLoaderDialog(context); + + bool isOnline = await AppSettings.internetConnectivity(); + + if (isOnline) { + var payload = new Map(); + payload["type_of_water"] = selectedWaterType.toString(); + payload["capacity"] = selectedCapacity.toString(); + payload["quantity"] = selectedQuantity.toString(); + payload["date"] = DateFormat('dd-MMM-yyyy').format(selectedDate!); + payload["time"] = _timeRangeText.toString(); + + payload["radius_from"] = selectedRadius?.start.round().toString() ?? ""; + payload["radius_to"] = selectedRadius?.end.round().toString() ?? ""; + + payload["rating_to"] = selectedRating?.end.toString() ?? ""; + + payload["price_from"] = selectedPrice?.start.round().toString() ?? ""; + payload["price_to"] = selectedPrice?.end.round().toString() ?? ""; + payload["pump"] = selectedPump.toString(); + setState(() { + isDataLoading = true; + }); + try { + var response = await AppSettings.getSuppliersForBooking(payload); + + setState(() { + SuppliersList = + ((jsonDecode(response)['suppliers']) as List) + .map((dynamic model) { + return SuppliersModel.fromJson(model); + }).toList(); + // Clean the selected value: remove comma and " L" + String normalizedSelectedLitres = selectedCapacity!.replaceAll(",", "").replaceAll(" L", "").trim(); + + List suppliersJson = jsonDecode(response)['suppliers']; + + SuppliersList = suppliersJson.map((supplierData) { + List tankers = supplierData['tankers']; + String? matchedPrice; + + for (var tanker in tankers) { + String capacity = tanker['capacity'].replaceAll(",", "").trim(); + if (capacity == normalizedSelectedLitres) { + matchedPrice = tanker['price']; + break; + } + } + + // Optional: attach matchedPrice to model if supported + var supplierModel = SuppliersModel.fromJson(supplierData); + supplierModel.matchedPrice = matchedPrice; // <- Add this field to your model class + return supplierModel; + }).toList(); + isDataLoading = false; + Navigator.of(context, rootNavigator: true).pop(); + }); + } catch (e) { + setState(() { + isDataLoading = false; + Navigator.of(context, rootNavigator: true).pop(); + }); + } + } else { + AppSettings.longFailedToast('Please check internet connection'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Color(0XFFFFFFFF), + elevation: 0, + scrolledUnderElevation: 0, + titleSpacing: 0, + title: GestureDetector( + onTap: () { + _showLocationBottomSheet(); + }, + child:Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisSize: + MainAxisSize.min, // Ensures it doesn't take extra space + children: [ + Text( + 'Home', + style: + fontTextStyle(16, Color(0XFF232527), FontWeight.w600), + ), + SizedBox(width: 5), // Space between text and icon + Image.asset( + 'images/arrow_down.png', // Replace with your arrow image + width: 24, // Adjust size + height: 24, + ) + ], + ), + Text( + AppSettings.userAddress, + style: + fontTextStyle(10, Color(0XFF515253), FontWeight.w400), + ), + ], + ) + ), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', // Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + 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, // Adjust the fit + ), + ), + ), + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 0), + children: [ + Padding(padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + !isSearchEnabled ? _beforeDataScreenAbove() : Container(), + SizedBox(height: MediaQuery.of(context).size.height * .024), + _buildDropdownField( + hint: "Type of Water", + value: selectedWaterType, + items: waterTypes, + onChanged: (val) => setState(() => selectedWaterType = val), + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + children: [ + Expanded( + child: _buildDropdownField( + hint: "Capacity", + value: selectedCapacity, + items: capacities, + onChanged: (val) => + setState(() => selectedCapacity = val))), + SizedBox(width: MediaQuery.of(context).size.width * .024), + Expanded( + child: _buildDropdownField( + hint: "Quantity", + value: selectedQuantity, + items: quantities, + onChanged: (val) => + setState(() => selectedQuantity = val))), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + children: [ + Expanded( + child: _buildDatePickerField( + hint: "Date", + value: selectedDate != null + ? DateFormat('dd-MMM-yyyy').format(selectedDate!) + : null, + onTap: () => _selectFromDate(context))), + SizedBox(width: MediaQuery.of(context).size.width * .024), + Expanded( + child: _buildTimePickerField( + hint: "Select Time", + value: _timeRangeText != '' ? _timeRangeText : null, + onTap: () => selectedDate != null + ? _selectTime(context) + : ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Please select date first"))), + )), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .024), + SizedBox( + width: double.infinity, + height: 41, + child: ElevatedButton( + onPressed: () async { + setState(() { + isSearchEnabled = true; + }); + if (selectedWaterType != '' && + selectedCapacity != '' && + selectedDate != '' && + _timeRangeText != '' && + selectedQuantity != '') { + await getAllSuppliers(); + } else { + AppSettings.longFailedToast('Please enter valid details'); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF1D7AFC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: Text( + "Search", + style: fontTextStyle(14, Color(0xFFFFFFFF), FontWeight.w600), + ), + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .036), + ], + ), + ), + !isSearchEnabled ? _beforeDataScreen() : _suppliersDataScreen(), + ], + ), + ); + } + + Widget _buildDropdownField({ + required String hint, + required String? value, + required List items, + required ValueChanged onChanged, + }) { + return DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + hint: Padding( + padding: EdgeInsets.symmetric(vertical: 0), + child: Text( + hint, + style: fontTextStyle(14, Color(0XFF939495), FontWeight.w400), + ), + ), + value: value, + items: items.map((item) { + return DropdownMenuItem( + value: item, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Text( + item, + style: fontTextStyle(14, Color(0xFF2D2E30), FontWeight.w400), + ), + ), + ); + }).toList(), + onChanged: onChanged, + buttonStyleData: ButtonStyleData( + height: 48, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(12), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: EdgeInsets.only(right: 4), // Right spacing from edge + child: Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF939495), + ), + ), + ), + dropdownStyleData: DropdownStyleData( + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + menuItemStyleData: MenuItemStyleData( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + ), + ), + ); + } + + Widget _buildDatePickerField({ + required String hint, + String? value, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 48, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value ?? hint, + style: value != null + ? fontTextStyle(14, Color(0xFF2D2E30), FontWeight.w400) + : fontTextStyle(14, Color(0XFF939495), FontWeight.w400), + ), + Image.asset( + 'images/calender_supplier_landing.png', + fit: BoxFit.cover, + width: 16, // Match the diameter of the CircleAvatar + height: 16, + color: Color(0XFF939495), + ), + ], + ), + ), + ); + } + + Widget _buildTimePickerField({ + required String hint, + String? value, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 48, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + value ?? hint, + style: value != null + ? fontTextStyle(14, Color(0xFF2D2E30), FontWeight.w400) + : fontTextStyle(14, Color(0XFF939495), FontWeight.w400), + overflow: TextOverflow.visible, + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF939495), + ), + ], + ), + ), + ); + } +} diff --git a/lib/supplier/cancel_order.dart b/lib/supplier/cancel_order.dart new file mode 100644 index 0000000..beeaf0c --- /dev/null +++ b/lib/supplier/cancel_order.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; + +class CancelOrderPage extends StatefulWidget { + var order; + var status; + CancelOrderPage({this.order, this.status}); + + @override + State createState() => _CancelOrderPageState(); +} + +class _CancelOrderPageState extends State { + String? selectedReason; + final TextEditingController reasonController = TextEditingController(); + + final List reasons = [ + "Changed my mind", + "Found another price", + "Delivery got delayed", + "Do not need it anymore", + "Supplier asked me to cancel", + "Other", + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + scrolledUnderElevation: 0, + title: Text( + 'Cancel Order', + style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w500), + ), + 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, + ), + ), + ), + ), + body: Padding( + padding:EdgeInsets.all(24.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Why are you cancelling?", + style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox(height:MediaQuery.of(context).size.height * .008,), + + // Radio buttons + ...reasons.map((reason) { + return RadioListTile( + value: reason, + groupValue: selectedReason, + activeColor: primaryColor, + contentPadding: EdgeInsets.zero, + title: Text( + reason, + style: + fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w400), + ), + onChanged: (value) { + setState(() { + selectedReason = value; + }); + }, + ); + }).toList(), + + if (selectedReason == "Other") + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextFormField( + controller: reasonController, + maxLines: 5, + style: fontTextStyle(14, Color(0XFF101214), FontWeight.w400), + cursorColor: Color(0XFF1D7AFC), + textCapitalization: TextCapitalization.sentences, + decoration: textFormFieldDecorationHintText( + Icons.phone, + 'Describe your reason..', + ), + ), + ), + + SizedBox(height:MediaQuery.of(context).size.height * .008,), + Text( + "Cancellation Policy", + style:fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox(height:MediaQuery.of(context).size.height * .016,), + Text( + 'Cancel anytime before delivery starts. If it’s already on the way, a β‚Ή100 cancellation fee may apply. Please review our full terms and conditions for more details.', + style:fontTextStyle(12, Color(0XFF2A2A2A), FontWeight.w400), + ), + SizedBox(height:MediaQuery.of(context).size.height * .016,), + GestureDetector( + onTap: () { + // open terms page + }, + child: Text( + "View Terms & Conditions", + style: fontTextStylewithUnderline(10, Color(0XFF4692FD), FontWeight.w400,Color(0XFF4692FD)), + ), + ), + + ], + ), + ), + ), + bottomNavigationBar: Padding( + padding: EdgeInsets.all(24.0), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + if (selectedReason == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Please select a reason")), + ); + return; + } + + String finalReason = + selectedReason == "Other" + ? reasonController.text.trim() + : selectedReason!; + + if (finalReason.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Please enter cancellation reason")), + ); + return; + } + + bool success = await AppSettings.cancelTankerBooking( + widget.order.dbId, + "cancel", + finalReason, + ); + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Order cancelled successfully")), + ); + Navigator.pop(context, true); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Failed to cancel order")), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0XFFE76960), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: Text( + "CANCEL ORDER", + style: fontTextStyle(14, Color(0XFFFFFFFF), FontWeight.w600), + ), + ), + ), + ), + ); + } +} diff --git a/lib/supplier/cart_summary.dart b/lib/supplier/cart_summary.dart new file mode 100644 index 0000000..3b27efb --- /dev/null +++ b/lib/supplier/cart_summary.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/models/supplier_tankers_model.dart'; + +class CartSummary extends StatefulWidget { + var details; + var supplierDetails; + CartSummary({this.details,this.supplierDetails}); + + @override + State createState() => _CartSummaryState(); +} + +class _CartSummaryState extends State { + + + + + @override + void initState() { + super.initState(); + + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: AppSettings.appBarWithoutActions(widget.supplierDetails.supplier_name), + body: Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + children: [Container( + width: double.infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(8), + top:Radius.circular(8), + ), // match Card border + gradient: LinearGradient( + colors: [ + Color(0xFFFFF8DF), + Color(0xFFFFFFFF), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.supplierDetails.supplier_name, + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600)), + Text(widget.supplierDetails.distanceInMeters + .toString() + + ' Km', + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600)), + ], + ), + )),], + ) + ), + + ); + } +} diff --git a/lib/supplier/chat_bot.dart b/lib/supplier/chat_bot.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/supplier/favourites_model.dart b/lib/supplier/favourites_model.dart new file mode 100644 index 0000000..11d6d07 --- /dev/null +++ b/lib/supplier/favourites_model.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'dart:typed_data'; +class favouritesModel { + String supplier_name = ''; + String status=''; + String supplier_address=''; + String supplier_phone_number=''; + String supplier_alternate_phone_number=''; + String supplier_id=''; + String about_supplier=''; + String startingprice_supplier=''; + String supplier_image=''; + String distance=''; + String distanceInKm=''; + String bs64str=''; + String picture=''; + Map picture1={}; + Color text_color=Colors.black; + double lat=0; + double lng=0; + var data; + double distanceInMeters=0; + String displayAddress=''; + bool isFavorite=false; + //File? updatedImage; + + favouritesModel(); + + factory favouritesModel.fromJson(Map json){ + favouritesModel rtvm = new favouritesModel(); + + rtvm.supplier_name = json['suppliername'] ?? ''; + rtvm.status = json['status'] ?? ''; + rtvm.supplier_address = json['profile']['office_address'] ?? ''; + rtvm.supplier_phone_number = json['phone'] ?? ''; + rtvm.supplier_alternate_phone_number = json['alternativeContactNumber'] ?? ''; + rtvm.supplier_id = json['supplierId'] ?? ''; + rtvm.lat = json['latitude'] ?? 0; + rtvm.lng = json['longitude'] ??0; + rtvm.about_supplier = json['description'] ?? ''; + rtvm.startingprice_supplier = json['startingPrice'] ?? ''; + rtvm.picture = json['picture'] ?? ''; + rtvm.isFavorite = json['favorate'] ?? false; + rtvm.distanceInMeters = double.parse((Geolocator.distanceBetween( + rtvm.lat, + rtvm.lng, + AppSettings.userLatitude, + AppSettings.userLongitude + ) / 1000).toStringAsFixed(2)); + + List parts = rtvm.supplier_address.split(','); + rtvm.displayAddress = parts[2].trim(); + + + + if(rtvm.status.toString().toLowerCase()=='pending'){ + rtvm.text_color=Colors.yellow; + } + else if(rtvm.status.toString().toLowerCase()=='accepted'){ + rtvm.text_color=Colors.green; + } + else if(rtvm.status.toString().toLowerCase()=='rejected'){ + rtvm.text_color=Colors.red; + } + else{ + rtvm.status='Connect?'; + rtvm.text_color=primaryColor; + } + + + + + return rtvm; + } + +} \ No newline at end of file diff --git a/lib/supplier/filter_screen.dart b/lib/supplier/filter_screen.dart new file mode 100644 index 0000000..aa17f5f --- /dev/null +++ b/lib/supplier/filter_screen.dart @@ -0,0 +1,296 @@ +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; + +class FilterScreen extends StatefulWidget { + final Map? initialFilters; + + FilterScreen({this.initialFilters}); + @override + _FilterScreenState createState() => _FilterScreenState(); +} + +class _FilterScreenState extends State { + /*bool all = true, connected = false, notConnected = false; + RangeValues radiusRange = RangeValues(4, 15); + RangeValues ratingRange = RangeValues(3.5, 5); + RangeValues priceRange = RangeValues(2500, 5000); + String? pumpChoice;*/ + bool all = true, connected = false, notConnected = false; + RangeValues? radiusRange; + RangeValues? ratingRange; + RangeValues? priceRange; + String? pumpChoice; + + @override + void initState() { + super.initState(); + final filters = widget.initialFilters ?? {}; + all = filters['all'] ?? true; + connected = filters['connected'] ?? false; + notConnected = filters['notConnected'] ?? false; + radiusRange = filters['radiusRange'] ?? RangeValues(4, 15); + ratingRange = filters['ratingRange'] ?? RangeValues(3.5, 5); + priceRange = filters['priceRange'] ?? RangeValues(2500, 5000); + pumpChoice = filters['pumpChoice']; + } + + void _applyFilters() { + Navigator.pop(context, { + 'all': all, + 'connected': connected, + 'notConnected': notConnected, + 'radiusRange': radiusRange, + 'ratingRange': ratingRange, + 'priceRange': priceRange, + 'pumpChoice': pumpChoice, + }); + } + + void _resetFilters() { + setState(() { + all = true; + connected = false; + notConnected = false; + radiusRange = RangeValues(4, 15); + ratingRange = RangeValues(3.5, 5); + priceRange = RangeValues(2500, 5000); + pumpChoice = null; + }); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + /*appBar: AppBar( + title: Text("Filters", style: TextStyle(color: Colors.black)), + backgroundColor: Colors.white, + elevation: 0, + automaticallyImplyLeading: false, + actions: [ + IconButton( + icon: Icon(Icons.close, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ], + ),*/ + body: SafeArea( + child:Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + SizedBox(height: MediaQuery.of(context).size.height * .048), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Filters", style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600),), + GestureDetector( + onTap: (){ + Navigator.pop(context); + }, + child: Image.asset( + 'images/cross.png', // Replace with your arrow image + width: 24, // Adjust size + height: 24, + ), + ) + ], + ), + Expanded( + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 0), + children: [ + SizedBox(height: MediaQuery.of(context).size.height * .012), + Text("TYPE OF SUPPLIERS", style: fontTextStyle(10, Color(0XFF646566), FontWeight.w400),), + CheckboxListTile( + value: all, + onChanged: (v) => setState(() => all = v!), + contentPadding: EdgeInsets.zero, + dense: true, + activeColor:Color(0XFF000000) , + visualDensity: VisualDensity.compact, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + "All", + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w400), + ), + ), + CheckboxListTile( + value: connected, + onChanged: (v) => setState(() => connected = v!), + contentPadding: EdgeInsets.zero, + dense: true, + activeColor:Color(0XFF000000) , + visualDensity: VisualDensity.compact, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + "Connected", + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w400), + ), + ), + CheckboxListTile( + value: notConnected, + onChanged: (v) => setState(() => notConnected = v!), + contentPadding: EdgeInsets.zero, + dense: true, + activeColor:Color(0XFF000000) , + visualDensity: VisualDensity.compact, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + "Not Connected", + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w400), + ), + ), + Divider(height: 32,color: Color(0XFFC3C4C4) ,), + _buildRangeSlider("SEARCH RADIUS", "km", 0, 25, radiusRange ?? RangeValues(0, 25)), + Divider(height: 32,color: Color(0XFFC3C4C4) ,), + Text("PUMP", style: fontTextStyle(10, Color(0XFF646566), FontWeight.w400),), + RadioListTile( + value: "Yes", + groupValue: pumpChoice, + onChanged: (v) => setState(() => pumpChoice = v), + activeColor:Color(0XFF000000) , + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Yes", style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w400)), + ], + ), + ), + RadioListTile( + value: "No", + groupValue: pumpChoice, + onChanged: (v) => setState(() => pumpChoice = v), + contentPadding: EdgeInsets.zero, + activeColor:Color(0XFF000000) , + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("No", style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w400)), + ], + ), + ), + + Divider(height: 32,color: Color(0XFFC3C4C4) ,), + _buildRangeSlider("RATING", "", 0, 5, ratingRange?? RangeValues(0, 5)), + Divider(height: 32,color: Color(0XFFC3C4C4) ,), + _buildRangeSlider("PRICE", "", 2500, 5000, priceRange?? RangeValues(0, 5000)), + SizedBox(height: MediaQuery.of(context).size.height * .012), + ], + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0,0,0,16), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _resetFilters, + style: OutlinedButton.styleFrom( + side: BorderSide(color:Color(0XFF2A2A2A)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text("Reset", style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600),), + ), + ), + SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: _applyFilters, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF2A2A2A), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text("Apply", style: fontTextStyle(14, Color(0XFFFFFFFF), FontWeight.w600),), + ), + ), + ], + ), + ), + ], + ), + ) + ) + ); + } + + + Widget _buildRangeSlider(String title, String suffix, double min, double max, RangeValues values) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: fontTextStyle(10, Color(0XFF646566), FontWeight.w400), + ), + SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 1.5, + activeTrackColor: Color(0XFF000000), + inactiveTrackColor: Color(0XFFC3C4C4), + thumbColor: Colors.black, + rangeThumbShape: RoundRangeSliderThumbShape(enabledThumbRadius: 7), + ), + child: RangeSlider( + values: values, + min: min, + max: max, + labels: RangeLabels( + "${values.start.round()}$suffix", + "${values.end.round()}$suffix", + ), + onChanged: (val) { + setState(() { + if (title == "SEARCH RADIUS") { + radiusRange = val; + } else if (title == "RATING") { + ratingRange = val; + } else if (title == "PRICE") { + priceRange = val; + } + }); + }, + ), + ), + Row( + children: [ + Text("From", style: fontTextStyle(10, Color(0XFF939495), FontWeight.w400)), + SizedBox(width: MediaQuery.of(context).size.width * .016), + Container( + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFFC3C4C4)), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + "${values.start.toStringAsFixed(suffix.isNotEmpty ? 0 : 1)}$suffix", + style: fontTextStyle(10, Color(0XFF2D2E30), FontWeight.w400), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .016), + Text("To", style: fontTextStyle(10, Color(0XFF939495), FontWeight.w400)), + SizedBox(width: MediaQuery.of(context).size.width * .016), + Container( + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFFC3C4C4)), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + "${values.end.toStringAsFixed(suffix.isNotEmpty ? 0 : 1)}$suffix", + style: fontTextStyle(10, Color(0XFF2D2E30), FontWeight.w400), + ), + ), + ], + ), + ], + ); + } + + +} diff --git a/lib/supplier/landing.dart b/lib/supplier/landing.dart new file mode 100644 index 0000000..81fd3da --- /dev/null +++ b/lib/supplier/landing.dart @@ -0,0 +1,3042 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/models/connected_suppliers_model.dart'; +import 'package:watermanagement/supplier/book_a_tanker.dart'; +import 'package:watermanagement/supplier/my_orders.dart'; +import 'package:watermanagement/supplier/my_suppliers.dart'; +import 'package:watermanagement/supplier/order_details.dart'; +import 'package:watermanagement/supplier/order_requests.dart'; +import 'package:watermanagement/supplier/payment_example.dart'; +import 'package:watermanagement/supplier/paymnets/payments_main.dart'; +import 'package:watermanagement/supplier/plans/plan_details.dart'; +import 'package:watermanagement/supplier/plans/plan_request_screen.dart'; +import 'package:watermanagement/supplier/plans/plan_requests.dart'; +import 'package:watermanagement/supplier/profile.dart'; +import 'package:watermanagement/supplier/supplier_calendar.dart'; +import 'package:watermanagement/supplier/supplier_orders_model.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:watermanagement/supplier/todaysprice/todays_price.dart'; + +import '../common/calander.dart'; +import 'order_arrived.dart'; +import 'order_status_overlay.dart'; +import 'order_tracking_page.dart'; +import 'my_orders_model.dart'; +import 'paln_requests_model.dart'; + +class SupplierLanding extends StatefulWidget { + const SupplierLanding({super.key}); + + @override + State createState() => _SupplierLandingState(); +} + +class _SupplierLandingState extends State { + final GlobalKey<_SupplierHomeState> _supplierHomeKey = + GlobalKey<_SupplierHomeState>(); + int _currentIndex = 0; + final ImagePicker _picker = ImagePicker(); + final storage = FlutterSecureStorage( + aOptions: AndroidOptions( + resetOnError: true, + encryptedSharedPreferences: true, + ), + ); + final GlobalKey _scaffoldKey = GlobalKey(); + TextEditingController searchController1 = new TextEditingController(); + List cart = []; + final ValueNotifier> activeOrdersNotifier = + ValueNotifier>([]); + + // List of bottom navigation bar items + final List _bottomNavItems = [ + BottomNavigationBarItem( + icon: Padding( + padding: + EdgeInsets.fromLTRB(0, 5, 0, 0), // Adjust height to control spacing + child: ImageIcon(AssetImage('images/homeBottomIcon.png')), + ), + label: "Home", + ), + BottomNavigationBarItem( + icon: Padding( + padding: + EdgeInsets.fromLTRB(0, 5, 0, 0), // Adjust height to control spacing + child: ImageIcon(AssetImage('images/ordersBottomIcon.png')), + ), + // Replace with your image path + + label: "Orders", + ), + BottomNavigationBarItem( + icon: Padding( + padding: + EdgeInsets.fromLTRB(0, 5, 0, 0), // Adjust height to control spacing + child: ImageIcon(AssetImage('images/calenderBottomIcon.png')), + ), + // Replace with your image path + + label: "Calendar", + ), + BottomNavigationBarItem( + icon: Padding( + padding: + EdgeInsets.fromLTRB(0, 5, 0, 0), // Adjust height to control spacing + child: ImageIcon(AssetImage('images/suppliersBottomIcon.png')), + ), // Replace with your image path + + label: "Suppliers", + ), + BottomNavigationBarItem( + icon: Padding( + padding: + EdgeInsets.fromLTRB(0, 5, 0, 0), // Adjust height to control spacing + child: ImageIcon(AssetImage('images/paymentsBottomIcon.png')), + ), + + // Replace with your image path + + label: "Payments", + ), + ]; + + void _onItemTapped(int index) { + setState(() { + _currentIndex = index; + }); + } + + Widget _buildBody() { + switch (_currentIndex) { + case 0: + return SupplierHome( + key: _supplierHomeKey, + activeDeliveryNotifier: activeOrdersNotifier, + ); + case 1: + return const MyOrders(navigationFrom: 'bottom_bar'); + case 2: + return SupplyPlanCalendarScreen(); + case 3: + return MySuppliers(navigationFrom: 'bottom_bar'); + case 4: + return const PaymentsMainScreen(); + default: + return SupplierHome(activeDeliveryNotifier: activeOrdersNotifier); + } + } + + _showLocationBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, // Makes it go full screen if needed + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return DraggableScrollableSheet( + expand: false, + builder: (context, scrollController) { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: + MainAxisSize.min, // Ensures it doesn't take full height + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// πŸ”Ή Drag Handle (Line at the top) + Align( + alignment: Alignment.center, + child: Container( + width: 53, + height: 2, + decoration: BoxDecoration( + color: Color(0XFF757575), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Text( + "Location", + style: + fontTextStyle(14, Color(0XFF2D2E30), FontWeight.w600), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Container( + child: TextFormField( + controller: searchController1, + decoration: searchTextFormFieldDecoration( + 'images/search.png', 'Search'), + style: + fontTextStyle(14, Color(0XFF101214), FontWeight.w400), + cursorColor: Color(0XFF1D7AFC), + + //TextStyle(color: Colors.black,fontWeight: FontWeight.bold), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + GestureDetector( + onTap: () {}, + child: Row( + children: [ + Image.asset( + 'images/add_icon.png', + width: 24, // Adjust size + height: 24, + ), + SizedBox( + width: MediaQuery.of(context).size.width * .024, + ), + Text( + "Add New Address", + style: fontTextStyle( + 12, Color(0XFF2D2E30), FontWeight.w600), + ), + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Divider( + color: Colors.grey.shade300, + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + GestureDetector( + onTap: () {}, + child: Row( + children: [ + Image.asset( + 'images/gps_current_location.png', + width: 24, // Adjust size + height: 24, + ), + SizedBox( + width: MediaQuery.of(context).size.width * .024, + ), + Text( + "Use Current Location", + style: fontTextStyle( + 12, Color(0XFF2D2E30), FontWeight.w600), + ), + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + ], + ), + ); + }, + ); + }, + ); + } + + double _calculateDistance(lat1, lon1, lat2, lon2) { + var p = 0.017453292519943295; + var a = 0.5 - + cos((lat2 - lat1) * p) / 2 + + cos(lat1 * p) * cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2; + return 12742 * asin(sqrt(a)); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + if (_currentIndex == 0) { + Navigator.pop(context, true); + return true; + } else { + setState(() { + _currentIndex = 0; + }); + return false; + } + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: (_currentIndex == 2 || + _currentIndex == 3 || + _currentIndex == 1) // πŸ‘ˆ index of Profile page + ? null + : AppBar( + backgroundColor: Color(0XFFFFFFFF), + elevation: 0, + scrolledUnderElevation: 0, + titleSpacing: 0, + title: GestureDetector( + onTap: () { + _showLocationBottomSheet(context); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize + .min, // Ensures it doesn't take extra space + children: [ + Text( + 'Home', + style: fontTextStyle( + 16, Color(0XFF232527), FontWeight.w600), + ), + SizedBox(width: 5), // Space between text and icon + Image.asset( + 'images/arrow_down.png', // Replace with your arrow image + width: 24, // Adjust size + height: 24, + ) + ], + ), + Text( + AppSettings.userAddress, + style: fontTextStyle( + 10, Color(0XFF515253), FontWeight.w400), + ), + ], + )), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', // Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + leading: IconButton( + icon: Image.asset( + 'images/bookatanker_profile.png', // Replace with your image path + width: 24, // Adjust size as needed + height: 24, + ), // + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Profile()), + ); + }, + ), + ), + body: Stack( + children: [ + _buildBody(), + // πŸ” Listen to the notifier to show/hide overlay + ValueListenableBuilder>( + valueListenable: activeOrdersNotifier, + builder: (context, activeOrders, _) { + final isVisible = activeOrders.isNotEmpty; + return OrderStatusOverlayMulti( + isVisible: isVisible, + activeOrders: activeOrders, + onTap: (order) async { + // 0.05 km = 50 meters + const double arrivalRadiusKm = 0.05; + + double dist = _calculateDistance( + AppSettings.userLatitude, + AppSettings.userLongitude, + 17.41670599999999, + 78.4519569, + ); + + if (dist <= arrivalRadiusKm) { + // πŸ‘‡ Pass ONLY the tapped order + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ArrivalScreen(orderDetails: order), + ), + ); + + // If result indicates API reload + if (result == true) { + _supplierHomeKey.currentState?.getOrdersData(); + } + } else { + // πŸ‘‡ Pass ONLY the tapped order + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OrderTrackingPage( + lat: AppSettings.userLatitude, + lng: AppSettings.userLongitude, + d_lat: 17.41670599999999, + d_lng: 78.4519569, + u_address: order.displayAddress, + orderDetails: + order, // <---- ADD THIS (you must add this parameter in the page) + ), + ), + ); + } + }, + ); + }, + ), + ], + ), + /*_body(),,*/ + bottomNavigationBar: SizedBox( + //height: 123, // 56 (top message) + 56 (BottomNavigationBar) + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomNavigationBar( + backgroundColor: Color(0XFFFFFFFF), + items: _bottomNavItems, + selectedItemColor: + primaryColor, // Color of selected icon and text + unselectedItemColor: + Color(0XFF939495), // Color of unselected icon and text + selectedLabelStyle: fontTextStyleHeight( + 10, + primaryColor, + FontWeight.w400, + MediaQuery.of(context).size.height * .0032, + ), + unselectedLabelStyle: fontTextStyleHeight( + 10, + Color(0XFF939495), + FontWeight.w400, + MediaQuery.of(context).size.height * .0032, + ), + currentIndex: _currentIndex, + onTap: _onItemTapped, + type: BottomNavigationBarType + .fixed, // Ensures all 6 items are visible + ), + ], + ), + ), + + /*Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 0), + child: BottomNavigationBar( + backgroundColor: Color(0XFFFFFFFF), + items: _bottomNavItems, + selectedItemColor: primaryColor, // Color of selected icon and text + unselectedItemColor: + Color(0XFF939495), // Color of unselected icon and text + selectedLabelStyle: fontTextStyleHeight( + 10, + primaryColor, + FontWeight.w400, + MediaQuery.of(context).size.height * .0032, + ), + unselectedLabelStyle: fontTextStyleHeight( + 10, + Color(0XFF939495), + FontWeight.w400, + MediaQuery.of(context).size.height * .0032, + ), + currentIndex: _currentIndex, + onTap: _onItemTapped, + type: BottomNavigationBarType.fixed, // Ensures all 6 items are visible + ), + )*/ + )); + } +} + +class SupplierHome extends StatefulWidget { + const SupplierHome({super.key, required this.activeDeliveryNotifier}); + final ValueNotifier> activeDeliveryNotifier; + + @override + State createState() => _SupplierHomeState(); +} + +class _SupplierHomeState extends State { + bool isConnectedDataLoading = false; + bool isOrdersDataLoading = false; + bool isSupplierOrdersDataLoading = false; + bool isPlanRequestsDataLoading = false; + List connectedSuppliersList = []; + List ordersList = []; + List supplierOrdersList = []; + List planRequestsList = []; + List upcomingOrders = []; + double lat = 0; + double lng = 0; + final PageController _pageController = PageController(); + final PageController _pageController1 = PageController(); + final PageController _pageControllerForPlans = PageController(); + + Future getConnectedSuppliersData() async { + isConnectedDataLoading = true; + + try { + var response = await AppSettings.getConnetcedSuppliers(); + + setState(() { + connectedSuppliersList = + ((jsonDecode(response)['data']) as List).map((dynamic model) { + return ConnectedSuppliersModel.fromJson(model); + }).toList(); + connectedSuppliersList.forEach((element) async { + var distanceInM; + double distanceInMeters = await Geolocator.distanceBetween( + element.lat, element.lng, lat, lng); + }); + isConnectedDataLoading = false; + }); + } catch (e) { + setState(() { + isConnectedDataLoading = false; + }); + } + } + + Future getOrdersData() async { + setState(() => isOrdersDataLoading = true); + + try { + final response1 = await AppSettings.getAllOrders(); + final decoded = jsonDecode(response1); + + print("πŸ”Ή API RESPONSE (getOrdersData): $decoded"); + + if (decoded == null || decoded['data'] == null) { + print("⚠️ No 'data' found in response"); + setState(() { + ordersList = []; + upcomingOrders = []; + isOrdersDataLoading = false; + }); + return; + } + + final List data = decoded['data']; + if (data.isEmpty) { + print("⚠️ Empty data list"); + setState(() { + ordersList = []; + upcomingOrders = []; + isOrdersDataLoading = false; + }); + return; + } + + final parsedOrders = data.map((e) => MyOrdersModel.fromJson(e)).toList(); + + // πŸ”Ή Flexible filter for upcoming deliveries + final filtered = parsedOrders.where((order) { + final s = order.orderStatus.toLowerCase(); + return s != "pending" || + s == "advance_paid" || + s == "assigned" || + s == "deliveryboy_assigned"; + }).toList(); + + print( + "βœ… Total orders: ${parsedOrders.length}, Upcoming: ${filtered.length}"); + + setState(() { + ordersList = parsedOrders; + upcomingOrders = filtered; + isOrdersDataLoading = false; + }); + +// βœ… Update the global overlay flag (deliverboystarted anywhere?) + final excludedStatuses = { + 'deliveryboy_assigned', + 'completed', + 'cancelled', + 'advance_paid' + }; + + final activeOrders = parsedOrders.where((o) { + final s = (o.orderStatus ?? '').toLowerCase().replaceAll(' ', ''); + + return !excludedStatuses.contains(s); + }).toList(); + +// βœ… publish full list to the notifier + widget.activeDeliveryNotifier.value = activeOrders; + +// You can OR-in other active states if needed: +// || (o.orderStatus ?? '').toLowerCase() == 'deliveryboy_assigned' + } catch (e) { + print("❌ Error in getOrdersData(): $e"); + setState(() => isOrdersDataLoading = false); + } + } + + Future getAcceptedOrdersFromSupplier() async { + isSupplierOrdersDataLoading = true; + var response1 = await AppSettings.getAcceptedOrdersDataFromSupplier(); + var json = jsonDecode(response1); + + setState(() { + supplierOrdersList = (json['data'] as List) + .map((model) => SupplierOrdersModel.fromJson(model)) + .where((order) => order.status.toLowerCase() == 'accept') + .toList(); + isSupplierOrdersDataLoading = false; + }); + } + + Future getAcceptedPlansFromSupplier() async { + isPlanRequestsDataLoading = true; + var response1 = await AppSettings.getAcceptedPlansFromSupplier(); + var json = jsonDecode(response1); + + setState(() { + planRequestsList = (json['data'] as List) + .map((model) => PlanRequestModel.fromJson(model)) + .where((order) => order.status.toLowerCase() == 'accepted') + .toList(); + + isPlanRequestsDataLoading = false; + }); + } + + Map getOrderStatus(String orderTimeStr, String dbStatus) { + String status = "Invalid time"; + Color color = Colors.grey; + + final dbLower = (dbStatus ?? "").toLowerCase().trim(); + + // βœ… Statuses that ignore time + if (dbLower == "reject") + return {"status": "Rejected", "color": const Color(0XFFE2483D)}; + if (dbLower == "accept") + return {"status": "Accepted", "color": const Color(0XFF0A9E04)}; + if (dbLower == "delivered") + return {"status": "Delivered", "color": const Color(0XFF2E7D32)}; + if (dbLower == "cancelled" || + dbLower == "cancelled_by_user" || + dbLower == "cancelled_by_supplier") { + return {"status": "Cancelled", "color": const Color(0XFF757575)}; + } + + // βœ… Time-based status (only if not rejected/accepted/etc.) + if (orderTimeStr.isEmpty) { + // fallback for pending without time + return {"status": "Pending", "color": const Color(0XFFE56910)}; + } + + try { + final format = DateFormat("dd-MM-yyyy HH:mm"); + final orderTime = format.parse(orderTimeStr); + final now = DateTime.now(); + final difference = now.difference(orderTime); + + if (difference.inMinutes < 2) { + status = "New"; + color = const Color(0XFF1D7AFC); // Blue + } else if (difference.inMinutes < 30) { + final remaining = 30 - difference.inMinutes; + status = "Expires in ${remaining}m"; // show time for pending + color = difference.inMinutes < 10 + ? const Color(0XFFE56910) + : const Color(0XFFE2483D); + } else { + // Expired overrides pending + status = "Expired"; + color = const Color(0XFF757575); + } + } catch (e) { + debugPrint("⚠️ Error parsing time: $e"); + status = "Invalid time"; + color = Colors.grey; + } + + return {"status": status, "color": color}; + } + + String getOrderTimeOnly(String orderTimeStr) { + if (orderTimeStr.isEmpty) return ""; + + try { + final format = DateFormat("dd-MM-yyyy HH:mm"); + final orderTime = format.parse(orderTimeStr); + final now = DateTime.now(); + final difference = now.difference(orderTime); + + if (difference.inDays < 2) { + return "New"; + } else if (difference.inDays < 20) { + final remaining = 20 - difference.inDays; + return "Expires in ${remaining}d"; + } else { + return "Expired"; + } + } catch (e) { + return ""; + } + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + widget.activeDeliveryNotifier.value = []; + getConnectedSuppliersData(); + getOrdersData(); + getAcceptedOrdersFromSupplier(); + getAcceptedPlansFromSupplier(); + lat = AppSettings.userLatitude; + lng = AppSettings.userLongitude; + } + + showRejectOrdersDialog(var object) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: + Color(0XFFFFFFFF), // Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center( + child: Text( + 'Reject Order Request', + style: fontTextStyle(16, Color(0XFF3B3B3B), FontWeight.w600), + ), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text( + 'Do u want to reject order request', + style: + fontTextStyle(14, Color(0XFF101214), FontWeight.w600), + ), + ), + ], + ), + ), + actions: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFF1D7AFC)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Cancel', + style: fontTextStyle( + 12, Color(0XFF1D7AFC), FontWeight.w600)), + ), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + Expanded( + child: GestureDetector( + onTap: () async { + var payload = {}; + payload["supplierId"] = object.supplierId; + payload["action"] = 'reject'; + payload["paymentId"] = ''; + + try { + bool status = + await AppSettings.acceptOrderRequests( + payload, object.dbId); + + if (!mounted) return; + + if (status) { + AppSettings.longSuccessToast('Rejected'); + Navigator.pop(context); + getAcceptedOrdersFromSupplier(); + } else { + AppSettings.longFailedToast( + 'Failed to reject order request'); + } + } catch (e) { + AppSettings.longFailedToast('Error: $e'); + } + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFE2483D), + border: Border.all( + width: 1, color: Color(0XFFE2483D)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Reject', + style: fontTextStyle(12, Color(0XFFFFFFFF), + FontWeight.w600)), + ), + ), + )), + ) + ], + ), + ), + ], + ); + }); + }, + ); + } + + showAcceptOrdersDialog(var object) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: + Color(0XFFFFFFFF), // Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center( + child: Text( + 'Accept Order Request', + style: fontTextStyle(16, Color(0XFF3B3B3B), FontWeight.w600), + ), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text( + 'Do u want to Accept the order request?', + style: + fontTextStyle(14, Color(0XFF101214), FontWeight.w600), + ), + ), + ], + ), + ), + actions: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFFE2483D)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Cancel', + style: fontTextStyle( + 12, Color(0XFFE2483D), FontWeight.w600)), + ), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + Expanded( + child: GestureDetector( + onTap: () async { + Navigator.pop(context); + + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OrderDetails( + supplierDetails: object, + ), + ), + ); + + // If result indicates API reload + if (result == true) { + getAcceptedOrdersFromSupplier(); + } + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFF1D7AFC)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Accept', + style: fontTextStyle(12, Color(0XFF1D7AFC), + FontWeight.w600)), + ), + ), + )), + ) + ], + ), + ), + ], + ); + }); + }, + ); + } + + showAcceptPlanDialog(var object) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: + Color(0XFFFFFFFF), // Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center( + child: Text( + 'Accept Plan Request', + style: fontTextStyle(16, Color(0XFF3B3B3B), FontWeight.w600), + ), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text( + 'Do u want to Accept the plan request?', + style: + fontTextStyle(14, Color(0XFF101214), FontWeight.w600), + ), + ), + ], + ), + ), + actions: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFFE2483D)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Cancel', + style: fontTextStyle( + 12, Color(0XFFE2483D), FontWeight.w600)), + ), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + Expanded( + child: GestureDetector( + onTap: () async { + + + var payload = { + "action": "accept", + "ref_number": '', + "payment_type":object.paymentType + }; + + try { + bool status = await AppSettings.acceptPlanRequestsWithPayment(payload, object.dbId); + + if (!mounted) return; + + if (status) { + AppSettings.longSuccessToast('Accepted'); + Navigator.pop(context); + getAcceptedPlansFromSupplier(); + + + // Use Navigator safely + /* Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => OrderDetails(supplierDetails: object), + ), + );*/ + } else { + AppSettings.longFailedToast('Failed to accept order request'); + } + } catch (e) { + AppSettings.longFailedToast('Error: $e'); + } + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFF1D7AFC)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Accept', + style: fontTextStyle(12, Color(0XFF1D7AFC), + FontWeight.w600)), + ), + ), + )), + ) + ], + ), + ), + ], + ); + }); + }, + ); + } + + showRejectPlanDialog(var object) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: + Color(0XFFFFFFFF), // Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center( + child: Text( + 'Reject Plan Request', + style: fontTextStyle(16, Color(0XFF3B3B3B), FontWeight.w600), + ), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text( + 'Do u want to Reject the plan request?', + style: + fontTextStyle(14, Color(0XFF101214), FontWeight.w600), + ), + ), + ], + ), + ), + actions: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFF1D7AFC)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Cancel', + style: fontTextStyle( + 12, Color(0XFF1D7AFC), FontWeight.w600)), + ), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + Expanded( + child: GestureDetector( + onTap: () async { + + + var payload = { + "action": "reject", + "ref_number": '', + "payment_type":object.paymentType + }; + + try { + bool status = await AppSettings.acceptPlanRequestsWithPayment(payload, object.dbId); + + if (!mounted) return; + + if (status) { + AppSettings.longSuccessToast('Rejected'); + Navigator.pop(context); + getAcceptedPlansFromSupplier(); + + + // Use Navigator safely + /* Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => OrderDetails(supplierDetails: object), + ), + );*/ + } else { + AppSettings.longFailedToast('Failed to accept order request'); + } + } catch (e) { + AppSettings.longFailedToast('Error: $e'); + } + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, color: Color(0XFFE2483D)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Reject', + style: fontTextStyle(12, Color(0XFFE2483D), + FontWeight.w600)), + ), + ), + )), + ) + ], + ), + ), + ], + ); + }); + }, + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Stack(children: [ + Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * .008, + ), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => BookATanker()), + ); + }, + child: Card( + color: Color(0XFFFFFFFF), + elevation: 2, + child: Container( + padding: EdgeInsets.fromLTRB(5, 5, 0, 0), + height: 165, + //width: MediaQuery.of(context).size.height * .14, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5, 5, 0, 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Text( + 'Book a tanker', + style: fontTextStyle( + 14, + Color(0XFF2D2E30), + FontWeight.w700), + ), + Text( + 'Order now or schedule for later', + style: fontTextStyle( + 12, + Color(0XFF757575), + FontWeight.w400), + ), + ], + )), + /* Expanded(child: Align( + alignment: Alignment.bottomRight, + child: Image.asset('images/WaterTankImage.png'), + ))*/ + ], + )), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + /* Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SupplyCalendarScreen()), + );*/ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlanRequestScreen()), + ); + }, + child: Card( + color: Color(0XFFFFFFFF), + elevation: 2, + child: Container( + padding: EdgeInsets.fromLTRB(5, 5, 0, 0), + //height:MediaQuery.of(context).size.height * .20, + height: 165, + // width: MediaQuery.of(context).size.height * .14, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(5, 5, 0, 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Text( + 'Plans', + style: fontTextStyle( + 14, + Color(0XFF2D2E30), + FontWeight.w700), + ), + Text( + 'Subscribe for long-term water delivery', + style: fontTextStyle( + 12, + Color(0XFF757575), + FontWeight.w400), + ), + ], + )), + SizedBox( + height: + MediaQuery.of(context).size.height * .038, + ), + /*Expanded(child: Align( + alignment: Alignment.bottomRight, + child: Image.asset('images/ConsumptionImage.png',),))*/ /*height: 80,width: 80,*/ + ], + )), + ), + )), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + + Visibility( + visible: supplierOrdersList.length != 0, + child: Column( + children: [ + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OrderRequestsPage()), + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Order Requests', + style: fontTextStyle( + 14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .093, + ), + Row( + children: [ + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ) + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Center( + child: Container( + width: double.infinity, + color: Color(0XFFFFFFFF), + height: 150, + padding: EdgeInsets.all(0), + child: isSupplierOrdersDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : supplierOrdersList.length != 0 + ? Column( + children: [ + Expanded( + child: PageView.builder( + controller: _pageController1, + itemCount: + supplierOrdersList.length, + itemBuilder: (context, index) { + final order = + supplierOrdersList[index]; + + final orderDateTime = + "${order.acceptedTime}" + .trim(); + final statusTime = + getOrderTimeOnly( + orderDateTime); + + // status badge color + Color statusColor = + Color(0xFF1D7AFC); + if (statusTime == "Expired") { + statusColor = + Color(0xFF757575); + } else if (statusTime == + "New") { + statusColor = + Color(0XFF1D7AFC); + } else if (statusTime + .endsWith("m")) { + statusColor = + Color(0XFFE56910); + } + + return Container( + decoration: BoxDecoration( + color: Color( + 0XFFFFFFFF), // Same as Card color + borderRadius: + BorderRadius.circular( + 12), // Rounded corners + border: Border.all( + color: Color( + 0XFF1D7AFC), // Border color + width: + 0.5, // Border width + ), + ), + //color: Color(0XFFFFFFFF), + child: Padding( + padding: + EdgeInsets.all(12), + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Visibility( + visible: supplierOrdersList[ + index] + .supplierName != + '', + child: Text( + supplierOrdersList[ + index] + .supplierName, + style: fontTextStyle( + 14, + Color( + 0XFF343637), + FontWeight + .w600), + ), + ), + Visibility( + visible: statusTime + .isNotEmpty, + child: Padding( + padding: const EdgeInsets + .only( + top: 4.0, + bottom: + 4.0), + child: + Container( + padding: EdgeInsets.symmetric( + horizontal: + 6, + vertical: + 2), + decoration: + BoxDecoration( + color: + statusColor, + borderRadius: + BorderRadius.circular( + 4), + ), + child: Text( + statusTime, + style: fontTextStyle( + 10, + Colors + .white, + FontWeight + .w500), + ), + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Visibility( + visible: supplierOrdersList[ + index] + .displayAddress != + '', + child: Text( + supplierOrdersList[ + index] + .displayAddress, + style: fontTextStyle( + 10, + Color( + 0XFF646566), + FontWeight + .w400), + ), + ), + Visibility( + visible: supplierOrdersList[ + index] + .date != + '', + child: Text( + supplierOrdersList[ + index] + .date + + ' ' + + supplierOrdersList[ + index] + .time, + style: fontTextStyle( + 9, + Color( + 0XFF646566), + FontWeight + .w400), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of( + context) + .size + .height * + .012, + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .start, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: + 4, + vertical: + 2), + decoration: + BoxDecoration( + color: supplierOrdersList[index].typeofwater.toString().toLowerCase() == + 'bore water' + ? Color( + 0xFF8877DD) + : Color( + 0xFFCA86B0), + borderRadius: + BorderRadius.circular( + 4), + border: + Border + .all( + width: + 1, + color: supplierOrdersList[index].typeofwater.toString().toLowerCase() == + 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + ), + ), + child: + AutoSizeText( + capitalizeFirst( + supplierOrdersList[index] + .typeofwater), + maxLines: + 1, + overflow: + TextOverflow + .ellipsis, + style: fontTextStyle( + 12, + Color( + 0xFFFFFFFF), + FontWeight + .w400), + ), + ), + Row( + children: [ + Text( + 'β‚Ή' + + AppSettings.formDouble(supplierOrdersList[index].quotedAmount) + + '/', + style: fontTextStyle( + 20, + Color(0XFF515253), + FontWeight.w600), + ), + Text( + supplierOrdersList[index] + .capacity, + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w600), + ) + ], + ), + ], + ), + Row( + children: [ + GestureDetector( + onTap: statusTime == + "Expired" + ? null + : () { + showRejectOrdersDialog(supplierOrdersList[index]); + }, + child: + ColorFiltered( + colorFilter: statusTime == + "Expired" + ? ColorFilter.mode( + Color( + 0XFF9F9F9F), + BlendMode + .srcIn) + : ColorFilter.mode( + Colors.transparent, + BlendMode.multiply), + child: Image + .asset( + 'images/wrong_button.png', + width: + 44, + height: + 44, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .012, + ), + GestureDetector( + onTap: statusTime == + "Expired" + ? null + : () async { + // showAcceptOrdersDialog(supplierOrdersList[index]); + final result = + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OrderDetails( + supplierDetails: supplierOrdersList[index], + ), + ), + ); + ; + if (result == + true) { + getConnectedSuppliersData(); + getOrdersData(); + getAcceptedOrdersFromSupplier(); + getAcceptedPlansFromSupplier(); + } + }, + child: + ColorFiltered( + colorFilter: statusTime == + "Expired" + ? ColorFilter.mode( + Color( + 0XFF9F9F9F), + BlendMode + .srcIn) + : ColorFilter.mode( + Colors.transparent, + BlendMode.multiply), + child: Image + .asset( + 'images/right_button.png', + width: + 44, + height: + 44, + ), + ), + ), + ], + ) + ], + ) + ], + )), + ); + }, + ), + ), + SizedBox( + height: MediaQuery.of(context) + .size + .height * + .010, + ), + SmoothPageIndicator( + controller: _pageController1, + count: supplierOrdersList.length, + effect: ExpandingDotsEffect( + activeDotColor: Colors.blueAccent, + dotHeight: 3, + dotWidth: 3, + ), + ), + ], + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle( + 12, + Color(0XFF000000), + FontWeight.w500), + ), + ), + ), + ), + ], + )), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + + Visibility( + visible: planRequestsList.length != 0, + child: Column( + children: [ + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlanRequestsScreen()), + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Plan Requests', + style: fontTextStyle( + 14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox( + width: + MediaQuery.of(context).size.width * .093, + ), + Row( + children: [ + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ) + ], + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Center( + child: Container( + width: double.infinity, + color: Color(0XFFFFFFFF), + height: 230, + padding: EdgeInsets.all(0), + child: isPlanRequestsDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : planRequestsList.length != 0 + ? Column( + children: [ + Expanded( + child: PageView.builder( + controller: + _pageControllerForPlans, + itemCount: + planRequestsList.length, + itemBuilder: (context, index) { + final plans = + planRequestsList[index]; + + final orderDateTime = + "${plans.acceptedTime}" + .trim(); + final statusTime = + getOrderTimeOnly( + orderDateTime); + + // status badge color + Color statusColor = + Color(0xFF1D7AFC); + if (statusTime == "Expired") { + statusColor = + Color(0xFF757575); + } else if (statusTime == + "New") { + statusColor = + Color(0XFF1D7AFC); + } else if (statusTime + .endsWith("m")) { + statusColor = + Color(0XFFE56910); + } + + final int totalOrders = + planRequestsList[index] + .dates + .length; + final double biddingPrice = + double.tryParse( + planRequestsList[ + index] + .biddingPrice) ?? + 0; + + final double totalAmount = + totalOrders * biddingPrice; + return Container( + decoration: BoxDecoration( + color: Color( + 0XFFFFFFFF), // Same as Card color + borderRadius: + BorderRadius.circular( + 12), // Rounded corners + border: Border.all( + color: Color( + 0XFF1D7AFC), // Border color + width: + 0.5, // Border width + ), + ), + //color: Color(0XFFFFFFFF), + child: Padding( + padding: + EdgeInsets.all(12), + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Visibility( + visible: planRequestsList[ + index] + .supplierName != + '', + child: Text( + planRequestsList[ + index] + .supplierName, + style: fontTextStyle( + 14, + Color( + 0XFF343637), + FontWeight + .w600), + ), + ), + Visibility( + visible: statusTime + .isNotEmpty, + child: Padding( + padding: const EdgeInsets + .only( + top: 4.0, + bottom: + 4.0), + child: + Container( + padding: EdgeInsets.symmetric( + horizontal: + 6, + vertical: + 2), + decoration: + BoxDecoration( + color: + statusColor, + borderRadius: + BorderRadius.circular( + 4), + ), + child: Text( + statusTime, + style: fontTextStyle( + 10, + Colors + .white, + FontWeight + .w500), + ), + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Visibility( + visible: planRequestsList[ + index] + .displayAddress != + '', + child: Text( + planRequestsList[ + index] + .displayAddress, + style: fontTextStyle( + 10, + Color( + 0XFF646566), + FontWeight + .w400), + ), + ), + Visibility( + visible: planRequestsList[ + index] + .startDate != + '', + child: Text( + planRequestsList[ + index] + .startDate + + ' to ' + + planRequestsList[ + index] + .endDate, + style: fontTextStyle( + 9, + Color( + 0XFF646566), + FontWeight + .w400), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of( + context) + .size + .height * + .012, + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + mainAxisAlignment: + MainAxisAlignment + .start, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: + 4, + vertical: + 2), + decoration: + BoxDecoration( + color: planRequestsList[index].typeofwater.toString().toLowerCase() == + 'bore water' + ? Color( + 0xFF8877DD) + : Color( + 0xFFCA86B0), + borderRadius: + BorderRadius.circular( + 4), + border: + Border + .all( + width: + 1, + color: planRequestsList[index].typeofwater.toString().toLowerCase() == + 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + ), + ), + child: + AutoSizeText( + capitalizeFirst( + planRequestsList[index] + .typeofwater), + maxLines: + 1, + overflow: + TextOverflow + .ellipsis, + style: fontTextStyle( + 12, + Color( + 0xFFFFFFFF), + FontWeight + .w400), + ), + ), + Row( + children: [ + Text( + 'Actual Price: ', + style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400), + ), + Text( + 'β‚Ή' + + AppSettings.formDouble(planRequestsList[index].quotedAmount) + + '/', + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w600), + ), + Text( + planRequestsList[index] + .capacity, + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w600), + ) + ], + ), + Row( + children: [ + Text( + 'Bidding Price: ', + style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400), + ), + Text( + 'β‚Ή' + + AppSettings.formDouble(planRequestsList[index].biddingPrice) + + '/', + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w600), + ), + Text( + planRequestsList[index] + .capacity, + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w600), + ) + ], + ), + Row( + children: [ + Text( + 'Frequency: ', + style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400), + ), + Text( + planRequestsList[index].frequency + + '/week', + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w600), + ) + ], + ), + ], + ), + Row( + children: [ + GestureDetector( + onTap: statusTime == + "Expired" + ? null + : () { + showRejectPlanDialog(planRequestsList[index]); + }, + child: + ColorFiltered( + colorFilter: statusTime == + "Expired" + ? ColorFilter.mode( + Color( + 0XFF9F9F9F), + BlendMode + .srcIn) + : ColorFilter.mode( + Colors.transparent, + BlendMode.multiply), + child: Image + .asset( + 'images/wrong_button.png', + width: + 44, + height: + 44, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .012, + ), + GestureDetector( + onTap: statusTime == + "Expired" + ? null + : () async{ + // showAcceptOrdersDialog(supplierOrdersList[index]); + if (planRequestsList[index].paymentType == + 'advance') { + + final result = + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlanDetails( + supplierDetails: planRequestsList[index], + ), + ), + ); + if (result == + true) { + getConnectedSuppliersData(); + getOrdersData(); + getAcceptedOrdersFromSupplier(); + getAcceptedPlansFromSupplier(); + } + } else { + showAcceptPlanDialog(planRequestsList[index]); + } + }, + child: + ColorFiltered( + colorFilter: statusTime == + "Expired" + ? ColorFilter.mode( + Color( + 0XFF9F9F9F), + BlendMode + .srcIn) + : ColorFilter.mode( + Colors.transparent, + BlendMode.multiply), + child: Image + .asset( + 'images/right_button.png', + width: + 44, + height: + 44, + ), + ), + ), + ], + ) + ], + ), + SizedBox( + height: MediaQuery.of( + context) + .size + .height * + .010, + ), + Row( + children: [ + Text( + 'Payment Type: ', + style: fontTextStyle( + 12, + Color( + 0XFF646566), + FontWeight + .w400), + ), + Text( + planRequestsList[ + index] + .paymentType, + style: fontTextStyle( + 12, + Color( + 0XFF515253), + FontWeight + .w600), + ), + ], + ), + Visibility( + visible: planRequestsList[ + index] + .paymentType != + 'after_delivery', + child: Row( + children: [ + Text( + planRequestsList[index] + .paymentType == + 'credit' + ? 'Credit Limit: ' + : 'Advance Amount: ', + style: fontTextStyle( + 12, + Color( + 0XFF646566), + FontWeight + .w400), + ), + Text( + planRequestsList[ + index] + .advanceAmount, + style: fontTextStyle( + 12, + Color( + 0XFF515253), + FontWeight + .w600), + ), + ], + ), + ), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: + "Total amount for this plan orders ", + style: + fontTextStyle( + 12, + const Color( + 0xFF939495), + FontWeight + .w400, + ), + ), + TextSpan( + text: + '$totalOrders Γ— ${biddingPrice.toStringAsFixed(0)} = β‚Ή${totalAmount.toStringAsFixed(0)}', + style: + fontTextStyle( + 12, + const Color( + 0xFF515253), + FontWeight + .w500, + ), + ), + ], + ), + softWrap: true, + maxLines: 2, + overflow: + TextOverflow + .visible, + textAlign: TextAlign + .center, + ) + ], + )), + ); + }, + ), + ), + SizedBox( + height: MediaQuery.of(context) + .size + .height * + .010, + ), + SmoothPageIndicator( + controller: _pageControllerForPlans, + count: planRequestsList.length, + effect: ExpandingDotsEffect( + activeDotColor: Colors.blueAccent, + dotHeight: 3, + dotWidth: 3, + ), + ), + ], + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle( + 12, + Color(0XFF000000), + FontWeight.w500), + ), + ), + ), + ), + ], + )), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Today’s Price', + style: + fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .093, + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TodaysPriceMainScreen()), + ); + }, + child: Row( + children: [ + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ), + ) + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .006, + ), + Column( + children: [ + GestureDetector( + onTap: () { + /* Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SupplyCalendarScreen()), + );*/ + }, + child: Card( + color: Colors.white, + //width: MediaQuery.of(context).size.height * .14, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + 'DRINKING WATER', + style: fontTextStyle(12, + Color(0XFF646566), FontWeight.w500), + ), + SizedBox( + height: + MediaQuery.of(context).size.height * + .012, + ), + Container( + decoration: BoxDecoration( + color: Color(0xFFED8C85), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Color(0xFFED8C85), width: 1), + ), + padding: EdgeInsets.all(2), + child: Row( + mainAxisSize: MainAxisSize + .min, // Important for wrapping content + children: [ + Image.asset( + 'images/long_arrow_down.png', + fit: BoxFit.cover, + width: 8, + height: 8, + ), + SizedBox( + width: + 4), // spacing between image and text + Text( + '0.35%', + style: fontTextStyle( + 10, + Color(0xFFFFFFFF), + FontWeight.w400), + ), + ], + ), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + 'Avg', + style: fontTextStyle(10, + Color(0XFF757575), FontWeight.w400), + ), + Row( + children: [ + Text( + 'β‚Ή 1,432/', + style: fontTextStyle( + 20, + Color(0XFF2D2E30), + FontWeight.w600), + ), + Text( + '5,000L', + style: fontTextStyle( + 14, + Color(0XFF2D2E30), + FontWeight.w600), + ) + ], + ), + ], + ) + ], + )), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .024, + ), + GestureDetector( + onTap: () { + /* Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SupplyCalendarScreen()), + );*/ + }, + child: Card( + color: Colors.white, + //width: MediaQuery.of(context).size.height * .14, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + 'BORE WATER', + style: fontTextStyle(12, + Color(0XFF646566), FontWeight.w500), + ), + SizedBox( + height: + MediaQuery.of(context).size.height * + .012, + ), + Container( + decoration: BoxDecoration( + color: Color(0XFF36AF31), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Color(0XFF36AF31), width: 1), + ), + padding: EdgeInsets.all(2), + child: Row( + mainAxisSize: MainAxisSize + .min, // Important for wrapping content + children: [ + Image.asset( + 'images/long_arrow_up.png', + fit: BoxFit.cover, + width: 8, + height: 8, + ), + SizedBox( + width: + 4), // spacing between image and text + Text( + '0.23%', + style: fontTextStyle( + 10, + Color(0xFFFFFFFF), + FontWeight.w400), + ), + ], + ), + ) + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + 'Avg', + style: fontTextStyle(10, + Color(0XFF757575), FontWeight.w400), + ), + Row( + children: [ + Text( + 'β‚Ή 840/', + style: fontTextStyle( + 20, + Color(0XFF2D2E30), + FontWeight.w600), + ), + Text( + '5,000L', + style: fontTextStyle( + 14, + Color(0XFF2D2E30), + FontWeight.w600), + ) + ], + ), + ], + ) + ], + )), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'My Suppliers', + style: + fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .093, + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MySuppliers( + navigationFrom: 'dashboard', + )), + ); + }, + child: Row( + children: [ + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ), + ) + ], + ), + Container( + height: 100, + child: isConnectedDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : connectedSuppliersList.isNotEmpty + ? ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.all(8), + itemCount: connectedSuppliersList.length, + separatorBuilder: (context, index) => SizedBox( + width: MediaQuery.of(context).size.width * .03, + ), + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 35, + backgroundColor: Color(0XFFE8F2FF), + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: 70, + height: 70, + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .012), + Text( + connectedSuppliersList[index] + .supplier_name, + style: fontTextStyle(10, + Color(0XFF646566), FontWeight.w400), + ), + ], + ), + ); + }, + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle( + 12, Color(0XFF000000), FontWeight.w500), + ), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Upcoming Deliveries', + style: + fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w600), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .093, + ), + Row( + children: [ + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MyOrders( + navigationFrom: 'dashboard', + ), + ), + ); + }, + child: Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ) + ], + ) + ], + ), + Center( + child: Container( + width: double.infinity, + color: Color(0XFFFFFFFF), + height: 160, + padding: EdgeInsets.all(0), + child: isOrdersDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : upcomingOrders.length != 0 + ? Column( + children: [ + Expanded( + child: PageView.builder( + controller: _pageController, + itemCount: upcomingOrders.length, + itemBuilder: (context, index) { + return Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Visibility( + visible: upcomingOrders[ + index] + .supplierName != + '', + child: Text( + upcomingOrders[index] + .supplierName, + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600), + ), + ), + Align( + alignment: Alignment + .centerRight, + child: Container( + padding: EdgeInsets + .fromLTRB( + 4, 2, 2, 4), + decoration: + BoxDecoration( + color: Color( + 0XFFFFFFFF), // Same as Card color + borderRadius: + BorderRadius + .circular( + 4), // Rounded corners + border: + Border.all( + color: Color( + 0XFFFFFFFF), // Border color + width: + 1, // Border width + ), + ), + child: Row( + children: [ + Image.asset( + 'images/credit.png', + width: + 12, // Adjust as needed + height: + 12, // Adjust as needed + ), + SizedBox( + width: MediaQuery.of( + context) + .size + .width * + .008, + ), + Text( + 'Credit Account', + style: fontTextStyle( + 9, + Color( + 0XFF2D2E30), + FontWeight + .w500), + ), + ], + )), + ) + ], + ), + Visibility( + visible: + upcomingOrders[index] + .typeofwater != + '', + child: Text( + upcomingOrders[index] + .typeofwater, + style: fontTextStyle( + 12, + Color(0XFF4692FD), + FontWeight.w400), + ), + ), + Visibility( + visible: upcomingOrders[ + index] + .displayAddress != + '', + child: Text( + upcomingOrders[index] + .displayAddress, + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w400), + ), + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Visibility( + visible: true, + child: Text( + upcomingOrders[index] + .displayDateOfOrder, + style: fontTextStyle( + 10, + Color(0XFF939495), + FontWeight.w500), + ), + ), + Align( + alignment: Alignment + .centerRight, + child: Visibility( + visible: + upcomingOrders[ + index] + .price != + '', + child: Text( + 'β‚Ή ' + + upcomingOrders[ + index] + .price, + style: fontTextStyle( + 12, + Color( + 0XFF515253), + FontWeight + .w400), + ), + ), + ) + ], + ), + Divider( + color: Colors.grey.shade300, + ), + Align( + alignment: Alignment.center, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .center, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + GestureDetector( + child: Text( + 'TRACK ORDER', + style: fontTextStyle( + 12, + Color( + 0XFF1D7AFC), + FontWeight + .w600)), + onTap: () {}, + ), + SizedBox( + width: MediaQuery.of( + context) + .size + .width * + .15, + ), + GestureDetector( + child: Text('CANCEL', + style: fontTextStyle( + 12, + Color( + 0XFFE2483D), + FontWeight + .w600)), + onTap: () {}, + ), + ], + ), + ), + ], + )), + ); + }, + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * + .010, + ), + SmoothPageIndicator( + controller: _pageController, + count: upcomingOrders.length, + effect: ExpandingDotsEffect( + activeDotColor: Colors.blueAccent, + dotHeight: 3, + dotWidth: 3, + ), + ), + ], + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle( + 12, Color(0XFF000000), FontWeight.w500), + ), + ), + ), + ), + ], + )), + ]), + ); + } +} diff --git a/lib/supplier/my_orders.dart b/lib/supplier/my_orders.dart new file mode 100644 index 0000000..8501283 --- /dev/null +++ b/lib/supplier/my_orders.dart @@ -0,0 +1,661 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/supplier/my_orders_model.dart'; +import 'cancel_order.dart'; + +class MyOrders extends StatefulWidget { + final String? navigationFrom; + const MyOrders({this.navigationFrom, Key? key}) : super(key: key); + + @override + State createState() => _MyOrdersState(); +} + +class _MyOrdersState extends State with TickerProviderStateMixin { + bool isOrdersDataLoading = false; + List ordersList = []; + List upcomingOrders = []; + List previousOrders = []; + bool isPlansLoading = false; + List plansList = []; + List activePlans = []; + List pendingPlans = []; + late TabController _controller; + String tabMessage = "Welcome to Tab 1"; + + + @override + void initState() { + super.initState(); + _controller = TabController(vsync: this, length: 2); + // Listen for tab changes + _controller.addListener(() { + setState(() { + tabMessage = + _controller.index == 0 ? "Welcome to Tab 1" : "Welcome to Tab 2"; + }); + }); + getOrdersData(); + getPlansData(); + } + + Future getPlansData() async { + setState(() => isPlansLoading = true); + + try { + final response = await AppSettings.getCustomerPlans(); + final decoded = jsonDecode(response); + + if (decoded == null || decoded['data'] == null) { + setState(() { + plansList = []; + isPlansLoading = false; + }); + return; + } + + final List data = decoded['data']; + + setState(() { + plansList = data; + + activePlans = data.where((p) => + p['status'] == "processed" || + p['status'] == "accepted" || + p['status'] == "payment_completed").toList(); + + pendingPlans = + data.where((p) => p['status'] == "pending").toList(); + + isPlansLoading = false; + }); + } catch (e) { + print("Plans error $e"); + setState(() => isPlansLoading = false); + } + } + + Future getOrdersData() async { + setState(() => isOrdersDataLoading = true); + try { + final response = await AppSettings.getAllOrders(); + final decoded = jsonDecode(response); + + print("πŸ”Ή Full API Response: $decoded"); + + if (decoded == null || decoded['data'] == null) { + print("⚠️ No data key found in API response"); + setState(() { + isOrdersDataLoading = false; + ordersList = []; + }); + return; + } + + final List data = decoded['data']; + if (data.isEmpty) { + print("⚠️ Empty data array"); + setState(() { + isOrdersDataLoading = false; + ordersList = []; + }); + return; + } + + final parsedOrders = + data.map((model) => MyOrdersModel.fromJson(model)).toList(); + + print("βœ… Total orders fetched: ${parsedOrders.length}"); + + setState(() { + ordersList = parsedOrders; + + upcomingOrders = parsedOrders.where((order) { + final status = order.orderStatus.toLowerCase(); + return status == "pending" || + status == "advance_paid" || + status == "assigned" || + status == "deliveryboy_assigned"; + }).toList(); + + previousOrders = parsedOrders.where((order) { + final status = order.orderStatus.toLowerCase(); + return status == "completed" || status == "cancelled"; + }).toList(); + + isOrdersDataLoading = false; + }); + + print("πŸ“¦ Upcoming: ${upcomingOrders.length}, Previous: ${previousOrders.length}"); + } catch (e) { + print("❌ Error in getOrdersData: $e"); + setState(() => isOrdersDataLoading = false); + } + } + + String capitalizeFirst(String input) { + if (input.isEmpty) return input; + return input[0].toUpperCase() + input.substring(1); + } + + Widget buildUpcomingOrdersCard(MyOrdersModel order) { + return Card( + color: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Supplier name & status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.supplierName.isNotEmpty, + child: Text( + order.supplierName, + style: fontTextStyle(16, const Color(0xFF2D2E30), FontWeight.w600), + ), + ), + _statusChip(order.orderStatus, const Color(0xFFFDF3D3), const Color(0xFFF5CD47), + 'images/out_for_delivery.png'), + ], + ), + const SizedBox(height: 6), + // Type + Capacity + Row( + children: [ + Text("${order.capacity} L - ${order.typeofwater}", + style: fontTextStyle(12, const Color(0xFF71ABFD), FontWeight.w500)), + const SizedBox(width: 6), + Image.asset('images/arrow-right.png', width: 16, height: 16, color: const Color(0xFF71ABFD)), + ], + ), + const SizedBox(height: 4), + // Address + Payment + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(order.displayAddress, + style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w400))), + _paymentChip(order.paymentStatus), + ], + ), + const SizedBox(height: 4), + // Expected Delivery + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Expected delivery: ${order.expectedDateOfDelivery}", + style: fontTextStyle(10, const Color(0xFF939495), FontWeight.w500)), + if (order.price.isNotEmpty) + Text("β‚Ή ${order.price}", + style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w400)), + ], + ), + const Divider(height: 20), + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: ()async{ + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CancelOrderPage( + order: order, + status: order.orderStatus, + ), + ), + ); + + if (result == true) { + getOrdersData(); + } + }, + + /* onTap: () => Navigator.push( + context, MaterialPageRoute(builder: (_) => CancelOrderPage(order:order,status: order.orderStatus,))),*/ + + + child: Text("CANCEL ORDER", + style: fontTextStyle(12, const Color(0xFFE2483D), FontWeight.w600))), + const SizedBox(width: 30), + GestureDetector( + onTap: () {}, + child: Text("TRACK ORDER", + style: fontTextStyle(12, const Color(0xFF1D7AFC), FontWeight.w600))), + ], + ), + ], + ), + ), + ); + } + + Widget buildPreviousOrdersCard(MyOrdersModel order) { + final isDelivered = order.orderStatus == 'completed'; + return Card( + color: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (order.supplierName.isNotEmpty) + Text(order.supplierName, + style: fontTextStyle(16, const Color(0xFF2D2E30), FontWeight.w600)), + _statusChip( + order.orderStatus, + isDelivered ? const Color(0xFFC4E8C3) : const Color(0xFFF8D3D0), + isDelivered ? const Color(0xFF098603) : const Color(0xFFE2483D), + isDelivered ? 'images/rite.png' : 'images/cancel.png', + ), + ], + ), + const SizedBox(height: 4), + Text(order.typeofwater, + style: fontTextStyle(12, const Color(0xFF71ABFD), FontWeight.w500)), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(order.displayAddress, + style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w400))), + _paymentChip(order.paymentStatus), + ], + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(order.displayDeliveredDate, + style: fontTextStyle(10, const Color(0xFF939495), FontWeight.w500)), + if (order.price.isNotEmpty) + Text("β‚Ή ${order.price}", + style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w400)), + ], + ), + if (isDelivered) ...[ + const Divider(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () {}, + child: Text("REPEAT ORDER", + style: fontTextStyle(12, const Color(0xFF4692FD), FontWeight.w600)), + ), + const SizedBox(width: 30), + GestureDetector( + onTap: () {}, + child: Text("RATE ORDER", + style: fontTextStyle(12, const Color(0xFF646566), FontWeight.w600)), + ), + ], + ), + ] + ], + ), + ), + ); + } + + Widget _statusChip(String text, Color bg, Color txt, String img) { + return Container( + padding: const EdgeInsets.fromLTRB(4, 2, 4, 2), + decoration: BoxDecoration( + color: bg, borderRadius: BorderRadius.circular(4), border: Border.all(color: bg)), + child: Row( + children: [ + Image.asset(img, width: 12, height: 12), + const SizedBox(width: 4), + Text(capitalizeFirst(text), + style: fontTextStyle(10, txt, FontWeight.w500)), + ], + ), + ); + } + + Widget _paymentChip(String status) { + final isPaid = status.toLowerCase() == 'paid'; + return Row( + children: [ + Image.asset(isPaid ? 'images/paid.png' : 'images/warning.png', width: 16, height: 16), + const SizedBox(width: 4), + Text(capitalizeFirst(status), + style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w400)), + ], + ); + } + + Widget renderUi() { + if (ordersList.isEmpty) { + return const Center( + child: Text("No Data Available", + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500))); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (upcomingOrders.isNotEmpty) ...[ + Text("UPCOMING ORDERS", + style: fontTextStyle(12, const Color(0xFF646566), FontWeight.w400)), + const SizedBox(height: 8), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: upcomingOrders.length, + itemBuilder: (context, index) => + buildUpcomingOrdersCard(upcomingOrders[index]), + ), + ], + if (previousOrders.isNotEmpty) ...[ + const SizedBox(height: 16), + Text("PREVIOUS ORDERS", + style: fontTextStyle(12, const Color(0xFF646566), FontWeight.w400)), + const SizedBox(height: 8), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: previousOrders.length, + itemBuilder: (context, index) => + buildPreviousOrdersCard(previousOrders[index]), + ), + ], + ], + ), + ); + } + + Widget buildPlansCard(var plan) { + return Card( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${plan['capacity']} L - ${plan['type_of_water']}", + style: fontTextStyle( + 14, const Color(0xFF2D2E30), FontWeight.w600), + ), + + _statusChip( + plan['status'], + const Color(0xFFE3F2FD), + const Color(0xFF1D7AFC), + 'images/rite.png', + ), + ], + ), + + const SizedBox(height: 6), + + Text( + "Frequency : ${plan['frequency']}", + style: fontTextStyle( + 12, const Color(0xFF71ABFD), FontWeight.w500), + ), + + const SizedBox(height: 4), + + Text( + "Start : ${plan['start_date']}", + style: fontTextStyle( + 10, const Color(0xFF939495), FontWeight.w500), + ), + + Text( + "End : ${plan['end_date']}", + style: fontTextStyle( + 10, const Color(0xFF939495), FontWeight.w500), + ), + + const Divider(height: 20), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () {}, + child: Text( + "VIEW PLAN", + style: fontTextStyle( + 12, const Color(0xFF1D7AFC), FontWeight.w600), + ), + ), + + const SizedBox(width: 30), + + GestureDetector( + onTap: () {}, + child: Text( + "CANCEL PLAN", + style: fontTextStyle( + 12, const Color(0xFFE2483D), FontWeight.w600), + ), + ), + ], + ) + ], + ), + ), + ); + } + + Widget renderPlansUi() { + + if (plansList.isEmpty) { + return const Center( + child: Text("No Plans Available"), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// ACTIVE PLANS + if (activePlans.isNotEmpty) ...[ + Text( + "ACTIVE PLANS", + style: fontTextStyle( + 12, + const Color(0xFF646566), + FontWeight.w400, + ), + ), + + const SizedBox(height: 10), + + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: activePlans.length, + itemBuilder: (context, index) { + + final plan = activePlans[index]; + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: buildPlansCard(plan), + ); + }, + ), + ], + + /// SPACE + const SizedBox(height: 20), + + /// PENDING PLANS + if (pendingPlans.isNotEmpty) ...[ + Text( + "PENDING PLANS", + style: fontTextStyle( + 12, + const Color(0xFF646566), + FontWeight.w400, + ), + ), + + const SizedBox(height: 10), + + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: pendingPlans.length, + itemBuilder: (context, index) { + + final plan = pendingPlans[index]; + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: buildPlansCard(plan), + ); + }, + ), + ], + + const SizedBox(height: 20), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'Orders', + style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w500), + ), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(10, 10, 0, 10), + child: IconButton( + icon: Image( + image: AssetImage('images/customercare_appbar.png')), + onPressed: () {}, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', // Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + bottom: PreferredSize( + preferredSize: Size.fromHeight(50.0), + child: Container( + color: Colors.white, + child: TabBar( + controller: _controller, + indicatorColor: Colors.transparent, + tabs: [ + // Tab 1 + Builder( + builder: (BuildContext context) { + return Container( + decoration: BoxDecoration( + color: _controller.index == 0 + ? Color(0XFFFE8F2FF) + : Colors.white, // Selected tab color + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: _controller.index == 0 + ? Color(0XFFFE8F2FF) + : Colors.white, + width: 2), + ), + child: Tab( + child: Center( + child: Text('Orders', + style: fontTextStyle(12, Color(0XFF101214), + FontWeight.w600))), + ), + ); + }, + ), + // Tab 2 + Builder( + builder: (BuildContext context) { + return Container( + decoration: BoxDecoration( + color: _controller.index == 1 + ? Color(0XFFFE8F2FF) + : Colors.white, // Selected tab color + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: _controller.index == 1 + ? Color(0XFFFE8F2FF) + : Colors.white, + width: 2), + ), + child: Tab( + child: Center( + child: Text('Plans', + style: fontTextStyle(12, Color(0XFF101214), + FontWeight.w600))), + ), + ); + }, + ), + ], + )), + ), + ), + body: Builder( + builder: (BuildContext context) { + return TabBarView( + controller: _controller, + children: [ + isOrdersDataLoading + ? const Center(child: CircularProgressIndicator()) + : renderUi(), + + isPlansLoading + ? const Center(child: CircularProgressIndicator()) + : renderPlansUi(), + ], + ); + }, + ), + ); + } +} diff --git a/lib/supplier/my_orders_model.dart b/lib/supplier/my_orders_model.dart new file mode 100644 index 0000000..850ce6d --- /dev/null +++ b/lib/supplier/my_orders_model.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class MyOrdersModel { + String tankerName = ''; + String bookingid = ''; + String supplierId = ''; + String dateOfOrder = ''; + String typeofwater = ''; + String delivery_agent_mobile = ''; + String delivery_agent = ''; + String capacity = ''; + String address = ''; + String price = ''; + String customerId = ''; + String startTime = ''; + String stopTime = ''; + String orderStatus = ''; + String initialWaterLevel = ''; + String finalWaterLevel = ''; + String quantityDelivered = ''; + String amountPaid = ''; + String amountDue = ''; + String paymentMode = ''; + String deliverdWater = ''; + String supplierName = ''; + String deliveredDate = ''; + String displayDeliveredDate = ''; + String displayAddress = ''; + String expectedDateOfDelivery = ''; + String displayDateOfOrder = ''; + String paymentStatus = ''; + String tankName = ''; + var tankerRunningStatus; + double lat = 0; + double lng = 0; + Color cardColor = Colors.white; + String tank_otp=''; + String stop_otp=''; + String dbId=''; + + MyOrdersModel(); + + factory MyOrdersModel.fromJson(Map json) { + final rtvm = MyOrdersModel(); + + rtvm.tankerName = json['tankerName']?.toString() ?? ''; + rtvm.bookingid = json['bookingid']?.toString() ?? ''; + rtvm.supplierId = json['supplierId']?.toString() ?? ''; + rtvm.dateOfOrder = json['dateOfOrder']?.toString() ?? ''; + rtvm.typeofwater = json['typeofwater']?.toString() ?? ''; + rtvm.supplierName = json['supplierName']?.toString() ?? ''; + rtvm.delivery_agent_mobile = json['delivery_agent_mobile']?.toString() ?? ''; + rtvm.capacity = json['capacity']?.toString() ?? ''; + rtvm.address = json['address']?.toString() ?? ''; + rtvm.price = json['price']?.toString() ?? ''; + rtvm.orderStatus = json['orderStatus']?.toString().toLowerCase() ?? ''; + rtvm.customerId = json['customerId']?.toString() ?? ''; + rtvm.startTime = json['start_time']?.toString() ?? ''; + rtvm.stopTime = json['stop_time']?.toString() ?? ''; + rtvm.initialWaterLevel = json['initial_water_level']?.toString() ?? ''; + rtvm.finalWaterLevel = json['final_water_level']?.toString() ?? ''; + rtvm.quantityDelivered = json['quantityDelivered']?.toString() ?? ''; + rtvm.amountPaid = json['amount_paid']?.toString() ?? ''; + rtvm.amountDue = json['amount_due']?.toString() ?? ''; + rtvm.paymentMode = json['payment_mode']?.toString() ?? ''; + rtvm.deliverdWater = json['quantityDelivered']?.toString() ?? ''; + rtvm.deliveredDate = json['deliveredDate']?.toString() ?? ''; + rtvm.expectedDateOfDelivery = json["expectedDateOfDelivery"]?.toString() ?? ''; + rtvm.tankerRunningStatus = json['tankerRunningStatus'] ?? ''; + rtvm.delivery_agent = json['delivery_agent']?.toString() ?? ''; + rtvm.paymentStatus = json['payment_status']?.toString() ?? ''; + rtvm.tankName = json['tankName']?.toString() ?? ''; + rtvm.tank_otp = json['tank_otp']?.toString() ?? ''; + rtvm.dbId = json['_id']?.toString() ?? ''; + rtvm.stop_otp = json['stop_otp']?.toString() ?? ''; + + + // βœ… Handle Delivered Date (Safe Parsing) + if (rtvm.deliveredDate.isNotEmpty) { + try { + final inputFormat = DateFormat("dd-MMM-yyyy - HH:mm"); + final dateTime = inputFormat.parse(rtvm.deliveredDate); + final outputFormat = DateFormat("MMMM d yyyy, h:mm a"); + rtvm.displayDeliveredDate = outputFormat.format(dateTime); + } catch (_) { + rtvm.displayDeliveredDate = rtvm.deliveredDate; // fallback to raw + } + } + + // βœ… Handle Date of Order + if (rtvm.dateOfOrder.isNotEmpty) { + final raw = rtvm.dateOfOrder.trim(); + DateTime? dt; + + try { + // Normalize format + String s = raw.replaceAll(RegExp(r'\s*-\s*'), '-'); + s = s.replaceAllMapped(RegExp(r'(\d{1,2})-([A-Za-z]{3,9})-(\d{4})'), (m) { + final mon = m.group(2)!; + final cap = mon[0].toUpperCase() + mon.substring(1).toLowerCase(); + return "${m.group(1)}-$cap-${m.group(3)}"; + }); + + final patterns = [ + "dd-MMM-yyyy-HH:mm", + "dd-MMM-yyyy HH:mm", + "dd-MMM-yyyy", + "dd-MM-yyyy HH:mm", + "dd-MM-yyyy", + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd", + ]; + + for (final p in patterns) { + try { + dt = DateFormat(p, 'en_US').parseStrict(s); + break; + } catch (_) {} + } + + if (dt == null) dt = DateTime.tryParse(raw); + } catch (_) {} + + if (dt != null) { + final hasTime = RegExp(r'\d{1,2}:\d{2}').hasMatch(raw); + final fmt = DateFormat(hasTime ? "MMMM d yyyy, h:mm a" : "MMMM d yyyy"); + rtvm.displayDateOfOrder = fmt.format(dt); + } + } + + // βœ… Handle Address + final rawAddr = (rtvm.address ?? '').trim(); + if (rawAddr.isNotEmpty) { + final parts = rawAddr.split(',').map((e) => e.trim()).toList(); + parts.removeWhere((p) => + p.toLowerCase().contains("india") || + RegExp(r'\d{6}').hasMatch(p) || + p.toLowerCase().contains("telangana")); + rtvm.displayAddress = parts.length >= 2 ? parts[parts.length - 2] : rawAddr; + } else { + rtvm.displayAddress = ''; + } + + // βœ… Latitude & Longitude Parsing + rtvm.lat = double.tryParse(json['latitude']?.toString() ?? '0') ?? 0; + rtvm.lng = double.tryParse(json['longitude']?.toString() ?? '0') ?? 0; + + return rtvm; + } +} diff --git a/lib/supplier/my_suppliers.dart b/lib/supplier/my_suppliers.dart new file mode 100644 index 0000000..30d1195 --- /dev/null +++ b/lib/supplier/my_suppliers.dart @@ -0,0 +1,1540 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:gallery_saver/gallery_saver.dart'; +import 'package:flutter/services.dart'; +import 'package:share/share.dart'; +import 'package:watermanagement/models/connected_suppliers_model.dart'; +import 'package:watermanagement/models/consumption_model.dart'; +import 'package:watermanagement/models/water_suppliers_model.dart'; +import 'package:watermanagement/supplier/favourites_model.dart'; +import 'package:watermanagement/supplier/place_order.dart'; +import 'package:watermanagement/supplier/supplier_details.dart'; + +class MySuppliers extends StatefulWidget { + var navigationFrom; + MySuppliers({this.navigationFrom}); + @override + State createState() => _MySuppliersState(); +} + +class _MySuppliersState extends State + with TickerProviderStateMixin { + late TabController _controller; + String tabMessage = "Welcome to Tab 1"; + bool isDataLoading = false; + bool isConnectedDataLoading = false; + bool isFavouriteDataLoading = false; + List suppliersList = []; + List connectedSuppliersList = []; + List favouritesList = []; + Set favouriteIds = {}; // for fast lookup + + @override + void initState() { + super.initState(); + _controller = TabController(vsync: this, length: 3); + // Listen for tab changes + _controller.addListener(() { + if (_controller.indexIsChanging) { + setState(() { + // Change message or perform any other update + tabMessage = + _controller.index == 0 ? "Welcome to Tab 1" : "Welcome to Tab 2"; + }); + } + }); + getAllSuppliersData(); + getConnectedSuppliersData(); + getAllFavouritesData(); + } + + Future getAllSuppliersData() async { + + setState(() { + isDataLoading = true; + }); + + try { + + String tankerResponse = await AppSettings.getAllSuppliers(); + + if (tankerResponse.isEmpty) { + setState(() { + isDataLoading = false; + }); + return; + } + + var decoded = jsonDecode(tankerResponse); + + List supplierList = decoded['suppliers'] ?? []; + + List tempList = []; + + for (var item in supplierList) { + + try { + + tempList.add(WaterSuppliersModel.fromJson(item)); + + } catch (e) { + + print("Supplier parse error: $e"); + + } + + } + + setState(() { + suppliersList = tempList; + isDataLoading = false; + }); + + } catch (e) { + + print("API ERROR: $e"); + + setState(() { + isDataLoading = false; + }); + + } + + } + + Future getAllFavouritesData() async { + isFavouriteDataLoading = true; + try { + var tankerResponse = await AppSettings.getAllFavourites(); + + setState(() { + favouritesList = + ((jsonDecode(tankerResponse)['data']) as List).map((dynamic model) { + return favouritesModel.fromJson(model); + }).toList(); + favouriteIds = favouritesList.map((e) => e.supplier_id).toSet(); + isFavouriteDataLoading = false; + }); + } catch (e) { + setState(() { + isFavouriteDataLoading = false; + }); + } + } + + Future getConnectedSuppliersData() async { + isConnectedDataLoading = true; + + try { + var response = await AppSettings.getConnetcedSuppliers(); + + setState(() { + connectedSuppliersList = + ((jsonDecode(response)['data']) as List).map((dynamic model) { + return ConnectedSuppliersModel.fromJson(model); + }).toList(); + isConnectedDataLoading = false; + }); + } catch (e) { + setState(() { + isConnectedDataLoading = false; + }); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Widget mySuppliers() { + return isConnectedDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : connectedSuppliersList.length != 0 + ? Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.separated( + padding: EdgeInsets.all(8), + itemCount: connectedSuppliersList.length, + separatorBuilder: (context, index) => SizedBox( + height: + MediaQuery.of(context).size.height * .008, + ), + itemBuilder: (BuildContext context, int index) { + return Container( + decoration: BoxDecoration( + color: + Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular( + 12), // Rounded corners + border: Border.all( + color: Colors.grey.shade300, // Border color + width: 1, // Border width + ), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + /*CircleAvatar( + radius: 30, + backgroundColor: Color(0XFFE8F2FF), // Fallback background color + child: usersList[index].profilePic.isNotEmpty? Image.network(usersList[index].profilePic,): + + Text( + usersList[index].username.isNotEmpty ? usersList[index].username[0].toUpperCase() : '?', // First letter of name or '?' + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color:Color(0XFF9EC6FE), + ), + ) + ),*/ + CircleAvatar( + radius: 20, + backgroundColor: Color( + 0XFFE8F2FF), // Fallback background color + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: + 50, // Match the diameter of the CircleAvatar + height: 50, + ), // Default icon if no profilePic + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .012, + ), + Expanded( + flex: 2, + child: Container( + //width:MediaQuery.of(context).size.height * .3, + //height: MediaQuery.of(context).size.height * .06, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Visibility( + visible: true, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Visibility( + visible: + connectedSuppliersList[ + index] + .supplier_name != + '', + child: Text( + connectedSuppliersList[ + index] + .supplier_name + .toString(), + style: fontTextStyle( + 16, + Color( + 0XFF2D2E30), + FontWeight + .w600), + ), + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .center, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Image.asset( + 'images/star.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + Visibility( + visible: true, + child: Text( + '4.2 (20K+ Ratings)', + style: fontTextStyle( + 9, + Color( + 0XFF515253), + FontWeight + .w400), + ), + ), + ], + ) + ], + ), + ), + Visibility( + visible: true, + child: Text( + 'Drinking | Bore Water', + style: fontTextStyle( + 12, + Color(0XFF4692FD), + FontWeight.w500), + ), + ), + Visibility( + visible: true, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + connectedSuppliersList[ + index] + .displayAddress + + ' ' + + connectedSuppliersList[ + index] + .distanceInMeters + .toString() + + ' Km', + style: fontTextStyle( + 12, + Color( + 0XFF515253), + FontWeight + .w400)), + GestureDetector( + onTap: () async { + AppSettings + .preLoaderDialog( + context); + if (connectedSuppliersList[ + index] + .isFavorite) { + try { + bool + tankerResponse = + await AppSettings.removeFavourites( + connectedSuppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longSuccessToast( + 'Supplier removed from favourites'); + await getAllSuppliersData(); + await getConnectedSuppliersData(); + } else { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to remove from favourites'); + } + } catch (e) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to remove from favourites'); + } + } else { + try { + bool + tankerResponse = + await AppSettings.addFavourites( + connectedSuppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longSuccessToast( + 'Supplier added to favourites'); + await getAllSuppliersData(); + await getConnectedSuppliersData(); + await getAllFavouritesData(); + } else { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to add favourites'); + } + } catch (e) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to add favourites'); + } + } + }, + child: + connectedSuppliersList[ + index] + .isFavorite + ? Image.asset( + 'images/heart_active.png', + fit: BoxFit + .cover, + width: + 16, // Match the diameter of the CircleAvatar + height: + 16, + ) + : Image.asset( + 'images/heart_outline.png', + fit: BoxFit + .cover, + width: + 16, // Match the diameter of the CircleAvatar + height: + 16, + ), + ) + ], + ), + ), + ], + ), + ), + ), + ], + ), + Divider( + color: Colors.grey.shade300, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Plan', + style: fontTextStyle( + 10, + Color(0XFF343637), + FontWeight.w400), + ), + Text( + '15,000 / week', + style: fontTextStyle( + 10, + Color(0XFF2D2E30), + FontWeight.w500), + ) + ], + ), + SizedBox( + height: + MediaQuery.of(context).size.height * + .016, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, + color: Color(0XFF1D7AFC), + ), + borderRadius: + BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB( + 16, 12, 16, 12), + child: Text('Chat', + style: fontTextStyle( + 12, + Color(0XFF1D7AFC), + FontWeight.w600)), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .010, + ), + Expanded( + child: GestureDetector( + onTap: () { + /* Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlaceOrder(details: connectedSuppliersList[index],)), + );*/ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SupplierScreen( + details: + connectedSuppliersList[ + index], + )), + ); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFF1D7AFC), + border: Border.all( + width: 1, + color: Color(0XFFFFFFFF), + ), + borderRadius: + BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB( + 16, 12, 16, 12), + child: Text('Place Order', + style: fontTextStyle( + 12, + Color(0XFFFFFFFF), + FontWeight.w600)), + ), + ), + )) + ], + ) + ], + ), + ), + ); + })), + Align( + alignment: Alignment.center, + child: GestureDetector( + onTap: () {}, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'images/add_icon.png', + fit: BoxFit.cover, + width: + 24, // Match the diameter of the CircleAvatar + height: 24, + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + Text('Find New Suppliers', + style: fontTextStyle( + 12, Color(0XFF000000), FontWeight.w400)) + ], + ), + ), + ) + ], + ), + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + ); + } + + Widget otherSuppliers() { + return isDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : suppliersList.length != 0 + ? Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.separated( + padding: EdgeInsets.all(8), + itemCount: suppliersList.length, + separatorBuilder: (context, index) => SizedBox( + height: + MediaQuery.of(context).size.height * .008, + ), + itemBuilder: (BuildContext context, int index) { + return Container( + decoration: BoxDecoration( + color: + Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular( + 12), // Rounded corners + border: Border.all( + color: Colors.grey.shade300, // Border color + width: 1, // Border width + ), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + /*CircleAvatar( + radius: 30, + backgroundColor: Color(0XFFE8F2FF), // Fallback background color + child: usersList[index].profilePic.isNotEmpty? Image.network(usersList[index].profilePic,): + + Text( + usersList[index].username.isNotEmpty ? usersList[index].username[0].toUpperCase() : '?', // First letter of name or '?' + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color:Color(0XFF9EC6FE), + ), + ) + ),*/ + CircleAvatar( + radius: 20, + backgroundColor: Color( + 0XFFE8F2FF), // Fallback background color + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: + 50, // Match the diameter of the CircleAvatar + height: 50, + ), // Default icon if no profilePic + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .012, + ), + Expanded( + flex: 2, + child: Container( + //width:MediaQuery.of(context).size.height * .3, + //height: MediaQuery.of(context).size.height * .06, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Visibility( + visible: true, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Visibility( + visible: suppliersList[ + index] + .supplier_name != + '', + child: Text( + suppliersList[index] + .supplier_name + .toString(), + style: fontTextStyle( + 16, + Color( + 0XFF2D2E30), + FontWeight + .w600), + ), + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .center, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + Image.asset( + 'images/star.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + Visibility( + visible: true, + child: Text( + '4.2 (20K+ Ratings)', + style: fontTextStyle( + 9, + Color( + 0XFF515253), + FontWeight + .w400), + ), + ), + ], + ) + ], + ), + ), + Visibility( + visible: true, + child: Text( + 'Drinking | Bore Water', + style: fontTextStyle( + 12, + Color(0XFF4692FD), + FontWeight.w500), + ), + ), + Visibility( + visible: true, + child: Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + suppliersList[index] + .displayAddress + + ' ' + + suppliersList[ + index] + .distanceInMeters + .toString() + + ' Km', + style: fontTextStyle( + 12, + Color( + 0XFF515253), + FontWeight + .w400)), + GestureDetector( + onTap: () async { + AppSettings + .preLoaderDialog( + context); + if (favouriteIds.contains( + suppliersList[ + index] + .supplier_id)) { + try { + bool + tankerResponse = + await AppSettings.removeFavourites( + suppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longSuccessToast( + 'Supplier removed from favourites'); + await getAllSuppliersData(); + await getConnectedSuppliersData(); + await getAllFavouritesData(); + } else { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to remove from favourites'); + } + } catch (e) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to remove from favourites'); + } + } else { + try { + bool + tankerResponse = + await AppSettings.addFavourites( + suppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longSuccessToast( + 'Supplier added to favourites'); + await getAllSuppliersData(); + await getConnectedSuppliersData(); + await getAllFavouritesData(); + } else { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to add favourites'); + } + } catch (e) { + Navigator.of( + context, + rootNavigator: + true) + .pop(); + AppSettings + .longFailedToast( + 'Failed to add favourites'); + } + } + }, + child: favouriteIds.contains( + suppliersList[ + index] + .supplier_id) + ? Image.asset( + 'images/heart_active.png', + fit: BoxFit + .cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ) + : Image.asset( + 'images/heart_outline.png', + fit: BoxFit + .cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ) + ], + ), + ), + ], + ), + ), + ), + ], + ), + Divider( + color: Colors.grey.shade300, + ), + SizedBox( + height: + MediaQuery.of(context).size.height * + .016, + ), + suppliersList[index].isConnected + ? Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + width: 1, + color: + Color(0XFF1D7AFC), + ), + borderRadius: + BorderRadius + .circular(12), + ), + alignment: + Alignment.center, + padding: + EdgeInsets.symmetric( + vertical: 12), + child: Text( + 'Chat', + style: fontTextStyle( + 12, + Color(0XFF1D7AFC), + FontWeight.w600), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .010, + ), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SupplierScreen( + details: + suppliersList[ + index], + ), + ), + ); + }, + child: Container( + decoration: BoxDecoration( + color: + Color(0XFF1D7AFC), + border: Border.all( + width: 1, + color: Colors.white, + ), + borderRadius: + BorderRadius + .circular(12), + ), + alignment: + Alignment.center, + padding: + EdgeInsets.symmetric( + vertical: 12), + child: Text( + 'Place Order', + style: fontTextStyle( + 12, + Colors.white, + FontWeight.w600), + ), + ), + ), + ) + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded( + child: GestureDetector( + onTap: () async { + var payload = new Map< + String, dynamic>(); + payload["customerId"] = + AppSettings + .customerId; + payload["supplierId"] = + suppliersList[index] + .supplier_id; + + bool requestStatus = + await AppSettings + .connectRequest( + payload); + + if (requestStatus) { + AppSettings + .longSuccessToast( + "Request Sent Successfully"); + await getAllSuppliersData(); + await getConnectedSuppliersData(); + } else { + AppSettings.longFailedToast( + "Request Sent faileds"); + } + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape + .rectangle, + color: + Color(0XFFFFFFFF), + border: Border.all( + width: 1, + color: Color( + 0XFF098603), + ), + borderRadius: + BorderRadius + .circular( + 24, + )), + alignment: + Alignment.center, + child: Padding( + padding: + EdgeInsets.fromLTRB( + 24, 12, 24, 12), + child: Text('Connect', + style: fontTextStyle( + 12, + Color( + 0XFF098603), + FontWeight + .w600)), + ), + ), + ), + ), + ], + ) + ], + ), + ), + ); + })), + ], + ), + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + ); + } + + Widget favorites() { + return isFavouriteDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : favouritesList.isNotEmpty + ? Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.separated( + padding: EdgeInsets.all(8), + itemCount: favouritesList.length, + separatorBuilder: (context, index) => SizedBox( + height: MediaQuery.of(context).size.height * .008, + ), + itemBuilder: (BuildContext context, int index) { + final supplier = favouritesList[index]; + final currentSupplierId = supplier.supplier_id; + + final isConnected = connectedSuppliersList.any( + (item) => item.supplier_id == currentSupplierId, + ); + + return Container( + decoration: BoxDecoration( + color: Color(0XFFFFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(10, 10, 10, 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Color(0XFFE8F2FF), + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: 50, + height: 50, + ), + ), + SizedBox( + width: + MediaQuery.of(context).size.width * + .012, + ), + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Visibility( + visible: + supplier.supplier_name != + '', + child: Text( + supplier.supplier_name, + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600), + ), + ), + Row( + children: [ + Image.asset( + 'images/star.png', + fit: BoxFit.cover, + width: 16, + height: 16, + ), + Text( + '4.2 (20K+ Ratings)', + style: fontTextStyle( + 9, + Color(0XFF515253), + FontWeight.w400), + ), + ], + ) + ], + ), + Text( + 'Drinking | Bore Water', + style: fontTextStyle( + 12, + Color(0XFF4692FD), + FontWeight.w500), + ), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + '${supplier.displayAddress} ${supplier.distanceInMeters} Km', + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w400)), + GestureDetector( + onTap: () async { + AppSettings.preLoaderDialog( + context); + try { + bool tankerResponse = + await AppSettings + .removeFavourites( + supplier + .supplier_id); + Navigator.of(context, + rootNavigator: + true) + .pop(); + if (tankerResponse) { + AppSettings + .longSuccessToast( + 'Supplier removed from favourites'); + await getAllSuppliersData(); + await getConnectedSuppliersData(); + await getAllFavouritesData(); + } else { + AppSettings.longFailedToast( + 'Failed to remove from favourites'); + } + } catch (e) { + Navigator.of(context, + rootNavigator: + true) + .pop(); + AppSettings.longFailedToast( + 'Failed to remove from favourites'); + } + }, + child: Image.asset( + 'images/heart_active.png', + fit: BoxFit.cover, + width: 16, + height: 16, + ), + ) + ], + ), + ], + ), + ), + ], + ), + Divider(color: Colors.grey.shade300), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Plan', + style: fontTextStyle(10, + Color(0XFF343637), FontWeight.w400), + ), + Text( + '15,000 / week', + style: fontTextStyle(10, + Color(0XFF2D2E30), FontWeight.w500), + ) + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * + .016, + ), + isConnected + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + width: 1, + color: Color(0XFF1D7AFC), + ), + borderRadius: + BorderRadius.circular( + 12), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + vertical: 12), + child: Text( + 'Chat', + style: fontTextStyle( + 12, + Color(0XFF1D7AFC), + FontWeight.w600), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .010, + ), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + SupplierScreen( + details: supplier, + ), + ), + ); + }, + child: Container( + decoration: BoxDecoration( + color: Color(0XFF1D7AFC), + border: Border.all( + width: 1, + color: Colors.white, + ), + borderRadius: + BorderRadius.circular( + 12), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + vertical: 12), + child: Text( + 'Place Order', + style: fontTextStyle( + 12, + Colors.white, + FontWeight.w600), + ), + ), + ), + ) + ], + ) + : Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + width: 1, + color: Color(0XFF098603), + ), + borderRadius: + BorderRadius.circular( + 24), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + vertical: 12), + child: Text( + 'Connect', + style: fontTextStyle( + 12, + Color(0XFF098603), + FontWeight.w600), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + ); + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: _controller.length, // Two tabs + child: Scaffold( + backgroundColor: Colors.white, + appBar:AppBar( + elevation: 0, + backgroundColor: Colors.white, + title: Text( + 'My Suppliers', + style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w500), + ), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(10, 10, 0, 10), + child: IconButton( + icon: Image( + image: AssetImage( + 'images/calender_supplier_landing.png')), + onPressed: () {}, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', // Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + 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, // Adjust the fit + ), + ), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(70.0), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + color: Color(0xFFF5F6F6), + borderRadius: BorderRadius.circular(23), + // Remove all borders/shadows + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(19), + child: TabBar( + controller: _controller, + indicator: BoxDecoration(), // No underline + indicatorColor: Colors.transparent, // Extra safety + labelPadding: EdgeInsets.zero, + dividerColor: Colors + .transparent, // <-- Removes line between TabBar and TabBarView (Flutter 3.10+) + tabs: List.generate(3, (index) { + final labels = [ + 'Suppliers', + 'My Suppliers', + 'Favourites' + ]; + return Container( + decoration: BoxDecoration( + color: _controller.index == index + ? Color(0XFFF5CD47) + : Color(0xFFF5F6F6), + borderRadius: BorderRadius.circular(27), + ), + child: Tab( + child: Center( + child: Text( + labels[index], + style: fontTextStyle( + 12, + Color(0XFF3B3B3B), + FontWeight.w600, + ), + ), + ), + ), + ); + }), + ), + ), + ), + ), + ), + body: Builder( + builder: (BuildContext context) { + return TabBarView( + controller: _controller, + children: [ + // Tab 1 content + otherSuppliers(), + + // Tab 2 content + mySuppliers(), + // Tab 2 content + favorites() + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/supplier/order_arrived.dart b/lib/supplier/order_arrived.dart new file mode 100644 index 0000000..0684385 --- /dev/null +++ b/lib/supplier/order_arrived.dart @@ -0,0 +1,904 @@ +// ------------- FULL FILE STARTS HERE --------------- + +import 'dart:convert'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import '../models/tanksview_model.dart'; + +class ArrivalScreen extends StatefulWidget { + var orderDetails; + + ArrivalScreen({ + required this.orderDetails, + }); + + @override + State createState() => _ArrivalScreenState(); +} + +class _ArrivalScreenState extends State { + bool isLoading = false; + List tankLevelsList = []; + List sumpTanks = []; + GetTanksDetailsModel? selectedTank; + + String otp = ''; + String unload_complete_otp = ''; + String? selectedTankId; + + bool _showOrderSummary = false; + int currentStep = 0; + bool allowTankChange = true; + + // ------------------------------------------- + // FETCH TANK DATA + // ------------------------------------------- + Future getAllTanksData() async { + isLoading = true; + var response1 = await AppSettings.getTankLevels(); + setState(() { + tankLevelsList = + ((jsonDecode(response1)['data']) as List).map((dynamic model) { + return GetTanksDetailsModel.fromJson(model); + }).toList(); + + sumpTanks = tankLevelsList + .where((product) => + product.tank_location.toString().toUpperCase() == 'SUMP') + .toList(); + + isLoading = false; + }); + } + + @override + void initState() { + getAllTanksData(); + super.initState(); + + otp = widget.orderDetails.tank_otp ?? ''; + unload_complete_otp = widget.orderDetails.stop_otp ?? ''; + + _setChecklistStepFromStatus(); + } + + // ------------------------------------------- + // DETERMINE CHECKLIST STEP + LOCK TANK CHANGE + // ------------------------------------------- + void _setChecklistStepFromStatus() { + String status = (widget.orderDetails.orderStatus ?? "").toLowerCase().trim(); + + if (status == "unloading_started" || status == "in_progress") { + currentStep = 1; // Step 1 active + allowTankChange = false; + } + else if (status == "unloading_stopped" || status == "unloading_completed") { + currentStep = 2; // Step 2 active β†’ View Bill ENABLED + allowTankChange = false; + } + else if (status == "payment_pending" || status == "payment_waiting") { + currentStep = 2; // Still show Step 2 + allowTankChange = false; + } + else { + currentStep = 0; // Initial stage + allowTankChange = true; // Only here tank change is allowed + } + + setState(() {}); + } + + // ------------------------------------------- + // REFRESH ALL DATA + // ------------------------------------------- + Future _refreshAllData() async { + try { + var response = + await AppSettings.updateBookingDetailsById(widget.orderDetails.dbId); + + if (response != '') { + final data = jsonDecode(response)['data'] ?? {}; + + widget.orderDetails.orderStatus = + data["orderStatus"] ?? widget.orderDetails.orderStatus; + + widget.orderDetails.tank_otp = + data["tank_otp"] ?? widget.orderDetails.tank_otp; + + widget.orderDetails.stop_otp = + data["stop_otp"] ?? widget.orderDetails.stop_otp; + + widget.orderDetails.tankName = + data["tankName"] ?? widget.orderDetails.tankName; + + otp = widget.orderDetails.tank_otp ?? ''; + unload_complete_otp = widget.orderDetails.stop_otp ?? ''; + selectedTankId = widget.orderDetails.tankName ?? ''; + } + + await getAllTanksData(); + + _setChecklistStepFromStatus(); + } catch (e) { + print("Refresh failed: $e"); + } + } + + // ------------------------------------------- + // UPDATE TANK NAME TO SERVER + // ------------------------------------------- + Future _updateTankName(String tankName) async { + var payload = {"tankName": tankName}; + + var updateTankStatus = await AppSettings.updateTankNameWhileDelivery( + widget.orderDetails.dbId, payload); + + if (updateTankStatus != '') { + AppSettings.longSuccessToast('Updated successfully'); + + final updatedData = jsonDecode(updateTankStatus)['data'] ?? {}; + final updatedTankName = updatedData["tankName"]?.toString() ?? tankName; + final updatedOtp = updatedData["tank_otp"]?.toString() ?? ""; + + setState(() { + selectedTankId = updatedTankName; + otp = updatedOtp; + + try { + widget.orderDetails.tankName = updatedTankName; + widget.orderDetails.tank_otp = updatedOtp; + } catch (_) {} + }); + + /*Navigator.of(context).pop(true);*/ + } else { + AppSettings.longFailedToast('Update failed'); + } + } + + // ------------------------------------------- + // BUILD MAIN UI + // ------------------------------------------- + @override + Widget build(BuildContext context) { + final bool hasTankAlready = + widget.orderDetails.tankName != null && + widget.orderDetails.tankName!.isNotEmpty; + + GetTanksDetailsModel? preSelectedTank; + + if (hasTankAlready) { + try { + preSelectedTank = sumpTanks.firstWhere( + (t) => t.tank_name == widget.orderDetails.tankName); + } catch (e) { + preSelectedTank = null; + } + } + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppSettings.supplierAppBarWithoutActions( + 'Order#${widget.orderDetails.bookingid}', context), + body: WillPopScope( + onWillPop: () async { + Navigator.pop(context, true); + return false; + }, + child: SafeArea( + child: RefreshIndicator( + onRefresh: _refreshAllData, + displacement: 60, + strokeWidth: 3, + color: Color(0xFF1D7AFC), + backgroundColor: Colors.white, + triggerMode: RefreshIndicatorTriggerMode.onEdge, + child: CustomScrollView( + physics: AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: buildMainBody( + context, hasTankAlready, preSelectedTank), + ), + ], + ), + ), + ), + ), + ); + } + + // ------------------------------------------- + // YOUR ENTIRE UI BODY + // ------------------------------------------- + Widget buildMainBody(context, hasTankAlready, preSelectedTank) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ------------ BANNER ----------------- + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color(0xFFFFF4D9), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + "Arrived at your location!", + style: fontTextStyle(20, Color(0xFF232527), FontWeight.w800), + ), + SizedBox(height: 4), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "HOME", + style: fontTextStyle( + 11, Color(0xFF343637), FontWeight.w500), + ), + TextSpan( + text: " - ${AppSettings.userAddress}", + style: fontTextStyle( + 11, Color(0xFF343637), FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 16), + + // ------------ PROFILE ----------------- + Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Color(0XFFE8F2FF), + child: Image.asset('images/profile_user.png', + width: 50, height: 50), + ), + SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.orderDetails.delivery_agent, + style: fontTextStyle( + 12, Color(0xFF343637), FontWeight.w500), + ), + SizedBox(height: 4), + Text( + "TS J8 8905", + style: fontTextStyle( + 12, Color(0xFF646566), FontWeight.w500), + ), + ], + ), + Spacer(), + Row(children: [ + Image.asset('images/message.png', width: 24, height: 24), + SizedBox(width: 16), + Image.asset('images/phone.png', width: 24, height: 24), + ]) + ], + ), + + SizedBox(height: 16), + + // ------------ TANK DROPDOWN ----------------- + buildTankDropdown(hasTankAlready, preSelectedTank), + + SizedBox(height: 16), + + // ------------ OTP Boxes ----------------- + if (selectedTankId != null || hasTankAlready) + buildStartOtp(), + + SizedBox(height: 16), + + if (unload_complete_otp != '') buildStopOtp(), + + SizedBox(height: 16), + + // ------------ ORDER SUMMARY ----------------- + buildOrderSummary(), + + SizedBox(height: 16), + + // ------------ CHECKLIST ----------------- + buildChecklist(), + ], + ), + ), + ); + } + + // ------------------------------------------- + // DROPDOWN WITH BLOCKED LOGIC + // ------------------------------------------- + Widget buildTankDropdown(hasTankAlready, preSelectedTank) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(8), + ), + child: Column(children: [ + Row( + children: [ + Expanded( + child: Text( + hasTankAlready + ? "Change tank to unload water to generate OTP" + : "Choose a tank to unload water to generate OTP", + style: fontTextStyle(12, Color(0xFF444444), FontWeight.w600), + ), + ), + SizedBox(width: 8), + Expanded( + child: isLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5, + ), + ) + : _buildDropdownField( + hint: hasTankAlready + ? widget.orderDetails.tankName + : "Select Tank", + value: selectedTank ?? preSelectedTank, + items: sumpTanks, + + // πŸ”₯ OPTION B β€” TANK CHANGE BLOCKED HERE + onChanged: (GetTanksDetailsModel? tank) { + if (tank == null) return; + + // ❌ BLOCK AFTER UNLOADING STARTED + if (!allowTankChange) { + AppSettings.longFailedToast( + "Tank cannot be changed after unloading has started."); + return; + } + + // MOTOR CHECK + bool motorOn = false; + + if (tank.inConnections.isNotEmpty) { + motorOn = tank.inConnections.any( + (conn) => conn['motor_status']?.toString() == '2'); + } + + if (!motorOn && tank.outConnections.isNotEmpty) { + motorOn = tank.outConnections.any( + (conn) => conn['motor_status']?.toString() == '2'); + } + + if (motorOn) { + showMotorOnAlert(tank); + return; + } + + showTankConfirmDialog(tank); + }, + ), + ), + ], + ) + ]), + ); + } + + // ------------------------------------------- + // ALERT WHEN MOTOR ON + // ------------------------------------------- + void showMotorOnAlert(GetTanksDetailsModel tank) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + backgroundColor: Colors.white, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Center( + child: Text( + "The motor of \"${tank.tank_name}\" is turned on. Please turn it off to start unloading.", + style: fontTextStyle(14, Colors.black, FontWeight.w500), + textAlign: TextAlign.center, + ), + ), + actions: [ + Center( + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFF1D7AFC)), + borderRadius: BorderRadius.circular(24), + ), + child: Text("Cancel", + style: fontTextStyle( + 12, Color(0xFF1D7AFC), FontWeight.w600)), + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + setState(() => selectedTank = tank); + }, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Color(0xFFC03D34), + borderRadius: BorderRadius.circular(24), + ), + child: Text( + "Stop Motor", + style: fontTextStyle( + 12, Colors.white, FontWeight.w600), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + // ------------------------------------------- + // CONFIRM TANK CHANGE + // ------------------------------------------- + void showTankConfirmDialog(GetTanksDetailsModel tank) { + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: Center( + child: Text( + "You have selected \"${tank.tank_name}\" for water unloading. Do you confirm?", + style: fontTextStyle(14, Colors.black, FontWeight.w500), + textAlign: TextAlign.center, + ), + ), + actions: [ + Center( + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0xFF1D7AFC)), + borderRadius: BorderRadius.circular(24), + ), + child: Text( + "Cancel", + style: fontTextStyle( + 12, Color(0xFF1D7AFC), FontWeight.w600), + ), + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () async { + await _updateTankName(tank.tank_name); + setState(() { + selectedTank = tank; + selectedTankId = tank.tank_name; + }); + Navigator.pop(context); + }, + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Color(0xFF1D7AFC), + borderRadius: BorderRadius.circular(24), + ), + child: Text( + "Confirm", + style: fontTextStyle( + 12, Colors.white, FontWeight.w600), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + // ------------------------------------------- + // OTP BOXES + // ------------------------------------------- + Widget buildStartOtp() { + return Container( + decoration: BoxDecoration( + color: Color(0xFFE8F2FF), + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: Text("Share the OTP to start unloading", + style: fontTextStyle( + 12, Color(0xFF444444), FontWeight.w600))), + Expanded(child: buildOtpBoxes(otp)), + ], + ), + ); + } + + Widget buildStopOtp() { + return Container( + decoration: BoxDecoration( + color: Color(0xFFE8F2FF), + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: Text("Share the OTP to stop unloading", + style: fontTextStyle( + 12, Color(0xFF444444), FontWeight.w600))), + Expanded(child: buildOtpBoxes(unload_complete_otp)), + ], + ), + ); + } + + Row buildOtpBoxes(String otp) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: (otp.isNotEmpty ? otp : "----") + .split('') + .map((digit) { + return Container( + width: 28, + height: 28, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 1), + decoration: BoxDecoration( + color: Color(0xFF4692FD), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + digit, + style: fontTextStyle(12, Colors.white, FontWeight.w500), + ), + ); + }).toList(), + ); + } + + // ------------------------------------------- + // ORDER SUMMARY + // ------------------------------------------- + Widget buildOrderSummary() { + return Column( + children: [ + InkWell( + onTap: () => setState(() => _showOrderSummary = !_showOrderSummary), + child: Row( + children: [ + Text("View Order Summary", + style: fontTextStyle( + 14, Color(0xFF1D7AFC), FontWeight.w600)), + Spacer(), + Image.asset( + _showOrderSummary + ? 'images/arrow-up.png' + : 'images/arrow-down.png', + width: 24, + height: 24, + color: Color(0xFF1D7AFC), + ) + ], + ), + ), + if (_showOrderSummary) + AnimatedContainer( + duration: Duration(milliseconds: 300), + child: Card( + color: Colors.white, + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("ITEMS", + style: fontTextStyle( + 12, Color(0xFF444444), FontWeight.w700)), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("10,000 L Drinking water x 1", + style: fontTextStyle( + 12, Color(0xFF515253), FontWeight.w400)), + Text("β‚Ή2,500", + style: fontTextStyle( + 12, Color(0xFF515253), FontWeight.w400)), + ], + ), + Divider(), + _priceRow("Item Total", "β‚Ή2,346.00"), + _priceRow("Delivery Charges", "β‚Ή150.00"), + _priceRow("Platform Fee", "β‚Ή6.00"), + _priceRow("Taxes", "β‚Ή12.49"), + Divider(), + _priceRow("Total Bill", "β‚Ή2,514", isBold: true), + Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Mode of Payment", + style: fontTextStyle( + 12, Color(0xFF444444), FontWeight.w500)), + Row( + children: [ + Image.asset('images/success-toast.png', + width: 12, height: 12), + SizedBox(width: 4), + Text("Cash on delivery", + style: fontTextStyle( + 12, Color(0xFF444444), FontWeight.w500)), + ], + ) + ], + ) + ], + ), + ), + ), + ) + ], + ); + } + + // ------------------------------------------- + // CHECKLIST + // ------------------------------------------- + Widget buildChecklist() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Arrival Checklist", + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + SizedBox(height: 16), + + _buildChecklistItem( + icon: Icons.play_circle_fill, + title: "Start unloading water", + subtitle: + "The status will be updated once the delivery agent starts unloading water.", + stepIndex: 0, + currentStep: currentStep, + selectedTankId: selectedTankId, + onTap: () {}, + ), + + _buildChecklistItem( + icon: Icons.check_circle, + title: "Complete unloading", + subtitle: + "The status will be updated after you confirm that unloading is complete.", + stepIndex: 1, + currentStep: currentStep, + selectedTankId: selectedTankId, + onTap: () {}, + ), + + _buildChecklistItem( + icon: Icons.receipt_long, + title: "View your bill", + subtitle: + "The bill will be calculated after unloading is complete.", + stepIndex: 2, + currentStep: currentStep, + selectedTankId: selectedTankId, + onTap: () {}, + ), + ], + ); + } + + // ------------------------------------------- + // HELPERS + // ------------------------------------------- + Widget _buildDropdownField({ + required String hint, + required GetTanksDetailsModel? value, + required List items, + String? path, + required ValueChanged onChanged, + }) { + return DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + hint: Row( + children: [ + if (path != null && path.isNotEmpty) + Image.asset(path, width: 16, height: 16), + SizedBox(width: 6), + Text(hint, + style: fontTextStyle( + 14, Color(0XFF939495), FontWeight.w400)), + ], + ), + value: value, + items: items.map((tank) { + return DropdownMenuItem( + value: tank, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Text(tank.tank_name, + style: fontTextStyle( + 14, Color(0xFF2D2E30), FontWeight.w400)), + ), + ); + }).toList(), + onChanged: onChanged, + buttonStyleData: ButtonStyleData( + height: 48, + padding: EdgeInsets.only(left: 0, right: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(12), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: EdgeInsets.only(right: 4), + child: Image.asset('images/arrow_down.png', + width: 16, height: 16, color: Color(0XFF939495)), + ), + ), + ), + ); + } +} + +Widget _buildChecklistItem({ + required IconData icon, + required String title, + required String subtitle, + required int stepIndex, + required int currentStep, + required VoidCallback onTap, + required String? selectedTankId, +}) { + bool isCompleted = currentStep > stepIndex; + bool isActive = currentStep == stepIndex; + + // πŸ”₯ Only allow clicking when this is the ACTIVE step AND it is step 2 + bool isClickable = (stepIndex == 2 && isActive); + + return GestureDetector( + onTap: isClickable ? onTap : null, + child: Opacity( + opacity: 1, + child: Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Icon( + (isActive || isCompleted) + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + color: (isActive || isCompleted) ? Colors.blue : Colors.grey, + ), + if (stepIndex < 2) + Container( + width: 2, + height: 50, + color: isCompleted ? Colors.blue : Colors.grey[300], + ), + ], + ), + + const SizedBox(width: 12), + + Expanded( + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF5F7FA), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + icon, + color: isCompleted ? Colors.blue : Colors.grey, + ), + const SizedBox(width: 10), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: isCompleted ? Colors.black : Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle(fontSize: 12, color: Colors.black54), + ), + ], + ), + ), + ], + ), + ), + ) + ], + ), + ), + ), + ); +} + +Widget _priceRow(String label, String amount, {bool isBold = false}) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, + style: fontTextStyle( + 12, + Color(0xFF444444), + isBold ? FontWeight.w600 : FontWeight.w400)), + Text(amount, + style: fontTextStyle( + 12, + Color(0xFF444444), + isBold ? FontWeight.w600 : FontWeight.w400)), + ], + ), + ); +} + +// ------------- FULL FILE ENDS HERE --------------- diff --git a/lib/supplier/order_details.dart b/lib/supplier/order_details.dart new file mode 100644 index 0000000..5550a67 --- /dev/null +++ b/lib/supplier/order_details.dart @@ -0,0 +1,651 @@ +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/supplier/payment_example2.dart'; +import 'package:watermanagement/supplier/payment_screen.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; + +class OrderDetails extends StatefulWidget { + var supplierDetails; + OrderDetails({this.supplierDetails}); + + @override + State createState() => _OrderDetailsState(); +} + +class _OrderDetailsState extends State { + bool isExpanded = false; + double actualPrice = 0.0; + int quantity = 0; + double deliveryCharges = 0.0; + double subtotal = 0.0; + double cgst = 0.0; + double sgst = 0.0; + double total = 0.0; + double platformFee = 0.0; + double totalTaxes = 0.0; + double advance = 0.0; + double balance = 0.0; + late Razorpay _razorpay; + + @override + void initState() { + super.initState(); + + actualPrice = + double.tryParse(widget.supplierDetails.quotedAmount ?? '0') ?? 0; + quantity = int.tryParse(widget.supplierDetails.quantity ?? '0') ?? 0; + deliveryCharges = double.tryParse(widget.supplierDetails.bookingCharges ?? '0') ?? 0;; + platformFee = 11; + subtotal = actualPrice * quantity; + cgst = subtotal * 0.09; + sgst = subtotal * 0.09; + totalTaxes = cgst + sgst; + total = subtotal + cgst + sgst + deliveryCharges + platformFee; + advance = deliveryCharges; + balance = total - advance; + _razorpay = Razorpay(); + _razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, (PaymentSuccessResponse response) { + _handleSuccess(response, widget.supplierDetails); + }); + _razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handleError); + _razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet); + } + + Widget _buildAddressRow(String label, String address, Color color) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Image.asset( + 'images/location_supplier_landing.png', + height: 24, + width: 24, + fit: BoxFit.contain, + color: Color(0XFF939495), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Text( + '$label ', + style: fontTextStyle(14, color, FontWeight.bold), + ), + Expanded( + child: Text( + address, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ) + ], + ), + ); + } + + Widget _itemRow(String title, String price, {bool bold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w400)), + Text(price, + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w400)), + ], + ), + ); + } + + + void _openCheckout(double amountInRupees) { + var options = { + 'key': 'rzp_test_1VCCWqEXUHdINz', // Replace with your Razorpay Key + 'amount': (amountInRupees * 100) + .toInt(), // Convert rupees to paise and ensure integer + 'name': 'Test Payment', + 'description': 'Water Order', + 'prefill': { + 'contact': '9876543210', + 'email': 'test@example.com', + } + }; + + try { + _razorpay.open(options); + } catch (e) { + print('Error: $e'); + } + } + + /* void _handleSuccess(PaymentSuccessResponse response) { + _showAlert('Payment Successful', 'Payment ID: ${response.paymentId}'); + }*/ + + void _handleSuccess(PaymentSuccessResponse response, dynamic object) async { + if (!mounted) return; // ensure widget is still active + + var payload = {}; + payload["supplierId"] = object.supplierId; + payload["action"] = 'accepted'; + payload["paymentId"] = response.paymentId; + + try { + bool status = await AppSettings.acceptOrderRequests(payload, object.dbId); + + if (!mounted) return; + + if (status) { + AppSettings.longSuccessToast('Accepted'); + + Navigator.pop(context, true); + } else { + AppSettings.longFailedToast('Failed to accept order request'); + } + } catch (e) { + AppSettings.longFailedToast('Error: $e'); + } + } + + void _handleError(PaymentFailureResponse response) { + _showAlert('Payment Failed', '${response.code} - ${response.message}'); + } + + void _handleExternalWallet(ExternalWalletResponse response) { + _showAlert('Wallet Selected', '${response.walletName}'); + } + + void _showAlert(String title, String content) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text(title), + content: Text(content), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('OK')) + ], + ), + ); + } + + @override + void dispose() { + _razorpay.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: AppSettings.supplierAppBarWithoutActions( + widget.supplierDetails.supplierName, context), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + children: [ + _buildAddressRow(widget.supplierDetails.supplierName, + '| ' + widget.supplierDetails.address, Color(0XFF4692FD)), + Padding( + padding: const EdgeInsets.only( + left: 8.0, bottom: 4.0), // πŸ‘ˆ Shift right + child: Align( + alignment: Alignment.centerLeft, + child: Image.asset( + 'images/Line.png', + height: 12, + width: 6, + fit: BoxFit.contain, + ), + ), + ), + _buildAddressRow('Home', '| ' + AppSettings.userAddress, + Color(0XFF2D2E30)), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(8), + top: Radius.circular(8), + ), // match Card border + gradient: LinearGradient( + colors: [ + Color(0xFFFFF8DF), + Color(0xFFFFFFFF), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + widget + .supplierDetails.supplierName, + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600)), + Text( + widget.supplierDetails + .distanceInMeters + .toString() + + ' Km', + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w600)), + ], + ), + Text( + widget.supplierDetails.displayAddress, + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w400)), + ], + ))), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + color: Color(0XFFF5F6F6), // Same as Card color + borderRadius: + BorderRadius.circular(4), // Rounded corners + border: Border.all( + color: Color(0XFFF5F6F6), // Border color + width: 0.5, // Border width + ), + ), + padding: EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Text('ITEMS', + style: fontTextStyle(10, Color(0XFF646566), + FontWeight.w400)), + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Padding( + padding: EdgeInsets.fromLTRB(4, 0, 4, 0), + child: Text( + widget.supplierDetails.capacity + + ' - ' + + widget.supplierDetails.typeofwater, + style: fontTextStyle( + 12, Color(0XFF2D2E30), FontWeight.w500), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + color: Color(0XFFF5F6F6), // Same as Card color + borderRadius: + BorderRadius.circular(4), // Rounded corners + border: Border.all( + color: Color(0XFFF5F6F6), // Border color + width: 0.5, // Border width + ), + ), + padding: EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Text('DELIVERY DETAILS', + style: fontTextStyle(10, Color(0XFF646566), + FontWeight.w400)), + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Padding( + padding: EdgeInsets.fromLTRB(4, 0, 4, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Date', + style: fontTextStyle(12, + Color(0XFF515253), FontWeight.w500), + ), + Text( + widget.supplierDetails.date, + style: fontTextStyle(12, + Color(0XFF232527), FontWeight.w500), + ) + ], + ), + SizedBox( + height: + MediaQuery.of(context).size.height * .012, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Time', + style: fontTextStyle(12, + Color(0XFF515253), FontWeight.w500), + ), + Text( + widget.supplierDetails.time, + style: fontTextStyle(12, + Color(0XFF232527), FontWeight.w500), + ) + ], + ) + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Align( + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + color: + Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular( + 4), // Rounded corners + border: Border.all( + color: Color(0XFF939495), // Border color + width: 0.5, // Border width + ), + ), + padding: EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Text('Change Delivery Details', + style: fontTextStyle(14, + Color(0XFF646566), FontWeight.w500)), + )), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + ], + ), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 12, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text('SAVINGS', + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400)), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Row( + children: [ + Image.asset( + 'images/coupon.png', + height: 16, + width: 16, + fit: BoxFit.contain, + ), + SizedBox( + width: MediaQuery.of(context).size.width * + .012), + Text( + 'Apply Coupon', + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w500, + ), + ), + Spacer(), // pushes the arrow to the right + Image.asset( + 'images/arrow-right.png', + height: 20, + width: 20, + fit: BoxFit.contain, + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + ], + ), + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Column( + children: [ + // Header card + GestureDetector( + onTap: () => setState(() => isExpanded = !isExpanded), + child: Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 12, 12, 12), + child: Row( + children: [ + Image.asset( + 'images/receipt.png', + height: 16, + width: 16, + fit: BoxFit.contain, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * .012, + ), + Expanded( + child: Text( + 'To Pay β‚Ή${AppSettings.moneyConvertion(total.toString())}', + style: fontTextStyle(12, + Color(0XFF2E302D), FontWeight.w600)), + ), + isExpanded + ? Image.asset( + 'images/arrow-up.png', + height: 20, + width: 20, + fit: BoxFit.contain, + ) + : Image.asset( + 'images/arrow-down.png', + height: 20, + width: 20, + fit: BoxFit.contain, + ) + ], + ), + ), + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: isExpanded ? null : 0, + padding: + isExpanded ? EdgeInsets.all(0) : EdgeInsets.zero, + child: isExpanded + ? Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(8), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text('ITEMS', + style: fontTextStyle( + 14, + Color(0XFF2D2E30), + FontWeight.w500)), + SizedBox( + height: + MediaQuery.of(context).size.height * + .012, + ), + _itemRow( + widget.supplierDetails.capacity + + ' ' + + widget + .supplierDetails.typeofwater + + ' x ' + + widget.supplierDetails.quantity, + 'β‚Ή' + + AppSettings.moneyConvertion( + subtotal.toString())), + Divider(), + _itemRow('Item Total', + 'β‚Ή${AppSettings.moneyConvertion(total.toString())}'), + _itemRow('Delivery Charges', + 'β‚Ή${AppSettings.moneyConvertion(deliveryCharges.toString())}'), + _itemRow('Platform Fee', 'β‚Ή${AppSettings.moneyConvertion(platformFee.toString())}'), + _itemRow('Taxes', 'β‚Ή${AppSettings.moneyConvertion(totalTaxes.toString())}'), + Divider(), + _itemRow('Total Bill', + 'β‚Ή${AppSettings.moneyConvertion(total.toString())}', + bold: true), + Divider(), + _itemRow('Advance', 'β‚Ή${AppSettings.moneyConvertion(advance.toString())}'), + _itemRow('To pay(after delivery)', + 'β‚Ή${AppSettings.moneyConvertion(balance.toString())}'), + ], + ), + ), + ) + : null, + ), + ], + ) + ], + )), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide(color: Color(0XFFC3C4C4), width: 0.8), + ), + boxShadow: [ + BoxShadow( + color: Color(0XFFC3C4C4), + blurRadius: 0, + offset: Offset(0, 0), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + // Button 1: Pay Advance + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF0A9E04), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.symmetric(vertical: 12), // Only vertical padding + ), + onPressed: () { + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RazorpayScreen()), + );*/ + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PaymentOptionsPage(amount: advance), + ), + );*/ + _openCheckout(advance); + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UpiIntentLauncher(), + ), + );*/ + }, + child: Text( + "Pay advance β‚Ή${AppSettings.moneyConvertion(advance.toString())}", + style: fontTextStyle(14, Color(0XFFFFFFFF), FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ),// spacing between buttons + // Button 2: Pay Full + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF1D7AFC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.symmetric(vertical: 12), + ), + onPressed: () { + // Handle full payment + }, + child: Text( + "Pay full β‚Ή${AppSettings.moneyConvertion(total.toStringAsFixed(2))}", + style: fontTextStyle(14, Color(0XFFFFFFFF), FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + + ); + } +} diff --git a/lib/supplier/order_requests.dart b/lib/supplier/order_requests.dart new file mode 100644 index 0000000..104cd96 --- /dev/null +++ b/lib/supplier/order_requests.dart @@ -0,0 +1,812 @@ +import 'dart:convert'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/supplier/order_details.dart'; + +import '../common/settings.dart'; +import 'supplier_orders_model.dart'; + +class OrderRequestsPage extends StatefulWidget { + const OrderRequestsPage({super.key}); + + @override + State createState() => _OrderRequestsPageState(); +} + +class _OrderRequestsPageState extends State { + bool isSupplierOrdersDataLoading = false; + List supplierOrdersList = []; + List acceptedOrders = []; + List acceptedByUserOrders = []; + List rejectedOrders = []; + + void groupOrdersByStatus() { + acceptedOrders = supplierOrdersList + .where((order) => order.status == "accept") // adjust field name + .toList(); + + acceptedByUserOrders = supplierOrdersList + .where( + (order) => order.status == "accepted_by_user") // adjust field name + .toList(); + rejectedOrders = supplierOrdersList + .where( + (order) => order.status == "rejected_by_user") // adjust field name + .toList(); + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + getAcceptedOrdersFromSupplier(); + } + + Future getAcceptedOrdersFromSupplier() async { + isSupplierOrdersDataLoading = true; + var response1 = await AppSettings.getAcceptedOrdersDataFromSupplier(); + var json = jsonDecode(response1); + + setState(() { + supplierOrdersList = (json['data'] as List) + .map((model) => SupplierOrdersModel.fromJson(model)) + .toList(); + groupOrdersByStatus(); + isSupplierOrdersDataLoading = false; + }); + } + + String getOrderTimeOnly(String orderTimeStr) { + if (orderTimeStr.isEmpty) return ""; + + try { + final format = DateFormat("dd-MM-yyyy HH:mm"); + final orderTime = format.parse(orderTimeStr); + final now = DateTime.now(); + final difference = now.difference(orderTime); + + if (difference.inDays < 2) { + return "New"; + } else if (difference.inDays < 10) { + final remaining = 10 - difference.inDays; + return "Expires in ${remaining}d"; + } else { + return "Expired"; + } + } catch (e) { + return ""; + } + } + + showRejectOrdersDialog(var object) async { + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: Color(0XFFFFFFFF),// Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center( + child: Text('Reject Order Request' ,style: fontTextStyle(16,Color(0XFF3B3B3B),FontWeight.w600),), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text('Do u want to reject order request',style: fontTextStyle(14,Color(0XFF101214),FontWeight.w600),), + ), + ], + ), + ), + actions: [ + Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded(child: GestureDetector( + onTap: (){ + Navigator.pop(context); + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, + color: Color(0XFF1D7AFC)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16,12,16,12), + child: Text('Cancel', style: fontTextStyle(12, Color(0XFF1D7AFC), FontWeight.w600)), + ), + ), + ), + ),), + SizedBox(width:MediaQuery.of(context).size.width * .016,), + Expanded(child: GestureDetector( + onTap: ()async{ + var payload = {}; + payload["supplierId"] = object.supplierId; + payload["action"] = 'reject'; + payload["paymentId"] = ''; + + try { + bool status = await AppSettings.acceptOrderRequests(payload, object.dbId); + + if (!mounted) return; + + if (status) { + AppSettings.longSuccessToast('Rejected'); + Navigator.pop(context); + getAcceptedOrdersFromSupplier(); + } else { + AppSettings.longFailedToast('Failed to reject order request'); + } + } catch (e) { + AppSettings.longFailedToast('Error: $e'); + } + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFE2483D), + border: Border.all( + width: 1, + color: Color(0XFFE2483D)), + borderRadius: BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Visibility( + visible: true, + child: Padding( + padding: EdgeInsets.fromLTRB(16,12,16,12), + child: Text( + 'Reject', + style: fontTextStyle( + 12, + Color(0XFFFFFFFF), + FontWeight.w600)), + ), + ), + ) + ),) + + + ], + ), + ), + ], + ); + }); + }, + ); + } + + Widget orderCard(SupplierOrdersModel order) { + final orderDateTime = "${order.acceptedTime}".trim(); + final statusTime = getOrderTimeOnly(orderDateTime); + + // status badge color + Color statusColor = Color(0xFF1D7AFC); + if (statusTime == "Expired") + statusColor = Color(0xFF757575); + else if (statusTime == "New") + statusColor = Color(0XFF1D7AFC); + else if (statusTime.endsWith("m")) statusColor = Color(0XFFE56910); + + return Padding( + padding: EdgeInsets.all(10), + child: Container( + decoration: BoxDecoration( + color: Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular(12), // Rounded corners + border: Border.all( + color: Color(0XFF9F9F9F), // Border color + width: 0.5, // Border width + ), + ), + //color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.supplierName != '', + child: Text( + order.supplierName, + style: fontTextStyle( + 14, Color(0XFF343637), FontWeight.w600), + ), + ), + Visibility( + visible: statusTime.isNotEmpty, + child: Padding( + padding: const EdgeInsets.only(top: 4.0, bottom: 4.0), + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: statusColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + statusTime, + style: fontTextStyle( + 10, Colors.white, FontWeight.w500), + ), + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.displayAddress != '', + child: Text( + order.displayAddress, + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + Visibility( + visible: order.date != '', + child: Text( + order.date + ' ' + order.time, + style: fontTextStyle( + 9, Color(0XFF646566), FontWeight.w400), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + padding: + EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: order.typeofwater.toString().toLowerCase() == + 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 1, + color: + order.typeofwater.toString().toLowerCase() == + 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + ), + ), + child: AutoSizeText( + capitalizeFirst(order.typeofwater), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle( + 12, Color(0xFFFFFFFF), FontWeight.w400), + ), + ), + Row( + children: [ + Text( + 'β‚Ή' + + AppSettings.formDouble(order.quotedAmount) + + '/', + style: fontTextStyle( + 20, Color(0XFF515253), FontWeight.w600), + ), + Text( + order.capacity, + style: fontTextStyle( + 12, Color(0XFF2D2E30), FontWeight.w600), + ) + ], + ), + ], + ), + Row( + children: [ + GestureDetector( + onTap: statusTime == "Expired" + ? null + : () { + showRejectOrdersDialog(order); + }, + child: ColorFiltered( + colorFilter: statusTime == "Expired" + ? ColorFilter.mode( + Color(0XFF9F9F9F), BlendMode.srcIn) + : ColorFilter.mode( + Colors.transparent, BlendMode.multiply), + child: Image.asset( + 'images/wrong_button.png', + width: 44, + height: 44, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + GestureDetector( + onTap: statusTime == "Expired" + ? null + : () { + //showAcceptOrdersDialog(order); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OrderDetails( + supplierDetails: order, + ), + ), + ); + }, + child: ColorFiltered( + colorFilter: statusTime == "Expired" + ? ColorFilter.mode( + Color(0XFF9F9F9F), BlendMode.srcIn) + : ColorFilter.mode( + Colors.transparent, BlendMode.multiply), + child: Image.asset( + 'images/right_button.png', + width: 44, + height: 44, + ), + ), + ), + ], + ) + ], + ) + ], + )), + ), + ); + } + + Widget acceptedOrdersCard(SupplierOrdersModel order) { + final orderDateTime = "${order.acceptedTime}".trim(); + final statusTime = getOrderTimeOnly(orderDateTime); + + // status badge color + Color statusColor = Color(0xFF1D7AFC); + if (statusTime == "Expired") + statusColor = Color(0xFF757575); + else if (statusTime == "New") + statusColor = Color(0XFF1D7AFC); + else if (statusTime.endsWith("m")) statusColor = Color(0XFFE56910); + + return Padding( + padding: EdgeInsets.all(10), + child: Container( + decoration: BoxDecoration( + color: Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular(12), // Rounded corners + border: Border.all( + color: Color(0XFF9F9F9F), // Border color + width: 0.5, // Border width + ), + ), + //color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.supplierName != '', + child: Text( + order.supplierName, + style: fontTextStyle( + 14, Color(0XFF343637), FontWeight.w600), + ), + ), + Visibility( + visible: order.date != '', + child: Text( + order.date + ' ' + order.time, + style: fontTextStyle( + 9, Color(0XFF646566), FontWeight.w400), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.displayAddress != '', + child: Text( + order.displayAddress, + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + Row( + children: [ + Image.asset( + 'images/paid.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * + .004, + ), + Text(capitalizeFirst('Paid'), style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400)), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Text( + 'β‚Ή' + AppSettings.formDouble(order.advancePaid), + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w400), + ), + ], + ) + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .004, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: order.typeofwater.toString().toLowerCase() == 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 1, + color: order.typeofwater.toString().toLowerCase() == 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + ), + ), + child: AutoSizeText( + capitalizeFirst(order.typeofwater), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0xFFFFFFFF), FontWeight.w400), + ), + ), + Row( + children: [ + Image.asset( + 'images/warning.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * + .004, + ), + Text(capitalizeFirst('Due'), style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400)), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Text( + 'β‚Ή' + AppSettings.formDouble(order.advancePaid), + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w400), + ), + ], + ) + ], + ), + Row( + children: [ + Text( + 'β‚Ή' + AppSettings.formDouble(order.quotedAmount) + '/', + style: fontTextStyle(20, Color(0XFF515253), FontWeight.w600), + ), + Text( + order.capacity, + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w600), + ), + ], + ), + ], + ), + ), + ], + ) + ], + )), + ), + ); + } + + Widget rejectedOrdersCard(SupplierOrdersModel order) { + final orderDateTime = "${order.acceptedTime}".trim(); + final statusTime = getOrderTimeOnly(orderDateTime); + + // status badge color + Color statusColor = Color(0xFF1D7AFC); + if (statusTime == "Expired") + statusColor = Color(0xFF757575); + else if (statusTime == "New") + statusColor = Color(0XFF1D7AFC); + else if (statusTime.endsWith("m")) statusColor = Color(0XFFE56910); + + return Padding( + padding: EdgeInsets.all(10), + child: Container( + decoration: BoxDecoration( + color: Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular(12), // Rounded corners + border: Border.all( + color: Color(0XFF9F9F9F), // Border color + width: 0.5, // Border width + ), + ), + //color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.supplierName != '', + child: Text( + order.supplierName, + style: fontTextStyle( + 14, Color(0XFF343637), FontWeight.w600), + ), + ), + Visibility( + visible: order.date != '', + child: Text( + order.date + ' ' + order.time, + style: fontTextStyle( + 9, Color(0XFF646566), FontWeight.w400), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Visibility( + visible: order.displayAddress != '', + child: Text( + order.displayAddress, + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .004, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: order.typeofwater.toString().toLowerCase() == 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 1, + color: order.typeofwater.toString().toLowerCase() == 'bore water' + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + ), + ), + child: AutoSizeText( + capitalizeFirst(order.typeofwater), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0xFFFFFFFF), FontWeight.w400), + ), + ), + ], + ), + Row( + children: [ + Text( + 'β‚Ή' + AppSettings.formDouble(order.quotedAmount) + '/', + style: fontTextStyle(20, Color(0XFF515253), FontWeight.w600), + ), + Text( + order.capacity, + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w600), + ), + ], + ), + ], + ), + ), + ], + ) + ], + )), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: + AppSettings.supplierAppBarWithoutActions('Order Requests', context), + body: Center( + child: isSupplierOrdersDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : supplierOrdersList.length != 0 + ? Column( + children: [ + Expanded( + child: ListView( + children: [ + // Accepted by User Orders + if (acceptedByUserOrders.isNotEmpty) ...[ + Padding( + padding: EdgeInsets.all(10), + child: Text( + "ACCEPTED REQUESTS", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + ...acceptedByUserOrders + .map((order) => acceptedOrdersCard(order)) + .toList(), + ], + + // Group acceptedOrders + if (acceptedOrders.isNotEmpty) ...[ + // Active / New Orders + if (acceptedOrders.any((o) => + getOrderTimeOnly(o.acceptedTime) != + "Expired")) ...[ + Padding( + padding: EdgeInsets.all(10), + child: Text( + "NEW REQUESTS", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + ...acceptedOrders + .where((o) => + getOrderTimeOnly(o.acceptedTime) != + "Expired") + .map((order) => orderCard(order)) + .toList(), + ], + + // Expired Orders + if (acceptedOrders.any((o) => + getOrderTimeOnly(o.acceptedTime) == + "Expired")) ...[ + Padding( + padding: EdgeInsets.all(10), + child: Text( + "EXPIRED REQUESTS", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + ...acceptedOrders + .where((o) => + getOrderTimeOnly(o.acceptedTime) == + "Expired") + .map((order) => orderCard(order)) + .toList(), + ], + ], + + //rejected orders] + if (rejectedOrders.isNotEmpty) ...[ + Padding( + padding: EdgeInsets.all(10), + child: Text( + "REJECTED REQUESTS", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + ...rejectedOrders + .map((order) => rejectedOrdersCard(order)) + .toList(), + ], + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .010, + ), + ], + ) + : Center( + child: Text( + 'No Data Available', + style: + fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + ), + ), + ); + } +} diff --git a/lib/supplier/order_status_overlay.dart b/lib/supplier/order_status_overlay.dart new file mode 100644 index 0000000..5e1d0e8 --- /dev/null +++ b/lib/supplier/order_status_overlay.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import '../supplier/my_orders_model.dart'; +import '../common/settings.dart'; + +class OrderStatusOverlayMulti extends StatelessWidget { + final bool isVisible; + final List activeOrders; + final void Function(MyOrdersModel order)? onTap; + + const OrderStatusOverlayMulti({ + super.key, + required this.isVisible, + required this.activeOrders, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + if (!isVisible || activeOrders.isEmpty) return const SizedBox.shrink(); + + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: SizedBox( + height: 67, // Enough height for full details + width: double.infinity, + child: PageView.builder( + itemCount: activeOrders.length, + controller: PageController(viewportFraction: 1.0), + itemBuilder: (context, index) { + final order = activeOrders[index]; + return GestureDetector( + onTap: () => onTap?.call(order), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Color(0XFF0A9E04), + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Supplier name + /* Text( + order.supplierName, + style: const TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), +*/ + /* /// Address + Text( + order.displayAddress, + style: const TextStyle( + color: Colors.black54, + fontSize: 12, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8),*/ + + /// Row for type & price + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + order.typeofwater, + style: fontTextStyle(12,Color(0XFFFFFFFF),FontWeight.w600), + ), + Text( + order.capacity, + style: fontTextStyle(12,Color(0XFFFFFFFF),FontWeight.w600), + ), + + ], + ), + const SizedBox(height: 6), + + /// Optional date or status + if (order.price != '' && order.price.isNotEmpty) + Text( + "β‚Ή ${order.price}", + style: fontTextStyle(12,Color(0XFFFFFFFF),FontWeight.w600), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/supplier/order_tracking_page.dart b/lib/supplier/order_tracking_page.dart new file mode 100644 index 0000000..2d39608 --- /dev/null +++ b/lib/supplier/order_tracking_page.dart @@ -0,0 +1,673 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; +import 'package:get/get.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:location/location.dart'; +import 'package:http/http.dart' as http; +import '../common/settings.dart'; +import '../maps/location_controller.dart'; +import 'dart:ui' as ui; +import 'package:flutter_compass/flutter_compass.dart'; + +import 'order_arrived.dart'; + +class OrderTrackingPage extends StatefulWidget { + var orderDetails; + final double lat; + final double lng; + final double d_lat; + final double d_lng; + final String u_address; + + OrderTrackingPage({ + required this.lat, + required this.lng, + required this.d_lat, + required this.d_lng, + required this.u_address, + this.orderDetails + }); + + @override + State createState() => _OrderTrackingPageState(); +} + +class _OrderTrackingPageState extends State { + final Completer _mapController = Completer(); + final Location _location = Location(); + late StreamSubscription _locationSubscription; + StreamSubscription? _compassSubscription; + LocationData? _currentLocation; + bool _showOrderSummary = false; + BitmapDescriptor? truckIcon; + BitmapDescriptor? destinationIcon; + + Set _markers = {}; + Map _polylines = {}; + List _routeCoords = []; + + String _eta = ''; + double _distance = 0.0; + double _truckRotation = 0.0; + + final String _googleApiKey = 'AIzaSyDJpK9RVhlBejtJu9xSGfneuTN6HOfJgSM'; + + late LatLng userLatLng; + late LatLng driverLatLng; + + // ----------------------- NEWLY ADDED ----------------------- + bool _arrivalTriggered = false; + Timer? _arrivalTimer; + // ------------------------------------------------------------ + Timer? _autoNavigateTimer; + + @override + void initState() { + super.initState(); + userLatLng = LatLng(widget.lat, widget.lng); + driverLatLng = LatLng(widget.d_lat, widget.d_lng); + _loadIcons().then((_) => _initLocationTracking()); + _initCompass(); + + // πŸ‘‰ Auto navigate after 20 seconds + _autoNavigateTimer = Timer(Duration(seconds: 5), () { + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => ArrivalScreen( + orderDetails: widget.orderDetails, + ), + ), + ); + } + }); + } + + + + void _initCompass() { + _compassSubscription = FlutterCompass.events?.listen((event) { + if (!mounted) return; // πŸ›‘ lifecycle guard + + final heading = event.heading; + if (heading == null) return; + + setState(() { + _truckRotation = heading; + }); + }); + } + + @override + void dispose() { + try { _locationSubscription.cancel(); } catch (_) {} + _compassSubscription?.cancel(); + _arrivalTimer?.cancel(); + _autoNavigateTimer?.cancel(); + super.dispose(); + } + + Future _loadIcons() async { + truckIcon = + await getResizedMarker('images/tanker_map_horizontal.png', 90, null); + destinationIcon = await getResizedMarker( + 'images/location_supplier_landing.png', 90, Color(0XFFE76960)); + } + + Future _initLocationTracking() async { + bool serviceEnabled = await _location.serviceEnabled(); + if (!serviceEnabled) serviceEnabled = await _location.requestService(); + if (!serviceEnabled) return; + + PermissionStatus permissionGranted = await _location.hasPermission(); + if (permissionGranted == PermissionStatus.denied) { + permissionGranted = await _location.requestPermission(); + if (permissionGranted != PermissionStatus.granted) return; + } + + _currentLocation = await _location.getLocation(); + _updateRouteAndMarkers(); + + _locationSubscription = _location.onLocationChanged.listen((newLoc) { + if (!mounted) return; // πŸ›‘ REQUIRED + _currentLocation = newLoc; + _updateRouteAndMarkers(); + }); + } + + Future getResizedMarker( + String imagePath, int width, Color? tintColor) async { + final ByteData data = await rootBundle.load(imagePath); + final Uint8List imageData = data.buffer.asUint8List(); + + final ui.Codec codec = await ui.instantiateImageCodec( + imageData, + targetWidth: width, + ); + final ui.FrameInfo fi = await codec.getNextFrame(); + + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final Paint paint = Paint(); + + if (tintColor != null) { + paint.colorFilter = ui.ColorFilter.mode(tintColor, BlendMode.srcIn); + } + + canvas.drawImage(fi.image, Offset.zero, paint); + + final ui.Image finalImage = + await recorder.endRecording().toImage(fi.image.width, fi.image.height); + final ByteData? byteData = + await finalImage.toByteData(format: ui.ImageByteFormat.png); + + return BitmapDescriptor.fromBytes(byteData!.buffer.asUint8List()); + } + + // ----------------------- NEWLY ADDED METHOD ----------------------- + void _checkDriverArrival() { + if (_arrivalTriggered) return; + + double dist = _calculateDistance( + driverLatLng.latitude, + driverLatLng.longitude, + userLatLng.latitude, + userLatLng.longitude, + ); + + if (dist <= 0.05) { + _arrivalTriggered = true; + + _arrivalTimer = Timer(Duration(minutes: 1), () { + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => ArrivalScreen(orderDetails: widget.orderDetails,)), + ); + } + }); + } + } + // ------------------------------------------------------------ + + Future _updateRouteAndMarkers() async { + if (_currentLocation == null) return; + + await _fetchPolyline(driverLatLng, userLatLng); + await _fetchETA(driverLatLng, userLatLng); + + setState(() { + _markers = { + Marker( + markerId: MarkerId('truck'), + position: driverLatLng, + icon: truckIcon ?? BitmapDescriptor.defaultMarker, + rotation: _truckRotation, + anchor: Offset(0.5, 0.5), + flat: true, + ), + Marker( + markerId: MarkerId('destination'), + position: userLatLng, + icon: destinationIcon ?? BitmapDescriptor.defaultMarker, + infoWindow: InfoWindow(title: widget.u_address), + ), + }; + }); + + final controller = await _mapController.future; + LatLngBounds bounds = LatLngBounds( + southwest: LatLng( + min(driverLatLng.latitude, userLatLng.latitude), + min(driverLatLng.longitude, userLatLng.longitude), + ), + northeast: LatLng( + max(driverLatLng.latitude, userLatLng.latitude), + max(driverLatLng.longitude, userLatLng.longitude), + ), + ); + controller.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80)); + + // ----------------------- NEWLY ADDED ----------------------- + _checkDriverArrival(); + // ------------------------------------------------------------ + } + + Future _fetchPolyline(LatLng from, LatLng to) async { + PolylinePoints polylinePoints = PolylinePoints(); + PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( + _googleApiKey, + PointLatLng(from.latitude, from.longitude), + PointLatLng(to.latitude, to.longitude), + travelMode: TravelMode.driving, + ); + + if (result.points.isNotEmpty) { + _routeCoords = + result.points.map((p) => LatLng(p.latitude, p.longitude)).toList(); + + double totalDistance = 0; + for (int i = 0; i < _routeCoords.length - 1; i++) { + totalDistance += _calculateDistance( + _routeCoords[i].latitude, + _routeCoords[i].longitude, + _routeCoords[i + 1].latitude, + _routeCoords[i + 1].longitude, + ); + } + + _distance = totalDistance; + + setState(() { + _polylines = { + PolylineId('route'): Polyline( + polylineId: PolylineId('route'), + points: _routeCoords, + color: primaryColor, + width: 4, + patterns: [ + PatternItem.dash(20), + PatternItem.gap(0), + ], + jointType: JointType.round, + ) + }; + }); + } + } + + Future _fetchETA(LatLng from, LatLng to) async { + final url = Uri.parse( + 'https://maps.googleapis.com/maps/api/directions/json?origin=${from.latitude},${from.longitude}&destination=${to.latitude},${to.longitude}&key=$_googleApiKey', + ); + + final response = await http.get(url); + if (response.statusCode == 200) { + final data = json.decode(response.body); + String duration = data['routes'][0]['legs'][0]['duration']['text']; + setState(() { + _eta = duration; + }); + } + } + + double _calculateDistance(lat1, lon1, lat2, lon2) { + var p = 0.017453292519943295; + var a = 0.5 - + cos((lat2 - lat1) * p) / 2 + + cos(lat1 * p) * cos(lat2 * p) * + (1 - cos((lon2 - lon1) * p)) / 2; + return 12742 * asin(sqrt(a)); + } + + Widget _priceRow(String label, String amount, {bool isBold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: fontTextStyle( + 12, + Color(0xFF444444), + isBold ? FontWeight.w600 : FontWeight.w400, + ), + ), + Text( + amount, + style: fontTextStyle( + 12, + Color(0xFF444444), + isBold ? FontWeight.w600 : FontWeight.w400, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppSettings.supplierAppBarWithoutActions( + 'Order#${widget.orderDetails.bookingid}', context), + body: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + height: MediaQuery.of(context).size.height * 0.6, + child: GoogleMap( + initialCameraPosition: CameraPosition( + target: driverLatLng, + zoom: 14, + ), + myLocationEnabled: true, + zoomControlsEnabled: false, + markers: _markers, + polylines: Set.of(_polylines.values), + onMapCreated: (controller) => + _mapController.complete(controller), + ), + ), + + DraggableScrollableSheet( + initialChildSize: 0.38, + minChildSize: 0.38, + maxChildSize: 0.70, + builder: (context, scrollController) { + return Container( + padding: EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.vertical(top: Radius.circular(16)), + ), + child: ListView( + controller: scrollController, + padding: EdgeInsets.only(top: 10), + children: [ + Center( + child: Container( + width: 53, + height: 2, + margin: EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Color(0XFF757575), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + Center( + child: Column( + children: [ + Text( + "Arriving in $_eta", + style: fontTextStyle( + 16, + Color(0xFF232527), + FontWeight.w800, + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .004, + ), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "HOME", + style: fontTextStyle( + 11, Color(0xFF343637), FontWeight.w500), + ), + TextSpan( + text: " - ${AppSettings.userAddress}", + style: fontTextStyle( + 11, Color(0xFF343637), FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Divider(color: Color(0xFFC3C4C4), thickness: 1), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Color(0XFFE8F2FF), + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: 50, + height: 50, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Prashanth", + style: fontTextStyle( + 12, + Color(0xFF343637), + FontWeight.w500, + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .004, + ), + Text( + "TS J8 8905", + style: fontTextStyle( + 12, + Color(0xFF646566), + FontWeight.w500, + ), + ), + ], + ), + Spacer(), + Padding( + padding: EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Row( + children: [ + Image.asset( + 'images/message.png', + width: 24, + height: 24, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * .020, + ), + Image.asset( + 'images/phone.png', + width: 24, + height: 24, + ), + ], + ), + ) + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .024, + ), + InkWell( + onTap: () { + setState(() { + _showOrderSummary = !_showOrderSummary; + }); + }, + child: Row( + children: [ + Text( + "View Order Summary", + style: fontTextStyle( + 12, + Color(0xFF444444), + FontWeight.w500, + ), + ), + Spacer(), + Image.asset( + _showOrderSummary + ? 'images/arrow-up.png' + : 'images/arrow-down.png', + width: 16, + height: 16, + ), + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .010, + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: _showOrderSummary ? null : 0, + padding: _showOrderSummary + ? EdgeInsets.all(0) + : EdgeInsets.zero, + child: _showOrderSummary + ? Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "ITEMS", + style: fontTextStyle( + 12, + Color(0xFF444444), + FontWeight.w700, + ), + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "10,000 L Drinking water x 1", + style: fontTextStyle( + 12, + Color(0xFF515253), + FontWeight.w400, + ), + ), + Text( + "β‚Ή2,500", + style: fontTextStyle( + 12, + Color(0xFF515253), + FontWeight.w400, + ), + ), + ], + ), + Divider( + thickness: 1, + color: Color(0xFFC3C4C4)), + _priceRow( + "Item Total", "β‚Ή2,346.00"), + _priceRow( + "Delivery Charges", "β‚Ή150.00"), + _priceRow( + "Platform Fee", "β‚Ή6.00"), + _priceRow("Taxes", "β‚Ή12.49"), + Divider( + thickness: 1, + color: Color(0xFFC3C4C4)), + _priceRow("Total Bill", "β‚Ή2,514", + isBold: true), + Divider( + thickness: 1, + color: Color(0xFFC3C4C4)), + SizedBox(height: 12), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Mode of Payment", + style: fontTextStyle( + 12, + Color(0xFF444444), + FontWeight.w500, + ), + ), + Row( + children: [ + Image.asset( + 'images/success-toast.png', + width: 12, + height: 12, + ), + SizedBox(width: 4), + Text( + "Cash on delivery", + style: fontTextStyle( + 12, + Color(0xFF444444), + FontWeight.w500, + ), + ), + ], + ) + ], + ), + ], + ), + ), + ) + : null, + ), + SizedBox( + height: + MediaQuery.of(context).size.height * .012, + ), + Padding( + padding: + EdgeInsets.fromLTRB(0, 0, 0, 12), + child: Container( + width: double.infinity, + height: 80, + decoration: BoxDecoration( + color: Color(0xFFE8F2FF), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ); + } + + Widget _itemRow(String title, String price, + {bool bold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400)), + Text(price, + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400)), + ], + ), + ); + } +} diff --git a/lib/supplier/paln_requests_model.dart b/lib/supplier/paln_requests_model.dart new file mode 100644 index 0000000..bc52560 --- /dev/null +++ b/lib/supplier/paln_requests_model.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:intl/intl.dart'; + +import '../common/settings.dart'; + + +class PlanRequestModel { + String typeofwater=''; + String supplierName = ''; + String date = ''; + String time = ''; + String address=''; + String displayAddress = ''; + String capacity = ''; + String quantity = ''; + String quotedAmount=''; + String status=''; + double lat=0; + double lng=0; + double distanceInMeters=0; + Color cardColor=Colors.white; + String dbId = ''; + String supplierId = ''; + String acceptedTime = ''; + String startDate = ''; + String endDate = ''; + String biddingPrice = ''; + String actualPrice=''; + String paymentType=''; + String advanceAmount=''; + List dates=[]; + String frequency=''; + + PlanRequestModel(); + + factory PlanRequestModel.fromJson(Map json){ + PlanRequestModel rtvm = new PlanRequestModel(); + + rtvm.status = json['status'] ?? ''; + rtvm.typeofwater = json['type_of_water'] ?? ''; + rtvm.date = json['date'] ?? ''; + rtvm.time = json['time'] ?? ''; + rtvm.startDate=json['start_date']??''; + rtvm.endDate=json['end_date']??''; + rtvm.capacity = json['capacity'] ?? ''; + rtvm.quantity = json['quantity'] ?? ''; + rtvm.biddingPrice = json['bidding_price'].toString() ?? ''; + rtvm.actualPrice = json['actual_price'].toString() ?? ''; + rtvm.paymentType = json['payment_type'].toString() ?? ''; + rtvm.advanceAmount = json['adavnce_amount'].toString() ?? ''; + rtvm.dates = json['dates']?? []; + rtvm.dbId=json['_id']?? ''; + rtvm.frequency= json["weekly_count"].toString()??''; + final suppliers = json['requested_suppliers'] as List?; + + if (suppliers != null && suppliers.isNotEmpty) { + final supplier = suppliers[0]; + rtvm.quotedAmount = supplier['quoted_amount']?.toString() ?? '0.00'; + rtvm.supplierName = supplier['supplier']?['suppliername'] ?? ''; + rtvm.supplierId = supplier['supplierId'] ?? ''; + rtvm.address = supplier['supplier']['profile']['office_address'] ?? ''; + rtvm.lat =supplier['supplier']?['latitude'] ?? ''; + rtvm.lng = supplier['supplier']?['longitude'] ?? ''; + rtvm.acceptedTime = supplier['time'] ?? ''; + } else { + rtvm.quotedAmount = ''; + rtvm.supplierName = ''; + rtvm.address = ''; + rtvm.lat =0.0; + rtvm.lng =0.0; + } + List parts = rtvm.address.split(','); + if (parts.length > 4) { + rtvm.displayAddress = parts[2].trim(); + } else { + rtvm.displayAddress = rtvm.address; // fallback + } + + + + rtvm.distanceInMeters = double.parse((Geolocator.distanceBetween( + rtvm.lat, + rtvm.lng, + AppSettings.userLatitude, + AppSettings.userLongitude + ) / 1000).toStringAsFixed(2)); + + return rtvm; + } + +} \ No newline at end of file diff --git a/lib/supplier/payment_example.dart b/lib/supplier/payment_example.dart new file mode 100644 index 0000000..57d87a5 --- /dev/null +++ b/lib/supplier/payment_example.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void main() { + runApp(MaterialApp(home: UpiIntentExample())); +} + +class UpiIntentExample extends StatelessWidget { + final String upiId = "sneha.reddymalla@ybl"; // Replace with your UPI ID + final String name = "Sneha Reddy"; + final String note = "Sample"; + final String amount = "1.00"; + + void launchUPIApp(BuildContext context) async { + final uri = Uri.parse( + "upi://pay?pa=$upiId&pn=$name&tn=$note&am=$amount&cu=INR"); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("No UPI app found to handle this request")), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("UPI Payment via Intent")), + body: Center( + child: ElevatedButton( + onPressed: () => launchUPIApp(context), + child: Text("Pay with UPI App"), + ), + ), + ); + } +} diff --git a/lib/supplier/payment_example2.dart b/lib/supplier/payment_example2.dart new file mode 100644 index 0000000..58ee1f3 --- /dev/null +++ b/lib/supplier/payment_example2.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:upi_india/upi_india.dart'; + +class PaymentOptionsPage extends StatefulWidget { + final double amount; + const PaymentOptionsPage({required this.amount}); + + @override + State createState() => _PaymentOptionsPageState(); +} + +class _PaymentOptionsPageState extends State { + late Razorpay _razorpay; + late UpiIndia _upiIndia; + List upiApps = []; + bool isFetching = true; + + @override + void initState() { + super.initState(); + + _razorpay = Razorpay(); + _razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handleRazorpaySuccess); + _razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handleRazorpayError); + _razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet); + + _upiIndia = UpiIndia(); + fetchUpiApps(); + } + + void fetchUpiApps() async { + try { + upiApps = await _upiIndia.getAllUpiApps(mandatoryTransactionId: false); + } catch (e) { + upiApps = []; + } + setState(() { + isFetching = false; + }); + } + + void _openRazorpay() { + var options = { + 'key': 'rzp_test_1DP5mmOlF5G5ag', // Replace with your Razorpay key + 'amount': (widget.amount * 100).toInt(), // In paise + 'name': 'Water Supplier', + 'description': 'Advance Payment', + 'prefill': { + 'contact': '9876543210', + 'email': 'test@example.com', + }, + }; + + try { + _razorpay.open(options); + } catch (e) { + print('Error: $e'); + } + } + + void _startUpiTransaction(UpiApp app) async { + UpiResponse response = await _upiIndia.startTransaction( + app: app, + receiverUpiId: 'yourupi@okaxis', // Replace with your real UPI ID + receiverName: 'Water Supplier', + transactionRefId: 'TXN${DateTime.now().millisecondsSinceEpoch}', + transactionNote: 'Advance Payment', + amount: widget.amount, + ); + _handleUpiResponse(response); + } + + void _handleUpiResponse(UpiResponse res) { + String status = res.status?.toLowerCase() ?? "unknown"; + if (status == "success") { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("UPI Payment Successful"))); + } else if (status == "failure") { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("UPI Payment Failed"))); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("UPI Payment Cancelled"))); + } + } + + void _handleRazorpaySuccess(PaymentSuccessResponse response) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Razorpay Payment Success"))); + } + + void _handleRazorpayError(PaymentFailureResponse response) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Razorpay Payment Failed"))); + } + + void _handleExternalWallet(ExternalWalletResponse response) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Wallet: ${response.walletName}"))); + } + + @override + void dispose() { + _razorpay.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Choose Payment Option")), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + // Razorpay Button + ElevatedButton.icon( + onPressed: _openRazorpay, + icon: Icon(Icons.credit_card), + label: Text("Pay with Razorpay (Cards, UPI, Wallets)"), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF1D7AFC), + padding: EdgeInsets.symmetric(vertical: 14), + textStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + SizedBox(height: 24), + Align( + alignment: Alignment.centerLeft, + child: Text( + "Or choose a UPI App:", + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + SizedBox(height: 12), + Expanded( + child: isFetching + ? Center(child: CircularProgressIndicator()) + : upiApps.isEmpty + ? Center(child: Text("No UPI apps found")) + : ListView.builder( + itemCount: upiApps.length, + itemBuilder: (context, index) { + final app = upiApps[index]; + return ListTile( + leading: Image.memory(app.icon, height: 40, width: 40), + title: Text(app.name), + onTap: () => _startUpiTransaction(app), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/supplier/payment_screen.dart b/lib/supplier/payment_screen.dart new file mode 100644 index 0000000..c924671 --- /dev/null +++ b/lib/supplier/payment_screen.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class UpiIntentLauncher extends StatelessWidget { + const UpiIntentLauncher({super.key}); + + Future pay() async { + const upiId = '9000950877@ybl'; // supplier UPI + const name = 'Sneha Supplier'; + const amount = '1.00'; + const note = 'Test Payment'; + + final uri = Uri.parse( + 'upi://pay?pa=$upiId&pn=$name&am=$amount&cu=INR&tn=$note', + ); + + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + throw Exception('Could not launch UPI intent'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Pay with UPI')), + body: Center( + child: ElevatedButton( + onPressed: pay, + child: const Text('Pay β‚Ή1.00'), + ), + ), + ); + } +} diff --git a/lib/supplier/paymnets/credit_accounts.dart b/lib/supplier/paymnets/credit_accounts.dart new file mode 100644 index 0000000..c698d84 --- /dev/null +++ b/lib/supplier/paymnets/credit_accounts.dart @@ -0,0 +1,326 @@ +import 'dart:convert'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/supplier/paymnets/credit_accounts_details.dart'; + +class CreditAccountsScreen extends StatefulWidget { + const CreditAccountsScreen({super.key}); + + @override + State createState() => _CreditAccountsScreenState(); +} + +class _CreditAccountsScreenState extends State { + bool isLoading = true; + List suppliers = []; + + Razorpay? _razorpay; + Supplier? _currentPaySupplier; + + @override + void initState() { + super.initState(); + + fetchCreditAccounts(); + + _razorpay = Razorpay(); + _razorpay?.on( + Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess); + _razorpay?.on( + Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError); + _razorpay?.on( + Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet); + } + + @override + void dispose() { + _razorpay?.clear(); + _razorpay = null; + super.dispose(); + } + + // ================= FETCH CREDIT ACCOUNTS ================= + Future fetchCreditAccounts() async { + try { + final response = + await AppSettings.getAdvanceTransactionsByCustomer(); + + final decoded = jsonDecode(response); + final List data = decoded["data"] ?? []; + + final List list = data.map((e) { + final status = (e["status"] ?? "").toString().toLowerCase(); + final amount = + double.tryParse(e["advance_amount"].toString()) ?? 0; + + double balance = 0; + if (status == "approved") balance = amount; + if (status == "pending") { + balance = -amount; + } + if (status == "completed") { + balance = amount; + } + if (status == "paid_by_customer") balance = amount; + + return Supplier( + name: e["supplierName"] ?? "Supplier", + supplierId:e["supplierId"] ?? "***", + monthlyAmount: amount, + balance: balance, + status: status, + raw: e, + ); + }).toList(); + + setState(() { + suppliers = list; + isLoading = false; + }); + } catch (e) { + setState(() => isLoading = false); + } + } + + // ================= RAZORPAY ================= + void _openRazorpay(Supplier supplier) { + _currentPaySupplier = supplier; + + var options = { + 'key': 'rzp_test_1VCCWqEXUHdINz', // πŸ”‘ replace in prod + 'amount': (supplier.monthlyAmount * 100).toInt(), + 'name': 'Advance Payment', + 'description': 'Water Credit Top Up', + /*'prefill': { + 'contact': AppSettings.userMobile, + 'email': AppSettings.userEmail, + }*/ + }; + + try { + _razorpay?.open(options); + } catch (e) { + debugPrint("Razorpay error: $e"); + } + } + + Future _handlePaymentSuccess( + PaymentSuccessResponse response) async { + if (_currentPaySupplier == null) return; + + AppSettings.preLoaderDialog(context); + + final success = await AppSettings.payAdvance( + transactionId: + _currentPaySupplier!.raw["transactionId"], + payload: { + "advance_amount": _currentPaySupplier!.monthlyAmount, + "payment_type": "razorpay", + "ref_number": response.paymentId, + }, + ); + + Navigator.of(context, rootNavigator: true).pop(); + + if (success) { + AppSettings.longSuccessToast("Payment successful"); + fetchCreditAccounts(); + } else { + AppSettings.longFailedToast("Payment failed"); + } + + _currentPaySupplier = null; + } + + void _handlePaymentError( + PaymentFailureResponse response) { + AppSettings.longFailedToast("Payment failed"); + } + + void _handleExternalWallet( + ExternalWalletResponse response) {} + + // ================= UI ================= + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0XFFFFFFFF), + appBar: AppSettings.supplierAppBarWithoutActions( + "Credit Accounts", context), + body: Padding( + padding: const EdgeInsets.all(16), + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : ListView( + children: [ + _addSupplierCard(), + const SizedBox(height: 20), + ...suppliers.map(_supplierCard).toList(), + ], + ), + ), + ); + } + + Widget _addSupplierCard() { + return InkWell( + onTap: () {}, + child: Row( + children: [ + DottedBorder( + borderType: BorderType.Circle, + dashPattern: const [6, 3], + color: const Color(0XFF444444), + strokeWidth: 1, + child: Container( + width: 48, + height: 48, + alignment: Alignment.center, + child: Image.asset( + 'images/plus.png', + width: 24, + height: 24, + color: const Color(0XFF444444), + ), + ), + ), + const SizedBox(width: 12), + Text( + "Add Supplier", + style: fontTextStyle( + 12, const Color(0XFF232527), FontWeight.w600), + ), + ], + ), + ); + } + + Widget _supplierCard(Supplier supplier) { + return GestureDetector( + onTap: () { + if (supplier.status == "pending" || + supplier.status == "paid_by_customer") { + return; // 🚫 no navigation + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + CreditAccountsDetails(details: supplier), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: const Color(0xFFE8F2FF), + borderRadius: BorderRadius.circular(28), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(28), + child: Image.asset("images/profile_user.png"), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + supplier.supplierId, + style: fontTextStyle( + 12, const Color(0XFF232527), FontWeight.w600), + ), + /* RichText( + text: TextSpan(children: [ + TextSpan( + text: + "β‚Ή${supplier.monthlyAmount.toStringAsFixed(0)} ", + style: fontTextStyle( + 10, + const Color(0xFF515253), + FontWeight.w400), + ), + TextSpan( + text: "/month", + style: fontTextStyle( + 10, + const Color(0xFF939495), + FontWeight.w400), + ), + ]), + ),*/ + + // ===== STATUS MESSAGES ===== + if (supplier.status == "pending") + Padding( + padding: const EdgeInsets.only(top: 4), + child: InkWell( + onTap: () => _openRazorpay(supplier), + child: Text( + "Please top up", + style: fontTextStyle( + 10, + const Color(0XFFE2483D), + FontWeight.w600), + ), + ), + ), + + if (supplier.status == "paid_by_customer") + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + "Confirmation needed from supplier", + style: fontTextStyle( + 10, + const Color(0XFF939495), + FontWeight.w600), + ), + ), + ], + ), + ), + Text( + "β‚Ή${AppSettings.formDouble(supplier.balance.toStringAsFixed(2))}", + style: fontTextStyle( + 16, + supplier.balance < 0 + ? const Color(0XFFE2483D) + : const Color(0XFF0A9E04), + FontWeight.w600, + ), + ), + ], + ), + ), + ); + } +} + +// ================= MODEL ================= +class Supplier { + final String name; + final double monthlyAmount; + final double balance; + final String status; + final String supplierId; + final Map raw; + + Supplier({ + required this.name, + required this.monthlyAmount, + required this.balance, + required this.status, + required this.supplierId, + required this.raw, + }); +} diff --git a/lib/supplier/paymnets/credit_accounts_details.dart b/lib/supplier/paymnets/credit_accounts_details.dart new file mode 100644 index 0000000..c6db1a0 --- /dev/null +++ b/lib/supplier/paymnets/credit_accounts_details.dart @@ -0,0 +1,311 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/supplier/paymnets/top_up_page.dart'; + +class CreditAccountsDetails extends StatefulWidget { + var details; + CreditAccountsDetails({this.details}); + + @override + State createState() => _CreditAccountsDetailsState(); +} + +class _CreditAccountsDetailsState extends State { + List> transactions = []; + bool isLoading = true; + + @override + void initState() { + fetchTransactions(); + super.initState(); + } + + Future fetchTransactions() async { + try { + final supplierId = widget.details.supplierId; + final customerId = AppSettings.customerId; + + final response = await AppSettings.getAdvanceTransactionsBySupplierAndCustomer( + supplierId, + customerId, + ); + + final decoded = jsonDecode(response); + final List data = decoded["data"] ?? []; + + setState(() { + transactions = data.map>((e) { + final amount = (e["advance_amount"] ?? 0).toDouble(); + final status = (e["status"] ?? "").toString().toLowerCase(); + + return { + "title": status == "completed" ? "Advance Received" : "Water Delivery", + "subtitle": e["payment_type"] ?? "", + "amount": status == "completed" ? amount : -amount, + "date": DateFormat("dd MMM, yyyy").format( + DateTime.parse(e["date_of_transaction"]), + ), + }; + }).toList(); + + isLoading = false; + }); + } catch (e) { + setState(() => isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0XFFFFFFFF), + appBar: AppSettings.supplierAppBarWithoutActions(widget.details.name, context), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + widget.details.balance < 0 ? const Color(0XFFFADEDE) : const Color(0XFFC3DEC2), + const Color(0XFFFFFFFF) + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.details.name, + style: fontTextStyle(16, const Color(0XFF3B3B3B), FontWeight.w600), + ), + const SizedBox(height: 4), + Text( + "Credit Account", + style: fontTextStyle(12, const Color(0XFF3B3B3B), FontWeight.w500), + ), + const SizedBox(height: 12), + Text( + "Available Balance", + style: fontTextStyle( + 12, + widget.details.balance < 0 ? const Color(0XFFE2483D) : const Color(0XFF0A9E04), + FontWeight.w500, + ), + ), + const SizedBox(height: 6), + Text( + "+β‚Ή${AppSettings.formDouble(widget.details.balance.toStringAsFixed(2))}", + style: fontTextStyle( + 36, + widget.details.balance < 0 ? const Color(0XFFE2483D) : const Color(0XFF0A9E04), + FontWeight.w700, + ), + ), + Padding( + padding: const EdgeInsets.all(0), + child: Row( + children: [ + // βœ… TopUp navigation + reload after payment + Expanded( + child: GestureDetector( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CreditTopUpConfirmScreen( + supplierName: widget.details.name, + supplierId: widget.details.supplierId, + ), + ), + ); + + if (result == true) { + // πŸ”₯ Reload transactions + you can also re-fetch balance from your profile API if needed + fetchTransactions(); + setState(() {}); // keep safe + } + }, + child: _actionButton( + 'images/plus_credit_account.png', + "Top Up", + const Color(0XFF8270DB), + ), + ), + ), + + if (widget.details.balance > 0) ...[ + const SizedBox(width: 12), + Expanded(child: _actionButton('images/request_credit_account.png', "Request", const Color(0XFF8270DB))), + const SizedBox(width: 12), + Expanded(child: _actionButton('images/cross_credit_account.png', "Close", const Color(0XFFE2483D))), + ] + ], + ), + ), + ], + ), + ), + + Visibility( + visible: widget.details.balance < 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Text( + 'Dues', + style: fontTextStyle(16, const Color(0XFF3B3B3B), FontWeight.w600), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1D7AFC), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFF1968D6), + ), + child: Center( + child: Image.asset('images/truck-payments.png', width: 20, height: 20), + ), + ), + const SizedBox(height: 8), + Text("Payment due", style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500)), + const SizedBox(height: 4), + Text( + "Due Today, ${DateFormat("MMM d, y").format(DateTime.now())}", + style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w400), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "β‚Ή ${AppSettings.formDouble(widget.details.balance.abs().toStringAsFixed(2))}", + style: fontTextStyle(16, const Color(0xFFFFFFFF), FontWeight.w500), + ), + const SizedBox(height: 30), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF36AF31), + borderRadius: BorderRadius.circular(12), + ), + child: Text("Pay now", style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w600)), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + + Padding( + padding: EdgeInsets.fromLTRB(16, widget.details.balance < 0 ? 0 : 16, 16, 0), + child: Text('Transactions', style: fontTextStyle(16, const Color(0XFF3B3B3B), FontWeight.w600)), + ), + SizedBox(height: MediaQuery.of(context).size.width * .012), + + Expanded( + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : ListView.separated( + itemCount: transactions.length, + separatorBuilder: (_, __) => const Divider(color: Color(0xFFE0E0E0)), + itemBuilder: (context, index) { + final t = transactions[index]; + final double amount = (t["amount"] as num).toDouble(); + final bool isCredit = amount > 0; + + return Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t["title"].toString(), style: fontTextStyle(12, const Color(0XFF000000), FontWeight.w400)), + if (t["subtitle"].toString().isNotEmpty) + Text(t["subtitle"].toString(), + style: fontTextStyle(10, const Color(0XFF114690), FontWeight.w400)), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "${isCredit ? '+' : ''}β‚Ή${amount.abs().toStringAsFixed(2)}", + style: fontTextStyle( + 12, + isCredit ? const Color(0XFF0A9E04) : const Color(0XFFE2483D), + FontWeight.w500, + ), + ), + Text(t["date"].toString(), style: fontTextStyle(10, const Color(0XFF757575), FontWeight.w400)), + ], + ), + ], + ), + ); + }, + ), + ) + ], + ), + ); + } + + Widget _actionButton(String path, String label, Color color) { + return Container( + height: 80, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(path, width: 24, height: 24, color: color), + const SizedBox(height: 6), + Text(label, style: fontTextStyle(14, const Color(0XFF2A2A2A), FontWeight.w400)), + ], + ), + ), + ); + } +} diff --git a/lib/supplier/paymnets/payments_main.dart b/lib/supplier/paymnets/payments_main.dart new file mode 100644 index 0000000..9207b7f --- /dev/null +++ b/lib/supplier/paymnets/payments_main.dart @@ -0,0 +1,395 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:watermanagement/supplier/paymnets/credit_accounts.dart'; +import 'package:watermanagement/supplier/paymnets/transaction_model.dart'; +import 'package:watermanagement/supplier/paymnets/transactions.dart'; +import 'package:http/http.dart' as http; +import '../../common/settings.dart'; + +class PaymentsMainScreen extends StatefulWidget { + const PaymentsMainScreen({super.key}); + + @override + State createState() => _PaymentsMainScreenState(); +} + +class _PaymentsMainScreenState extends State { + + List transactions = []; + bool isLoading = true; + + Future fetchTransactions() async { + String customerId = AppSettings.customerId; + final url = Uri.parse( + "http://armintaaqua.com:3000/api/userAccounts/$customerId"); + + try { + final response = await http.get(url); + if (response.statusCode != 200) return; + + final body = json.decode(response.body); + final data = body["data"] ?? {}; + + final List loaded = []; + + double parseAmount(dynamic v) { + if (v == null) return 0; + final s = v.toString().trim().toLowerCase(); + if (s.isEmpty || s == "null" || s == "undefined" || s == "nan") return 0; + return double.tryParse(s) ?? 0; + } + + String safe(dynamic v) { + if (v == null) return "-"; + final s = v.toString().trim(); + if (s.isEmpty || s == "null" || s == "undefined") return "-"; + return s; + } + + // ================= PAID ================= + for (final item in data["advance_paid_entries"] ?? []) { + final amount = parseAmount(item["amount_paid"]); + + loaded.add(TransactionModel( + safe(item["supplierName"]), + safe(item["dateOfOrder"]), + "+β‚Ή${amount.toStringAsFixed(0)}", + Colors.green, + )); + } + + // ================= DELIVERED ================= + for (final item in data["delivered_entries"] ?? []) { + final amount = parseAmount(item["amount"]); + + loaded.add(TransactionModel( + safe(item["supplierName"]), + safe(item["date"] ?? item["dateOfOrder"]), + "+β‚Ή${amount.toStringAsFixed(0)}", + Colors.blue, + )); + } + + // ================= PENDING ================= + for (final item in data["pending_entries"] ?? []) { + final amount = parseAmount(item["amount"]); + + loaded.add(TransactionModel( + safe(item["supplierName"]), + safe(item["date"]), + "β‚Ή${amount.toStringAsFixed(0)}", + Colors.orange, + )); + } + + // ================= REJECTED ================= + for (final item in data["rejected_entries"] ?? []) { + final amount = parseAmount(item["amount"]); + + loaded.add(TransactionModel( + safe(item["supplierName"]), + safe(item["date"]), + "β‚Ή${amount.toStringAsFixed(0)}", + Colors.grey, + )); + } + + setState(() { + transactions = loaded; // ← ALL RECORDS NOW + isLoading = false; + }); + } catch (e) { + isLoading = false; + debugPrint("Transactions error: $e"); + } + } + + @override + void initState() { + super.initState(); + fetchTransactions(); + } + + Widget _quickMenuItem(String imagePath, String title) { + return Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Color(0XFFC3C4C4), width: 1.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset(imagePath, width: 20, height: 20, fit: BoxFit.contain), + SizedBox(height: 8), + Text( + title, + style: fontTextStyle(14, Color(0xFF2A2A2A), FontWeight.w600), + ), + ], + ), + ); + } + + Widget _transferItem(String name, String imagePath) { + return Column( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: Color(0xFFE8F2FF), + borderRadius: BorderRadius.circular(28), // 28px radius = circle + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(28), + child: Image.asset( + imagePath, + width: 56, + height: 56, + ), + ), + ), + SizedBox(height: 4), + Text(name, style: TextStyle(fontSize: 12)), + ], + ); + } + + Widget _transactionItem( + String supplier, String dateTime, String amount, Color color) { + return ListTile( + contentPadding: EdgeInsets.zero, // πŸ‘ˆ removes default padding + title: Text( + supplier, + style: TextStyle(fontWeight: FontWeight.w600), + ), + subtitle: Text(dateTime), + trailing: Text( + amount, + style: TextStyle(color: color, fontWeight: FontWeight.w600), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Quick Menu + Text( + "Quick Menu", + style: fontTextStyle(14, Color(0xFF2A2A2A), FontWeight.w600), + ), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + + + Expanded( + child: GestureDetector( + onTap: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreditAccountsScreen()), + ); + }, + child: _quickMenuItem('images/credit-accounts.png', "Credit Accounts"), + ), + ), + SizedBox(width: 12), // spacing between items + Expanded( + child: _quickMenuItem('images/payment-methods.png', "Payment methods"), + ), + SizedBox(width: 12), + Expanded( + child: _quickMenuItem('images/spending-summary.png', "Spending\nSummary"), + ), + ], + ), + SizedBox(height: 20), + // Payment due card + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Color(0xFF1D7AFC), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFF1968D6), + ), + child: Center( + child: Image.asset( + 'images/truck-payments.png', + width: 20, + height: 20, + ), + ), + ), + SizedBox(height: 8), + Text( + "Payment due", + style: + fontTextStyle(12, Color(0xFFFFFFFF), FontWeight.w500), + ), + SizedBox(height: 4), + Text( + "Due today, May 20, 2025", + style: + fontTextStyle(10, Color(0xFFFFFFFF), FontWeight.w400), + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "β‚Ή2,864.10", + style: + fontTextStyle(16, Color(0xFFFFFFFF), FontWeight.w500), + ), + SizedBox(height: 30), + Container( + padding: + EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Color(0xFF36AF31), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + "Pay now", + style: + fontTextStyle(10, Color(0xFFFFFFFF), FontWeight.w600), + ), + ), + ], + ), + ], + ), + ), + SizedBox(height: 20), + // Transfer + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transfer", + style: fontTextStyle(14, Color(0xFF2A2A2A), FontWeight.w600), + ), + Image.asset('images/arrow-right.png', width: 16, height: 16), + ], + ), + SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + DottedBorder( + borderType: BorderType.Circle, + dashPattern: [6, 3], + color: Colors.grey, + strokeWidth: 1, + child: Container( + width: 48, + height: 48, + alignment: Alignment.center, + child: Icon(Icons.add, color: Colors.black), + ), + ), + SizedBox(height: 4), + Text("Add", style: TextStyle(fontSize: 12)), + ], + ), + _transferItem("Name", 'images/profile_user.png'), + _transferItem("Name", 'images/profile_user.png'), + _transferItem("Name", 'images/profile_user.png'), + _transferItem("Name", 'images/profile_user.png'), + ], + ), + SizedBox(height: 20), + // Transactions + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: fontTextStyle(14, Color(0xFF2A2A2A), FontWeight.w600), + ), + GestureDetector( + onTap: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Transactions( + transactions: transactions, + ), + ), + ); + }, + child: Image.asset( + 'images/arrow-right.png', // Replace with your actual image path + width: 16, + height: 16, + ), + ) + + ], + ), + SizedBox(height: 12), + + // Transaction list + isLoading + ? const Center(child: CircularProgressIndicator(color: primaryColor,)): + + (transactions.length != 0 + ? ListView.builder( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: transactions.length, + itemBuilder: (context, index) { + final txn = transactions[index]; + return _transactionItem( + txn.name, + txn.dateTime, + txn.amount, + txn.color, + ); + }, + ) + : Center( + child: Text( + 'No Data Available', + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + )) + + ], + ), + ), + ); + } +} diff --git a/lib/supplier/paymnets/top_up_page.dart b/lib/supplier/paymnets/top_up_page.dart new file mode 100644 index 0000000..ebe952a --- /dev/null +++ b/lib/supplier/paymnets/top_up_page.dart @@ -0,0 +1,440 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:upi_india/upi_india.dart'; +import 'package:watermanagement/common/settings.dart'; + +class CreditTopUpConfirmScreen extends StatefulWidget { + final String supplierName; + final String supplierId; + + const CreditTopUpConfirmScreen({ + super.key, + required this.supplierName, + required this.supplierId, + }); + + @override + State createState() => _CreditTopUpConfirmScreenState(); +} + +class _CreditTopUpConfirmScreenState extends State { + final TextEditingController amountCtrl = TextEditingController(text: "2000"); + bool isPaying = false; + String? errorText; + + // UPI + late UpiIndia _upiIndia; + List _apps = []; + UpiApp? _selectedUpiApp; + + // Razorpay + Razorpay? _razorpay; + + @override + void initState() { + super.initState(); + _upiIndia = UpiIndia(); + _loadUpiApps(); + + _razorpay = Razorpay(); + _razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _onRazorpaySuccess); + _razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _onRazorpayError); + _razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _onRazorpayExternalWallet); + } + + @override + void dispose() { + amountCtrl.dispose(); + _razorpay?.clear(); + super.dispose(); + } + + Future _loadUpiApps() async { + try { + final apps = await _upiIndia.getAllUpiApps(mandatoryTransactionId: false); + if (!mounted) return; + setState(() { + _apps = apps; + // Prefer PhonePe if exists else first + _selectedUpiApp = _apps.firstWhere( + (a) => a.name.toLowerCase().contains("phonepe"), + orElse: () => _apps.isNotEmpty ? _apps.first : UpiApp.bhim, + ); + }); + } catch (_) { + // Ignore, we will show Razorpay option anyway + } + } + + double _readAmount() { + final raw = amountCtrl.text.trim().replaceAll(",", ""); + final v = double.tryParse(raw) ?? 0; + return v; + } + + bool _validateAmount() { + final amt = _readAmount(); + if (amt < 1) { + setState(() => errorText = "Enter a valid amount"); + return false; + } + if (amt > 1000000) { + setState(() => errorText = "Amount too high"); + return false; + } + setState(() => errorText = null); + return true; + } + + Future _payViaSelectedUpiApp() async { + if (!_validateAmount()) return; + if (_selectedUpiApp == null) { + setState(() => errorText = "No UPI app found. Use Razorpay option."); + return; + } + + final amt = _readAmount(); + + setState(() { + isPaying = true; + errorText = null; + }); + + try { + // πŸ”₯ Use YOUR business UPI ID here (store / supplier / platform UPI) + // If you want supplier-wise UPI ID, pass it in widget. + const receiverUpiId = "9912686262@xyz"; + const receiverName = "Water Management"; + + final txnRef = "TOPUP_${DateTime.now().millisecondsSinceEpoch}"; + + final res = await _upiIndia.startTransaction( + app: _selectedUpiApp!, + receiverUpiId: receiverUpiId, + receiverName: receiverName, + transactionRefId: txnRef, + transactionNote: "Credit TopUp", + amount: amt, + ); + + if (!mounted) return; + + // βœ… UPI result handling + final status = (res.status ?? "").toLowerCase(); + if (status == UpiPaymentStatus.SUCCESS.toLowerCase()) { + // Save in your backend + await AppSettings.addAdvanceTopUp( + widget.supplierId, + AppSettings.customerId, + amt, + "upi_${_selectedUpiApp!.name.toLowerCase()}", + gatewayTxnId: res.transactionId ?? txnRef, + ); + + if (!mounted) return; + Navigator.pop(context, true); + } else if (status == UpiPaymentStatus.SUBMITTED.toLowerCase()) { + setState(() => errorText = "Payment pending. Please check in your UPI app."); + } else { + setState(() => errorText = "Payment failed or cancelled."); + } + } catch (e) { + if (!mounted) return; + setState(() => errorText = "Payment failed. Try again."); + } finally { + if (mounted) setState(() => isPaying = false); + } + } + + /// βœ… Razorpay (Recommended for full PhonePe/cards/netbanking etc.) + /// Backend creates order_id securely, then open checkout. + Future _payViaRazorpay() async { + if (!_validateAmount()) return; + + final amt = _readAmount(); + + setState(() { + isPaying = true; + errorText = null; + }); + + try { + // 1) Create Razorpay order from backend + final orderResp = await AppSettings.createRazorpayOrder( + amount: amt, + supplierId: widget.supplierId, + customerId: AppSettings.customerId, + ); + + final decoded = jsonDecode(orderResp); + final orderId = decoded["order_id"]; // backend should return this + final keyId = decoded["key_id"]; // backend may return key_id or keep static in app + + if (orderId == null || keyId == null) { + throw Exception("Invalid order response"); + } + + final options = { + 'key': keyId, + 'amount': (amt * 100).round(), // paise + 'name': widget.supplierName, + 'description': 'Credit Top Up', + 'order_id': orderId, + 'retry': {'enabled': true, 'max_count': 1}, + 'prefill': { + 'contact': AppSettings.phoneNumber ?? '', + 'email': AppSettings.email ?? '', + }, + 'theme': {'color': '#1D7AFC'}, + // 'external': {'wallets': ['paytm']} // optional + }; + + _razorpay?.open(options); + } catch (e) { + if (!mounted) return; + setState(() { + isPaying = false; + errorText = "Unable to start payment. Try again."; + }); + } + } + + // Razorpay callbacks + Future _onRazorpaySuccess(PaymentSuccessResponse response) async { + final amt = _readAmount(); + + try { + // 2) Verify payment on backend (signature verification) + await AppSettings.verifyRazorpayPayment( + razorpayPaymentId: response.paymentId ?? "", + razorpayOrderId: response.orderId ?? "", + razorpaySignature: response.signature ?? "", + ); + + // 3) Save topup + await AppSettings.addAdvanceTopUp( + widget.supplierId, + AppSettings.customerId, + amt, + "razorpay", + gatewayTxnId: response.paymentId ?? "", + ); + + if (!mounted) return; + Navigator.pop(context, true); + } catch (e) { + if (!mounted) return; + setState(() { + errorText = "Payment captured but verification failed. Contact support."; + isPaying = false; + }); + } + } + + void _onRazorpayError(PaymentFailureResponse response) { + if (!mounted) return; + setState(() { + isPaying = false; + errorText = "Payment failed. Please try again."; + }); + } + + void _onRazorpayExternalWallet(ExternalWalletResponse response) { + // optional + } + + @override + Widget build(BuildContext context) { + final amount = _readAmount(); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.close, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + const SizedBox(height: 24), + + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: Text( + widget.supplierName, + key: ValueKey(widget.supplierName), + style: fontTextStyle(16, Colors.black, FontWeight.w600), + ), + ), + const SizedBox(height: 6), + Text("Credit Account Β· Top Up", style: fontTextStyle(12, Colors.grey, FontWeight.w400)), + + const SizedBox(height: 24), + + // βœ… Editable amount + AnimatedContainer( + duration: const Duration(milliseconds: 250), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFEFF4FF), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: errorText != null ? Colors.red.withOpacity(0.5) : Colors.transparent, + ), + ), + child: TextField( + controller: amountCtrl, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + onChanged: (_) { + if (errorText != null) setState(() => errorText = null); + setState(() {}); // animate amount changes + }, + style: fontTextStyle(26, const Color(0xFF1D7AFC), FontWeight.w700), + decoration: const InputDecoration( + border: InputBorder.none, + prefixText: "β‚Ή", + ), + ), + ), + + const SizedBox(height: 8), + Text("What is it for?", style: fontTextStyle(12, Colors.grey, FontWeight.w400)), + + if (errorText != null) ...[ + const SizedBox(height: 12), + AnimatedOpacity( + opacity: 1, + duration: const Duration(milliseconds: 250), + child: Text(errorText!, style: fontTextStyle(12, Colors.red, FontWeight.w500)), + ), + ], + + const SizedBox(height: 18), + + // βœ… UPI App Selector + if (_apps.isNotEmpty) ...[ + Align( + alignment: Alignment.centerLeft, + child: Text("Pay using UPI App", style: fontTextStyle(12, const Color(0xFF3B3B3B), FontWeight.w600)), + ), + const SizedBox(height: 10), + SizedBox( + height: 56, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: _apps.length, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (_, i) { + final app = _apps[i]; + final selected = _selectedUpiApp?.name == app.name; + + return GestureDetector( + onTap: () => setState(() => _selectedUpiApp = app), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: selected ? const Color(0xFF1D7AFC).withOpacity(0.12) : Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: selected ? const Color(0xFF1D7AFC) : const Color(0xFFE0E0E0), + ), + ), + child: Row( + children: [ + if (app.icon != null) Image.memory(app.icon!, width: 22, height: 22), + const SizedBox(width: 8), + Text(app.name, style: fontTextStyle(12, const Color(0xFF3B3B3B), FontWeight.w500)), + ], + ), + ), + ); + }, + ), + ), + ], + + const Spacer(), + + // βœ… Buttons (Animated) + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: isPaying + ? SizedBox( + key: const ValueKey("loading"), + width: double.infinity, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1D7AFC), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + onPressed: null, + child: const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ), + ), + ) + : Column( + key: const ValueKey("buttons"), + children: [ + // βœ… Confirm = UPI app selected + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1D7AFC), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + onPressed: (_apps.isNotEmpty) ? _payViaSelectedUpiApp : null, + child: Text( + _apps.isNotEmpty + ? "Confirm (UPI: ${_selectedUpiApp?.name ?? ''})" + : "Confirm (UPI not available)", + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + ), + ), + const SizedBox(height: 10), + + // βœ… Razorpay = best for PhonePe/cards/netbanking + SizedBox( + width: double.infinity, + height: 48, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Color(0xFF1D7AFC)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + ), + onPressed: _payViaRazorpay, + child: Text( + "Pay via Razorpay / PhonePe", + style: fontTextStyle(14, const Color(0xFF1D7AFC), FontWeight.w600), + ), + ), + ), + + const SizedBox(height: 10), + Text( + "Amount: β‚Ή${amount.toStringAsFixed(0)}", + style: fontTextStyle(10, const Color(0xFF757575), FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/supplier/paymnets/transaction_model.dart b/lib/supplier/paymnets/transaction_model.dart new file mode 100644 index 0000000..5dbade2 --- /dev/null +++ b/lib/supplier/paymnets/transaction_model.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class TransactionModel { + final String name; + final String dateTime; + final String amount; + final Color color; + + TransactionModel(this.name, this.dateTime, this.amount, this.color); +} \ No newline at end of file diff --git a/lib/supplier/paymnets/transactions.dart b/lib/supplier/paymnets/transactions.dart new file mode 100644 index 0000000..7590a66 --- /dev/null +++ b/lib/supplier/paymnets/transactions.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/supplier/paymnets/transaction_model.dart'; + +class Transactions extends StatefulWidget { + final List transactions; + + const Transactions({ + super.key, + required this.transactions, + }); + @override + State createState() => _TransactionsState(); +} + +class _TransactionsState extends State { + + Widget _transactionItem( + String supplier, String dateTime, String amount, Color color) { + return ListTile( + contentPadding: EdgeInsets.zero, + title: Text(supplier, style: const TextStyle(fontWeight: FontWeight.w600)), + subtitle: Text(dateTime), + trailing: Text( + amount, + style: TextStyle(color: color, fontWeight: FontWeight.w600), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0XFFFFFFFF), + appBar: AppSettings.supplierAppBarWithoutActions( + 'Transactions', context), + body: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: widget.transactions.isEmpty + ? Center( + child: Text( + 'No Data Available', + style: fontTextStyle( + 12, const Color(0XFF000000), FontWeight.w500), + ), + ) + : ListView.builder( + itemCount: widget.transactions.length, + itemBuilder: (context, index) { + final txn = widget.transactions[index]; + return Column( + children: [ + _transactionItem( + txn.name, + txn.dateTime, + txn.amount, + txn.color, + ), + if (index != widget.transactions.length - 1) + const Divider( + thickness: 1, + color: Color(0xFFE0E0E0), + height: 1, + ), + ], + ); + }, + ), + ), + ); + } + +} + diff --git a/lib/supplier/place_order.dart b/lib/supplier/place_order.dart new file mode 100644 index 0000000..13a9105 --- /dev/null +++ b/lib/supplier/place_order.dart @@ -0,0 +1,686 @@ +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/models/supplier_tankers_model.dart'; + +class PlaceOrder extends StatefulWidget { + var details; + PlaceOrder({this.details}); + + @override + State createState() => _PlaceOrderState(); +} + +class _PlaceOrderState extends State { + + bool isTankerDataLoading = false; + bool isSereverIssue = false; + List tankersList = []; + String? _selectedOption = 'Yes'; + int quantity = 1; + DateTime? _selectedDate; + String displayDeliveryDate=''; + TimeOfDay? _selectedTime; + TimeOfDay _selectedTime1 = TimeOfDay(hour: 9, minute: 0); + + Future _pickTime() async { + final TimeOfDay now = TimeOfDay.now(); + + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime ?? now, + helpText: 'Select a time', + ); + + if (picked != null && picked != _selectedTime) { + setState(() { + _selectedTime = picked; + }); + } + } + + void _showTimePicker() { + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 300, + color: Colors.white, + child: Column( + children: [ + Container( + padding: EdgeInsets.all(16), + child: Text( + 'Select Time', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: false, // true for 24-hour format + initialDateTime: DateTime( + 0, + 0, + 0, + /* _selectedTime!.hour, + _selectedTime!.minute,*/ + ), + onDateTimeChanged: (DateTime newTime) { + setState(() { + _selectedTime = TimeOfDay( + hour: newTime.hour, + minute: newTime.minute, + ); + }); + }, + ), + ), + CupertinoButton( + child: Text('Done'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ); + } + + Future _selectTime(BuildContext context) async { + + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime1, + cancelText: 'Cancel', + confirmText: 'Confirm', + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.dark().copyWith( + primaryColor: Color(0XFF1D7AFC), + timePickerTheme: TimePickerThemeData( + backgroundColor: Colors.white, + dialBackgroundColor: Colors.white, + hourMinuteTextColor: Color(0XFF1D7AFC), + dayPeriodTextColor: Color(0XFF1D7AFC), + dialTextColor: Color(0XFF1D7AFC), + dayPeriodColor: Color(0XFFC3C4C4), + hourMinuteShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Color(0XFFFFFFFF), width: 2), + ), + dayPeriodShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + dialHandColor: Color(0XFFC3C4C4), + hourMinuteColor: Color(0XFFFFFFFF), + dialTextStyle: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + dayPeriodTextStyle: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + hourMinuteTextStyle: fontTextStyle(50, Color(0XFF1D7AFC), FontWeight.w600), + helpTextStyle: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + cancelButtonStyle: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Color(0XFF1D7AFC)), + padding: MaterialStateProperty.all(EdgeInsets.symmetric(horizontal: 20, vertical: 10)), + textStyle: MaterialStateProperty.all(fontTextStyle(14, Colors.white, FontWeight.w600)), + ), + confirmButtonStyle: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Color(0XFF1D7AFC)), + padding: MaterialStateProperty.all(EdgeInsets.symmetric(horizontal: 20, vertical: 10)), + textStyle: MaterialStateProperty.all(fontTextStyle(14, Colors.white, FontWeight.w600)), + ), + )), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedTime) { + setState(() { + _selectedTime = picked; + }); + } + } + + Future _pickDate() async { + DateTime now = DateTime.now(); + DateTime lastDate = now.add(Duration(days: 15)); + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? now, + firstDate: now, // Restrict to today or later + lastDate: lastDate, // Only allow next 15 days + helpText: 'Select From Date', + initialEntryMode: DatePickerEntryMode.calendarOnly, + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + inputDecorationTheme: InputDecorationTheme( + border: InputBorder.none, // Removes the text field border + ), + colorScheme: ColorScheme.light( + primary: Color(0XFF1D7AFC), + onPrimary: Color(0XFFFFFFFF), // Header text color + surface: Color(0XFFFFFFFF), + onSurface: Colors.black, + secondary: Colors.pink, + + ), + dividerColor: Color(0XFFF5F6F6), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(Color(0XFF1D7AFC),), // Text color + //backgroundColor: MaterialStateProperty.all(Colors.grey[200]), // Background + textStyle: MaterialStateProperty.all(fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600),), // Font style + shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), // Rounded corners + ), + // Background of the dialog box + ), + textTheme: TextTheme( + bodyLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + labelLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + titleLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + headlineLarge: fontTextStyle(20, Color(0XFF1D7AFC), FontWeight.w600), + + ), + + ), + child: child!, + ); + }, + ); + + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + //apiFromDate = DateFormat('dd-MMM-yyyy - 23:50').format(_selectedDate!); + String from=DateFormat('dd-MMM-yyyy - HH:mm').format(_selectedDate!); + DateTime parsedFromDate = DateFormat('dd-MMM-yyyy - HH:mm').parse(from); + String formattedFromDate = DateFormat('dd MMM yyyy').format(parsedFromDate); + displayDeliveryDate = formattedFromDate; + }); + } + } + + Future getTankers() async { + isTankerDataLoading = true; + try { + var tankerResponse = await AppSettings.getAllTankers(widget.details.supplier_id); + + setState(() { + tankersList = + ((jsonDecode(tankerResponse)['data']) as List).map((dynamic model) { + return SupplierTankersModel.fromJson(model); + }).toList(); + + + + isTankerDataLoading = false; + }); + } catch (e) { + setState(() { + isTankerDataLoading = false; + isSereverIssue = true; + }); + /* AppSettings.longFailedToast('There is an issue at server side please try after some time'); + Navigator.pop(context);*/ + } + } + + + @override + void initState() { + // TODO: implement initState + super.initState(); + getTankers(); + } + + void increment() { + setState(() { + quantity++; + }); + } + + void decrement() { + setState(() { + if (quantity > 1) quantity--; + }); + } + + Widget renderUi(){ + Map> groupByType(List items) { + final Map> grouped = {}; + for (var item in items) { + grouped.putIfAbsent(item.type_of_water, () => []).add(item); + } + return grouped; + } + final groupedData = groupByType(tankersList); + + if(tankersList.isNotEmpty){ + return Padding(padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // πŸ”Ή Supplier Name + SizedBox(height: MediaQuery.of(context).size.height * .016), + Text( + widget.details.supplier_name, + style: fontTextStyle(16, Color(0XFF2D2E30), FontWeight.w600), + ), + SizedBox(height: MediaQuery.of(context).size.height * .020), + + // πŸ”Ή Grouped Items + ...groupedData.entries.map((entry) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(entry.key, + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500)), + SizedBox(height: MediaQuery.of(context).size.height * .020), + Wrap( + spacing: 8, + runSpacing: 8, + children: entry.value.map((item) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: item.isStrikethrough + ? Colors.black87 + : item.isSelected + ? Colors.black + : Colors.transparent, + border: Border.all( + color: item.isSelected + ? Colors.orange + : Colors.grey.shade400, + ), + borderRadius: BorderRadius.circular(30), + ), + child: Text( + item.capacity + ' L', + style: TextStyle( + color: item.isStrikethrough ? Colors.white : Colors.black, + decoration: item.isStrikethrough + ? TextDecoration.lineThrough + : TextDecoration.none, + ), + ), + ); + }).toList(), + ), + SizedBox(height: MediaQuery.of(context).size.height * .020), + Divider(color: Colors.grey.shade300,), + ], + ); + }).toList(), + SizedBox(height: MediaQuery.of(context).size.height * .016), + // πŸ”Ή Quantity Row: Comes immediately after list + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Quantity', + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + Row( + children: [ + GestureDetector( + onTap: () { + decrement(); + }, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Color(0XFFC9DFFE), + shape: BoxShape.circle, + ), + child: Center( + child: Image.asset( + 'images/minus.png', + width: 12, + height: 12, + ), + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Text( + quantity.toString(), + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w400), + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + GestureDetector( + onTap: () { + increment(); + }, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: Color(0XFFC9DFFE), + shape: BoxShape.circle, + ), + child: Center( + child: Image.asset( + 'images/plus.png', + width: 12, + height: 12, + ), + ), + ), + ), + ], + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Divider(color: Colors.grey.shade300,), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Text( + 'Pump', + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + SizedBox(height: MediaQuery.of(context).size.height * .004), + Row( + children: [ + Row( + children: [ + Radio( + value: 'Yes', + groupValue: _selectedOption, + activeColor: primaryColor, + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (value) { + setState(() { + _selectedOption = value; + }); + }, + ), + Text('Yes',style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500),), + ], + ), + SizedBox(width: MediaQuery.of(context).size.width * .040), + Row( + children: [ + Radio( + value: 'No', + groupValue: _selectedOption, + activeColor: primaryColor, + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onChanged: (value) { + setState(() { + _selectedOption = value; + }); + }, + ), + Text('No',style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500),), + ], + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Divider(color: Colors.grey.shade300,), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Date of delivery', + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFF6F6F6), + border: Border.all( + width: 1, + color: Color(0XFFF6F6F6), + ), + borderRadius: + BorderRadius.circular( + 5, + )), + padding: EdgeInsets.fromLTRB(4, 4, 4,4), + child: Text( + displayDeliveryDate.toString(), + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w500), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .032), + GestureDetector( + onTap: () { + _pickDate(); + }, + child: Image.asset( + 'images/calender.png', + width: 20, + height: 20, + ), + ), + ], + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Divider(color: Colors.grey.shade300,), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Time of delivery', + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFF6F6F6), + border: Border.all( + width: 1, + color: Color(0XFFF6F6F6), + ), + borderRadius: + BorderRadius.circular( + 5, + )), + padding: EdgeInsets.fromLTRB(4, 4, 4,4), + child: Text( + _selectedTime1.format(context), + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w500), + ), + ), + + SizedBox(width: MediaQuery.of(context).size.width * .032), + GestureDetector( + onTap: () { + //_pickTime(); + //_showTimePicker(); + _selectTime(context); + }, + child: Image.asset( + 'images/clock.png', + width: 20, + height: 20, + ), + ), + ], + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Row( + children: [ + GestureDetector( + onTap: () {}, + child: Image.asset( + 'images/add_icon.png', + width: 24, + height: 24, + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .032), + Text( + 'Add order', + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Expanded(child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, + color: Color(0XFF1D7AFC), + ), + borderRadius: + BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB( + 16, 12, 16, 12), + child: Text('SAVE DRAFT', + style: fontTextStyle( + 12, + Color(0XFF1D7AFC), + FontWeight.w600)), + ), + ), + ),), + SizedBox( + width: MediaQuery.of(context) + .size + .width * + .032, + ), + Expanded(child: GestureDetector( + onTap: () { + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlaceOrder(details: connectedSuppliersList[index],)), + );*/ + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFF1D7AFC), + border: Border.all( + width: 1, + color: Color(0XFFFFFFFF), + ), + borderRadius: + BorderRadius.circular( + 12, + )), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB( + 16, 12, 16, 12), + child: Text('CONTINUE', + style: fontTextStyle( + 12, + Color(0XFFFFFFFF), + FontWeight.w600)), + ), + ), + )) + ], + ) + ], + ), + ); + } + + else{ + return Center( + child: Text( + 'No Data Available', + style: fontTextStyle( + 12, Color(0XFF000000), FontWeight.w500), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'Place Order', + style: fontTextStyle(14, Color(0XFF2A2A2A), FontWeight.w500), + ), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(10, 10, 0, 10), + child: IconButton( + icon: Image( + image: AssetImage( + 'images/calender_supplier_landing.png')), + onPressed: () {}, + ), + ), + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', // Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + 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, // Adjust the fit + ), + ), + ), + ), + body: isTankerDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ) + : renderUi(), + ); + } +} diff --git a/lib/supplier/plans/plan_details.dart b/lib/supplier/plans/plan_details.dart new file mode 100644 index 0000000..906c342 --- /dev/null +++ b/lib/supplier/plans/plan_details.dart @@ -0,0 +1,768 @@ +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:watermanagement/supplier/payment_example2.dart'; +import 'package:watermanagement/supplier/payment_screen.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; + +class PlanDetails extends StatefulWidget { + var supplierDetails; + PlanDetails({this.supplierDetails}); + + @override + State createState() => _PlanDetailsState(); +} + +class _PlanDetailsState extends State { + bool isExpanded = false; + double actualPrice = 0.0; + int quantity = 0; + double deliveryCharges = 0.0; + double subtotal = 0.0; + double cgst = 0.0; + double sgst = 0.0; + double total = 0.0; + double platformFee = 0.0; + double totalTaxes = 0.0; + double advance = 0.0; + double balance = 0.0; + late Razorpay _razorpay; + + @override + void initState() { + super.initState(); + + actualPrice = + double.tryParse(widget.supplierDetails.quotedAmount ?? '0') ?? 0; + quantity = int.tryParse(widget.supplierDetails.quantity ?? '0') ?? 0; + //deliveryCharges = double.tryParse(widget.supplierDetails.bookingCharges ?? '0') ?? 0;; + platformFee = 11; + subtotal = actualPrice * quantity; + cgst = subtotal * 0.09; + sgst = subtotal * 0.09; + totalTaxes = cgst + sgst; + total = subtotal + cgst + sgst + deliveryCharges + platformFee; + advance = deliveryCharges; + //balance = total - advance; + _razorpay = Razorpay(); + _razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, (PaymentSuccessResponse response) { + _handleSuccess(response, widget.supplierDetails); + }); + _razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handleError); + _razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet); + } + + Widget _buildAddressRow(String label, String address, Color color) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Image.asset( + 'images/location_supplier_landing.png', + height: 24, + width: 24, + fit: BoxFit.contain, + color: Color(0XFF939495), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Text( + '$label ', + style: fontTextStyle(14, color, FontWeight.bold), + ), + Expanded( + child: Text( + address, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ) + ], + ), + ); + } + + Widget _itemRow(String title, String price, {bool bold = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w400)), + Text(price, + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w400)), + ], + ), + ); + } + + + void _openCheckout(double amountInRupees) { + var options = { + 'key': 'rzp_test_1VCCWqEXUHdINz', // Replace with your Razorpay Key + 'amount': (amountInRupees * 100) + .toInt(), // Convert rupees to paise and ensure integer + 'name': 'Test Payment', + 'description': 'Water Order', + 'prefill': { + 'contact': '9876543210', + 'email': 'test@example.com', + } + }; + + try { + _razorpay.open(options); + } catch (e) { + print('Error: $e'); + } + } + + /* void _handleSuccess(PaymentSuccessResponse response) { + _showAlert('Payment Successful', 'Payment ID: ${response.paymentId}'); + }*/ + + void _handleSuccess(PaymentSuccessResponse response, dynamic object) async { + if (!mounted) return; // ensure widget is still active + + var payload = {}; + + payload["action"] = 'accept'; + payload["ref_number"] = response.paymentId; + payload["payment_type"] = object.paymentType; + + try { + bool status = await AppSettings.acceptPlanRequestsWithPayment(payload, object.dbId); + + if (!mounted) return; + + if (status) { + AppSettings.longSuccessToast('Accepted'); + + Navigator.pop(context, true); + + // Use Navigator safely + /* Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => OrderDetails(supplierDetails: object), + ), + );*/ + } else { + AppSettings.longFailedToast('Failed to accept order request'); + } + } catch (e) { + AppSettings.longFailedToast('Error: $e'); + } + } + + void _handleError(PaymentFailureResponse response) { + _showAlert('Payment Failed', '${response.code} - ${response.message}'); + } + + void _handleExternalWallet(ExternalWalletResponse response) { + _showAlert('Wallet Selected', '${response.walletName}'); + } + + void _showAlert(String title, String content) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text(title), + content: Text(content), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('OK')) + ], + ), + ); + } + + @override + void dispose() { + _razorpay.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final int totalOrders = widget.supplierDetails.dates.length; + final double biddingPrice = + double.tryParse(widget.supplierDetails.biddingPrice) ?? 0; + + final double totalAmount = totalOrders * biddingPrice; + final num total = totalAmount is num + ? totalAmount + : num.tryParse(totalAmount.toString()) ?? 0; + + final num advance = widget.supplierDetails.advanceAmount is num + ? widget.supplierDetails.advanceAmount + : num.tryParse(widget.supplierDetails.advanceAmount.toString()) ?? 0; + + final num balance = total - advance; + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: AppSettings.supplierAppBarWithoutActions( + widget.supplierDetails.supplierName, context), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Column( + children: [ + _buildAddressRow(widget.supplierDetails.supplierName, + '| ' + widget.supplierDetails.address, Color(0XFF4692FD)), + Padding( + padding: const EdgeInsets.only( + left: 8.0, bottom: 4.0), // πŸ‘ˆ Shift right + child: Align( + alignment: Alignment.centerLeft, + child: Image.asset( + 'images/Line.png', + height: 12, + width: 6, + fit: BoxFit.contain, + ), + ), + ), + _buildAddressRow('Home', '| ' + AppSettings.userAddress, + Color(0XFF2D2E30)), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(8), + top: Radius.circular(8), + ), // match Card border + gradient: LinearGradient( + colors: [ + Color(0xFFFFF8DF), + Color(0xFFFFFFFF), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + widget + .supplierDetails.supplierName, + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600)), + Text( + widget.supplierDetails + .distanceInMeters + .toString() + + ' Km', + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w600)), + ], + ), + Text( + widget.supplierDetails.displayAddress, + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w400)), + ], + ))), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + color: Color(0XFFF5F6F6), // Same as Card color + borderRadius: + BorderRadius.circular(4), // Rounded corners + border: Border.all( + color: Color(0XFFF5F6F6), // Border color + width: 0.5, // Border width + ), + ), + padding: EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Text('ITEMS', + style: fontTextStyle(10, Color(0XFF646566), + FontWeight.w400)), + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Padding( + padding: EdgeInsets.fromLTRB(4, 0, 4, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.supplierDetails.capacity + + ' - ' + + widget.supplierDetails.typeofwater+' - ' + + widget.supplierDetails.frequency+'/week', + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500), + ), + Row( + children: [ + Text('Actual Price: ', style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400),), + Text( + 'β‚Ή' + + AppSettings.formDouble(widget.supplierDetails.quotedAmount) + + '/', + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500), + ), + Text( + widget.supplierDetails.capacity, + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500), + ) + ], + ), + Row( + children: [ + Text('Bidding Price: ', style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400),), + Text( + 'β‚Ή' + + AppSettings.formDouble(widget.supplierDetails.biddingPrice) + + '/', + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500), + ), + Text( + widget.supplierDetails.capacity, + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500), + ) + ], + ), + Row( + children: [ + Text('Payment Type: ', style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400),), + Text( + widget.supplierDetails.paymentType, + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w600), + ), + ], + ), + Visibility( + visible:widget.supplierDetails.paymentType!='after_delivery' , + child: Row( + children: [ + Text(widget.supplierDetails.paymentType=='credit'?'Credit Limit: ':'Advance Amount: ', style: fontTextStyle( + 12, + Color(0XFF646566), + FontWeight.w400),), + Text( + widget.supplierDetails.advanceAmount, + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w600), + ), + ], + ),), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Total amount for this plan orders ", + style: fontTextStyle( + 12, + const Color(0xFF939495), + FontWeight.w400, + ), + ), + TextSpan( + text: + '$totalOrders Γ— ${biddingPrice.toStringAsFixed(0)} = β‚Ή${totalAmount.toStringAsFixed(0)}', + style: fontTextStyle( + 12, + const Color(0xFF515253), + FontWeight.w500, + ), + ), + ], + ), + softWrap: true, + maxLines: 2, + overflow: TextOverflow.visible, + textAlign: TextAlign.center, + ) + ], + ) + + + + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + color: Color(0XFFF5F6F6), // Same as Card color + borderRadius: + BorderRadius.circular(4), // Rounded corners + border: Border.all( + color: Color(0XFFF5F6F6), // Border color + width: 0.5, // Border width + ), + ), + padding: EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Text('DELIVERY DETAILS', + style: fontTextStyle(10, Color(0XFF646566), + FontWeight.w400)), + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Padding( + padding: EdgeInsets.fromLTRB(4, 0, 4, 0), + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Plan Period', + style: fontTextStyle(12, + Color(0XFF515253), FontWeight.w500), + ), + Visibility( + visible: widget.supplierDetails.startDate != + '', + child: Text( + widget.supplierDetails.startDate + + ' to ' + + widget.supplierDetails.endDate, + style: fontTextStyle( + 12, + Color(0XFF232527), + FontWeight + .w500), + ), + ) + + ], + ), + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Align( + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + color: + Color(0XFFFFFFFF), // Same as Card color + borderRadius: BorderRadius.circular( + 4), // Rounded corners + border: Border.all( + color: Color(0XFF939495), // Border color + width: 0.5, // Border width + ), + ), + padding: EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Text('Change Delivery Details', + style: fontTextStyle(14, + Color(0XFF646566), FontWeight.w500)), + )), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + ], + ), + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 12, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text('SAVINGS', + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400)), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Row( + children: [ + Image.asset( + 'images/coupon.png', + height: 16, + width: 16, + fit: BoxFit.contain, + ), + SizedBox( + width: MediaQuery.of(context).size.width * + .012), + Text( + 'Apply Coupon', + style: fontTextStyle( + 12, + Color(0XFF2D2E30), + FontWeight.w500, + ), + ), + Spacer(), // pushes the arrow to the right + Image.asset( + 'images/arrow-right.png', + height: 20, + width: 20, + fit: BoxFit.contain, + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + ], + ), + )), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Column( + children: [ + // Header card + GestureDetector( + onTap: () => setState(() => isExpanded = !isExpanded), + child: Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.fromLTRB(12, 12, 12, 12), + child: Row( + children: [ + Image.asset( + 'images/receipt.png', + height: 16, + width: 16, + fit: BoxFit.contain, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * .012, + ), + Expanded( + child: Text( + 'To Pay β‚Ή${AppSettings.moneyConvertion(widget.supplierDetails.advanceAmount.toString())}', + style: fontTextStyle(12, + Color(0XFF2E302D), FontWeight.w600)), + ), + isExpanded + ? Image.asset( + 'images/arrow-up.png', + height: 20, + width: 20, + fit: BoxFit.contain, + ) + : Image.asset( + 'images/arrow-down.png', + height: 20, + width: 20, + fit: BoxFit.contain, + ) + ], + ), + ), + ), + ), + AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: isExpanded ? null : 0, + padding: + isExpanded ? EdgeInsets.all(0) : EdgeInsets.zero, + child: isExpanded + ? Card( + color: Color(0XFFFFFFFF), + child: Padding( + padding: EdgeInsets.all(8), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text('ITEMS', + style: fontTextStyle( + 14, + Color(0XFF2D2E30), + FontWeight.w500)), + SizedBox( + height: + MediaQuery.of(context).size.height * + .012, + ), + _itemRow( + widget.supplierDetails.capacity + + ' ' + + widget + .supplierDetails.typeofwater + + ' x ' + + widget.supplierDetails.quantity, + 'β‚Ή' + + AppSettings.moneyConvertion( + biddingPrice.toString())), + Divider(), + _itemRow('Total Amount For Plan', + 'β‚Ή${AppSettings.moneyConvertion(totalAmount.toStringAsFixed(0))}'), + _itemRow('Delivery Charges', + 'β‚Ή 0.0'), + //_itemRow('Platform Fee', 'β‚Ή${AppSettings.moneyConvertion(platformFee.toString())}'), + _itemRow('Platform Fee', 'β‚Ή 0.0'), + _itemRow('Taxes', 'β‚Ή 0.0'), + Divider(), + _itemRow('Total Bill', + 'β‚Ή${AppSettings.moneyConvertion(totalAmount.toString())}', + bold: true), + Divider(), + _itemRow('Advance', 'β‚Ή${AppSettings.moneyConvertion(widget.supplierDetails.advanceAmount.toString())}'), + _itemRow('To pay(after delivery)', + 'β‚Ή${AppSettings.moneyConvertion(balance.toString())}'), + ], + ), + ), + ) + : null, + ), + ], + ) + ], + )), + ), + bottomNavigationBar: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide(color: Color(0XFFC3C4C4), width: 0.8), + ), + boxShadow: [ + BoxShadow( + color: Color(0XFFC3C4C4), + blurRadius: 0, + offset: Offset(0, 0), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + // Button 1: Pay Advance + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF0A9E04), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.symmetric(vertical: 12), // Only vertical padding + ), + onPressed: () { + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RazorpayScreen()), + );*/ + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PaymentOptionsPage(amount: advance), + ), + );*/ + _openCheckout(double.parse(widget.supplierDetails.advanceAmount)); + /*Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UpiIntentLauncher(), + ), + );*/ + }, + child: Text( + "Pay advance β‚Ή${AppSettings.moneyConvertion(advance.toString())}", + style: fontTextStyle(14, Color(0XFFFFFFFF), FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ),// spacing between buttons + // Button 2: Pay Full + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF1D7AFC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.symmetric(vertical: 12), + ), + onPressed: () { + // Handle full payment + }, + child: Text( + "Pay full β‚Ή${AppSettings.moneyConvertion(total.toStringAsFixed(2))}", + style: fontTextStyle(14, Color(0XFFFFFFFF), FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + + ); + } +} diff --git a/lib/supplier/plans/plan_request_screen.dart b/lib/supplier/plans/plan_request_screen.dart new file mode 100644 index 0000000..c1760b4 --- /dev/null +++ b/lib/supplier/plans/plan_request_screen.dart @@ -0,0 +1,1830 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:watermanagement/supplier/filter_screen.dart'; +import 'package:watermanagement/supplier/plans/plan_suppliers_model.dart'; +import 'package:watermanagement/supplier/supplier_details.dart'; +import 'package:watermanagement/supplier/supplier_details_from_search.dart'; +import 'package:watermanagement/supplier/suppliers_model.dart'; + +class PlanRequestScreen extends StatefulWidget { + const PlanRequestScreen({super.key}); + + @override + State createState() => _PlanRequestScreenState(); +} + +class _PlanRequestScreenState extends State { + String? selectedWaterType; + String? selectedFrequency; + String? selectedCapacity; + String? selectedQuantity; + String? selectedTime; + DateTime? selectStartDate; + DateTime? selectStopDate; + List PlanSuppliersList = []; + bool isDataLoading = false; + bool isSearchEnabled = false; + int sentRequests = 0; + int maxRequests = 5; + DateTime? firstRequestTime; + Timer? _resetTimer; + Timer? _countdownTimer; + String remainingTime = ''; + double progress = 1.0; + int totalCooldown = 900; // 15 minutes in seconds + + RangeValues? selectedRadius; + RangeValues? selectedRating; + RangeValues? selectedPrice; + String? selectedPump; + bool filterAll = true, filterConnected = false, filterNotConnected = false; + final TextEditingController _quotedAmountController = TextEditingController(); + + + // TimeOfDay _selectedTime = (TimeOfDay.now()); + + final List waterTypes = ['Drinking Water', 'Bore water']; + final List frequencyTypes = ['Daily', 'Weekly Once','Weekly Twice','Weekly Thrice']; + final List capacities = [ + '1,000 L', + '2,000 L', + '3,000 L', + '5,000 L', + '10,000 L', + '20,000 L' + ]; + final List quantities = ['1', '2', '3']; + final List timeSlots = ['8 AM', '12 PM', '4 PM', '8 PM']; + + @override + void initState() { + // TODO: implement initState + super.initState(); + _selectedTime = _roundToNextHour(TimeOfDay.now()); + _loadRequestState(); + // Defer the bottom sheet call until after build + WidgetsBinding.instance.addPostFrameCallback((_) { + _showLocationBottomSheet(); + }); + } + + @override + void dispose() { + _countdownTimer?.cancel(); + _quotedAmountController.dispose(); + super.dispose(); + } + + Future _loadRequestState() async { + List validTimes = await _getValidRequestTimes(); + validTimes.sort(); // Ensure chronological order + setState(() { + sentRequests = validTimes.length; + }); + + if (validTimes.isNotEmpty) { + final firstExpiry = validTimes.first.add(Duration(minutes: 15)); + final remaining = firstExpiry.difference(DateTime.now()).inSeconds; + if (remaining > 0) { + _startCountdownTimer(remaining); + } + } + } + + Future> _getValidRequestTimes() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + final rawList = prefs.getStringList('requestTimes') ?? []; + final now = DateTime.now(); + final validTimes = rawList + .map((s) => DateTime.tryParse(s)) + .whereType() + .where((t) => now.difference(t).inMinutes < 15) + .toList(); + return validTimes; + } + + Future _saveRequestTimes(List times) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setStringList('requestTimes', times.map((e) => e.toIso8601String()).toList()); + } + + void _startCountdownTimer(int totalSeconds) { + _countdownTimer?.cancel(); + totalCooldown = totalSeconds; + progress = 1.0; + + _countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) async { + final elapsed = timer.tick; + final remaining = totalSeconds - elapsed; + + if (remaining <= 0) { + timer.cancel(); + + // 🧹 Clean expired request times + List updatedTimes = await _getValidRequestTimes(); + updatedTimes.sort(); // Ensure order + await _saveRequestTimes(updatedTimes); + + setState(() { + sentRequests = updatedTimes.length; + remainingTime = ''; + progress = 0.0; + }); + + // πŸ• If more valid requests remain, restart countdown with new first one + if (updatedTimes.isNotEmpty) { + final nextExpiry = updatedTimes.first.add(Duration(minutes: 15)); + final newRemaining = nextExpiry.difference(DateTime.now()).inSeconds; + if (newRemaining > 0) { + _startCountdownTimer(newRemaining); + } + } + + } else { + final minutes = (remaining ~/ 60).toString().padLeft(2, '0'); + final seconds = (remaining % 60).toString().padLeft(2, '0'); + setState(() { + remainingTime = "$minutes:$seconds"; + progress = remaining / totalCooldown; + }); + } + }); + } + + + + + void _showLocationBottomSheet() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, // πŸ”’ Prevents dragging + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Container( + padding: EdgeInsets.all(16), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.45, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Confirm Address?", + style: fontTextStyle(16, Color(0XFF343637), FontWeight.w600), + ), + SizedBox(height: MediaQuery.of(context).size.height * .024), + Row( + children: [ + Text( + "Your current selected address is", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w600), + ), + SizedBox(width: MediaQuery.of(context).size.width * .004), + Text( + "HOME", + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w600), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + children: [ + Image.asset( + 'images/marker-pin.png', + width: 24, + height: 24, + ), + SizedBox(width: MediaQuery.of(context).size.width * .024), + Expanded( + child: Text( + AppSettings.userAddress, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ) + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () { + // Optional: handle "Change" + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(width: 1, color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Change', + style: fontTextStyle(14, Color(0XFF939495), FontWeight.w600), + ), + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .010), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + /* setState(() { + _isConfirmed = true; + });*/ + }, + child: Container( + decoration: BoxDecoration( + color: Color(0XFF1D7AFC), + border: Border.all(width: 1, color: Colors.white), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Confirm', + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } + + + + + TimeOfDay _roundToNextHour(TimeOfDay time) { + int nextHour = (time.minute > 0) ? (time.hour + 1) % 24 : time.hour; + return TimeOfDay(hour: nextHour, minute: 0); + } + + void sendRequest() { + if (sentRequests < maxRequests) { + setState(() { + sentRequests++; + }); + } + } + + Future _selectFromDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: selectStartDate ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(Duration(days: 3)), + helpText: 'Select Date', + initialEntryMode: DatePickerEntryMode.calendarOnly, + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + inputDecorationTheme: InputDecorationTheme( + border: InputBorder.none, // Removes the text field border + ), + colorScheme: ColorScheme.light( + primary: Color(0XFF1D7AFC), + onPrimary: Color(0XFFFFFFFF), // Header text color + surface: Color(0XFFFFFFFF), + onSurface: Colors.black, + secondary: Colors.pink, + ), + dividerColor: Color(0XFFF5F6F6), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + Color(0XFF1D7AFC), + ), // Text color + //backgroundColor: MaterialStateProperty.all(Colors.grey[200]), // Background + textStyle: MaterialStateProperty.all( + fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + ), // Font style + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8))), // Rounded corners + ), + // Background of the dialog box + ), + textTheme: TextTheme( + bodyLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + labelLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + titleLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + headlineLarge: + fontTextStyle(20, Color(0XFF1D7AFC), FontWeight.w600), + ), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + setState(() { + selectStartDate = picked; + + //toDate1 = null; // Reset To Date + //toDateController1.clear(); + }); + } + } + + Future _selectToDate(BuildContext context) async { + if (selectStartDate == null) { + // Handle case when start date is not selected + return; + } + + final DateTime firstDate = selectStartDate!; + final DateTime lastDate = DateTime( + firstDate.year, + firstDate.month + 1, + firstDate.day, + ); + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: selectStopDate ?? firstDate, + firstDate: firstDate, + lastDate: lastDate, + helpText: 'Select Date', + initialEntryMode: DatePickerEntryMode.calendarOnly, + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + ), + colorScheme: const ColorScheme.light( + primary: Color(0XFF1D7AFC), + onPrimary: Colors.white, + surface: Colors.white, + onSurface: Colors.black, + secondary: Colors.pink, + ), + dividerColor: Color(0XFFF5F6F6), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStatePropertyAll(Color(0XFF1D7AFC)), + textStyle: MaterialStatePropertyAll( + fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + ), + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + ), + textTheme: TextTheme( + bodyLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + labelLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + titleLarge: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + headlineLarge: fontTextStyle(20, Color(0XFF1D7AFC), FontWeight.w600), + ), + ), + child: child ?? const SizedBox.shrink(), + ); + }, + ); + + if (picked != null) { + setState(() { + selectStopDate = picked; + }); + } + } + + + TimeOfDay _selectedTime = TimeOfDay.now(); + String _timeRangeText = ''; + + + Future _selectTime(BuildContext context) async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: _selectedTime, + cancelText: 'Cancel', + confirmText: 'Confirm', + builder: (BuildContext context, Widget? child) { + return Theme( + data: ThemeData.light().copyWith( + primaryColor: Color(0XFF1D7AFC), + timePickerTheme: TimePickerThemeData( + backgroundColor: Colors.white, + dialBackgroundColor: Colors.white, + hourMinuteTextColor: Color(0XFF1D7AFC), + dayPeriodTextColor: Color(0XFF1D7AFC), + dialTextColor: Color(0XFF1D7AFC), + dayPeriodColor: Color(0XFFC3C4C4), + hourMinuteShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: Color(0XFFFFFFFF), width: 2), + ), + dayPeriodShape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + dialHandColor: Color(0XFFC3C4C4), + hourMinuteColor: Color(0XFFFFFFFF), + dialTextStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + dayPeriodTextStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + hourMinuteTextStyle: + TextStyle(fontSize: 50, fontWeight: FontWeight.w600), + helpTextStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + child: child!, + ); + }, + ); + + if (picked != null) { + final now = TimeOfDay.now(); + final today = DateTime.now(); + final isToday = selectStartDate?.day == today.day && + selectStartDate?.month == today.month && + selectStartDate?.year == today.year; + + bool isValid = true; + + if (isToday) { + final nowMinutes = now.hour * 60 + now.minute; + final pickedMinutes = picked.hour * 60 + picked.minute; + isValid = pickedMinutes >= nowMinutes; + } + + if (isValid) { + setState(() { + _selectedTime = picked; + _timeRangeText = _formatTimeRange(_selectedTime); + }); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Please select a time after the current time.")), + ); + } + } + } + + String _formatTimeRange(TimeOfDay start) { + final endHour = (start.hour + 2) % 24; + final end = TimeOfDay(hour: endHour, minute: start.minute); + return '${_formatTime(start)} to ${_formatTime(end)}'; + } + + String _formatTime(TimeOfDay time) { + final now = DateTime.now(); + final dt = DateTime(now.year, now.month, now.day, time.hour, time.minute); + return TimeOfDay.fromDateTime(dt).format(context); + } + + Widget _beforeDataScreen() { + return Padding(padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "What’s new?", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + RichText( + text: TextSpan( + text: "Find ", + style: fontTextStyle(20, Color(0XFF343637), FontWeight.w600), + children: [ + TextSpan( + text: "exclusive offers", + style: fontTextStyle(20, Color(0XFF8270DB), FontWeight.w600), + ), + TextSpan( + text: " & best deals available for you.", + style: fontTextStyle(20, Color(0XFF343637), FontWeight.w600), + ), + ], + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .024), + Container( + height: 80, + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + Color(0xFFFFFFFF), + Color(0xFFE2DCFF) + ], // Light gradient background + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + border: Border.all(color: Color(0xFF8270DB)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AQUINTO', + style: + fontTextStyle(20, Color(0XFF8270DB), FontWeight.w800), + ), + SizedBox(height: MediaQuery.of(context).size.height * .004), + Text( + 'Your Water, Your Data, Your Control.', + style: + fontTextStyle(12, Color(0XFF8270DB), FontWeight.w500), + ), + ], + ), + /*Image.asset( + 'images/WaterTankImage.png', // Replace with your actual image asset path + height: 40, + width: 40, + fit: BoxFit.contain, + ),*/ + ], + ), + ) + ], + ),); + } + + Widget _beforeDataScreenAbove() { + return Column( + children: [ + SizedBox(height: MediaQuery.of(context).size.height * .024), + CircleAvatar(radius: 80, backgroundColor: Colors.grey.shade300), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Center( + child: Text("Plans", + style: fontTextStyle(20, Color(0XFF000000), FontWeight.w600)), + ), + SizedBox(height: MediaQuery.of(context).size.height * .008), + Text( + "No more last-minute orders! Set up a regular water delivery schedule with this supplier and stay stress-free", + textAlign: TextAlign.center, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ], + ); + } + + Widget _suppliersDataScreen() { + if (isDataLoading) { + return Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5.0, + ), + ); + } + + if (PlanSuppliersList.isEmpty) { + return Center( + child: Text( + 'No Data Available', + style: fontTextStyle(12, Color(0XFF000000), FontWeight.w500), + ), + ); + } + + return Container( + color: Color(0xFFF1F1F1), // Set your desired background color + padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 0, vertical: 12), + decoration: BoxDecoration( + color: Color(0xFFF1F1F1), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Row( + children: [ + Image.asset( + 'images/info.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + SizedBox( + width: MediaQuery.of(context).size.width * .008, + ), + Text( + "Plan requests sent", + style: fontTextStyle(14, Color(0XFF2D2E30), FontWeight.w600), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .012, + ), + Text("$sentRequests/$maxRequests",style: fontTextStyle(14, Color(0XFF515253), FontWeight.w600),), + Spacer(), + Text("$remainingTime",style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400),), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .016, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(maxRequests, (index) { + return Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 4), + height: 3, + decoration: BoxDecoration( + color: index < sentRequests + ?Color(0XFF114690) + :Color(0XFF939495), + borderRadius: BorderRadius.circular(2), + ), + ), + ); + }), + ), + Padding( + padding: EdgeInsets.only(top: 12, right: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: (){}, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF515253)), + borderRadius: BorderRadius.circular(45), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'images/sort.png', + width: 12, + height: 12, + color: Color(0XFF515253), + fit: BoxFit.contain, + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Text( + "Sort", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF515253), + fit: BoxFit.contain, + ), + ], + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .016, + ), + GestureDetector( + onTap: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => FilterScreen( + initialFilters: { + 'all': filterAll, + 'connected': filterConnected, + 'notConnected': filterNotConnected, + 'radiusRange': selectedRadius, + 'ratingRange': selectedRating, + 'priceRange': selectedPrice, + 'pumpChoice': selectedPump, + }, + ), + ), + ); + + if (result != null && result is Map) { + setState(() { + filterAll = result['all']; + filterConnected = result['connected']; + filterNotConnected = result['notConnected']; + selectedRadius = result['radiusRange']; + selectedRating = result['ratingRange']; + selectedPrice = result['priceRange']; + selectedPump = result['pumpChoice']; + }); + + await getAllSuppliersPlans(); + } + }, + + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF515253)), + borderRadius: BorderRadius.circular(45), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'images/filter.png', + width: 12, + height: 12, + color: Color(0XFF515253), + fit: BoxFit.contain, + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Text( + "Filter", + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ], + ), + ), + ) + ], + ), + ) + + ], + )), + ], + ), + ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: PlanSuppliersList.length, + separatorBuilder: (context, index) => SizedBox( + height: MediaQuery.of(context).size.height * .008, + ), + itemBuilder: (BuildContext context, int index) { + //final supplier = SuppliersList[index]; + return GestureDetector( + onTap: (){ + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SupplierScreenFromSearch(details: PlanSuppliersList[index],)), + ); + }, + child:Card( + color: Colors.white, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: EdgeInsets.all(12), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 25, + backgroundColor: Color(0XFFE8F2FF), + child: Image.asset( + 'images/profile_user.png', + fit: BoxFit.cover, + width: 50, + height: 50, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .024, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Name and Rating Row + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + PlanSuppliersList[index].supplier_name, + style: fontTextStyle(16, + Color(0XFF2D2E30), FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + ), + Row( + children: [ + Image.asset( + 'images/star.png', + width: 16, + height: 16, + ), + SizedBox( + width: + MediaQuery.of(context).size.width * + .008, + ), + Text( + '4.2', + style: fontTextStyle(10, + Color(0XFF515253), FontWeight.w400), + ), + ], + ), + ], + ), + SizedBox( + height: + MediaQuery.of(context).size.height * .004, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Drinking | Bore Water', + style: fontTextStyle( + 12, Color(0XFF4692FD), FontWeight.w500), + ), + + Text( + PlanSuppliersList[index].matchedPrice.toString()!="null"&&PlanSuppliersList[index].matchedPrice.toString()!="" + ""? '\u20B9 ${AppSettings.formDouble(PlanSuppliersList[index].matchedPrice.toString())}': '', + style: fontTextStyle(10, Color(0XFF515253), FontWeight.w400), + ), + ], + ), + SizedBox( + height: + MediaQuery.of(context).size.height * .004, + ), + + /// Address and Favourite + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '${PlanSuppliersList[index].displayAddress} ${PlanSuppliersList[index].distanceInMeters} Km', + style: fontTextStyle(12, + Color(0XFF515253), FontWeight.w400), + overflow: TextOverflow.ellipsis, + ), + ), + GestureDetector( + onTap: () async { + AppSettings.preLoaderDialog(context); + if (PlanSuppliersList[index].isFavorite) { + try { + bool tankerResponse = + await AppSettings + .removeFavourites( + PlanSuppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longSuccessToast( + 'Supplier removed from favourites'); + await getAllSuppliersPlans(); + // await getConnectedSuppliersData(); + } else { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to remove from favourites'); + } + } catch (e) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to remove from favourites'); + } + } else { + try { + bool tankerResponse = + await AppSettings.addFavourites( + PlanSuppliersList[index] + .supplier_id); + if (tankerResponse) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longSuccessToast( + 'Supplier added to favourites'); + await getAllSuppliersPlans(); + //await getConnectedSuppliersData(); + //await getAllFavouritesData(); + } else { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to add favourites'); + } + } catch (e) { + Navigator.of(context, + rootNavigator: true) + .pop(); + AppSettings.longFailedToast( + 'Failed to add favourites'); + } + } + }, + child: Image.asset( + PlanSuppliersList[index].isFavorite + ? 'images/heart_active.png' + : 'images/heart_outline.png', + width: 20, + height: 20, + ), + ), + ], + ), + ], + ), + ), + ], + ), + SizedBox( + height: MediaQuery.of(context).size.height * .016, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFFFFFF), + border: Border.all( + width: 1, + color: PlanSuppliersList[index].isRequetsedBooking?Color(0XFF1D7AFC):Color(0XFF939495), + ), + borderRadius: BorderRadius.circular( + 24, + )), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Call', + style: fontTextStyle(12, PlanSuppliersList[index].isRequetsedBooking?Color(0XFF1D7AFC):Color(0XFF939495), + FontWeight.w600)), + ), + ), + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width * .010, + ), + Expanded( + child: GestureDetector( + onTap: () async{ + if(!PlanSuppliersList[index].isRequetsedBooking){ + requestPlanDialog(PlanSuppliersList[index]); + } + + /* Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlaceOrder(details: connectedSuppliersList[index],)), + );*/ + }, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: PlanSuppliersList[index].isRequetsedBooking?Color(0XFFDBDBDC):Color(0XFF1D7AFC), + border: Border.all( + width: 1, + color: Color(0XFFFFFFFF), + ), + borderRadius: BorderRadius.circular( + 24, + )), + alignment: Alignment.center,// + child: Padding( + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Request plan', + style: fontTextStyle(12, Color(0XFFFFFFFF), FontWeight.w600)), + ), + ), + )) + ], + ) + ], + ), + ), + ) , + ) + + ; + }, + + ) + ], + ), + ); + + + } + + requestPlanDialog(var details) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + backgroundColor: Color(0XFFFFFFFF),// Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center(child: Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text('ORDER DETAILS',style:fontTextStyle(16,Color(0XFF646566),FontWeight.w400)), + ), + SizedBox(height: MediaQuery.of(context).size.height * .024,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Water Type',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(selectedWaterType!,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Capacity',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(selectedCapacity!,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Quantity',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(selectedQuantity!,style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Start Date',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(DateFormat('dd-MMM-yyyy').format(selectStartDate!),style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('End Date',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text(DateFormat('dd-MMM-yyyy').format(selectStopDate!),style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500)), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Actual Price',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + Text( + details.matchedPrice.toString()!="null"&&details.matchedPrice.toString()!="" + ""? '\u20B9 ${AppSettings.formDouble(details.matchedPrice.toString())}': '', + style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .010,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Your Price',style:fontTextStyle(14,Color(0XFF646566),FontWeight.w500)), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4, // Adjust width as needed + child: TextFormField( + controller: _quotedAmountController, + keyboardType: TextInputType.number, + textCapitalization: TextCapitalization.sentences, + maxLength: 10, + decoration: textFormFieldDecoration(Icons.phone,''), + style:fontTextStyle(14,Color(0XFF2D2E30),FontWeight.w500), + cursorColor: Color(0XFF1D7AFC), + + //TextStyle(color: Colors.black,fontWeight: FontWeight.bold), + ) + ), + ], + ), + ], + ),), + actions: [ + Center( + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Color(0XFF1D7AFC), width: 1), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Cancel', + style: fontTextStyle(14, Color(0XFF1D7AFC), FontWeight.w600), + ), + ), + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .010,), + Expanded( + child: GestureDetector( + onTap: () async { + final now = DateTime.now(); + List validTimes = await _getValidRequestTimes(); + + if (validTimes.length >= maxRequests) { + AppSettings.longFailedToast("Maximum 5 requests allowed in any 15-minute window."); + return; + } + + final text = _quotedAmountController.text.trim(); + if (text.isEmpty) return; + + final quoted = int.tryParse(text); + if (quoted == null) { + AppSettings.longFailedToast("Invalid number"); + return; + } + + int matched = int.parse(details.matchedPrice); + if (quoted > matched + 300 || quoted < matched - 300) { + AppSettings.longFailedToast("Enter less than 300 within matched price ${details.matchedPrice}"); + return; + } + + AppSettings.preLoaderDialog(context); + bool isOnline = await AppSettings.internetConnectivity(); + + if (!isOnline) { + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast("Check your internet connection."); + return; + } + String freq=''; + if(selectedFrequency.toString().toLowerCase()=='daily'){ + freq='daily'; + } + else if(selectedFrequency.toString().toLowerCase()=='weekly once'){ + freq='weekly_once'; + } + else if(selectedFrequency.toString().toLowerCase()=='weekly twice'){ + freq='weekly_twice'; + } + else if(selectedFrequency.toString().toLowerCase()=='weekly thrice'){ + freq='weekly_thrice'; + } + + var payload = { + + "customerId": AppSettings.customerId, + "type_of_water":selectedWaterType, + "capacity":selectedCapacity, + "quantity":selectedQuantity, + "start_date": DateFormat('dd-MMM-yyyy').format(selectStartDate!), + "end_date": DateFormat('dd-MMM-yyyy').format(selectStopDate!), + "time": DateFormat('dd-MM-yyyy HH:mm').format(now), + "frequency": freq, + "weekly_count": 1, + "requested_suppliers": [ + { + "supplierId": details.supplier_id, + "quoted_amount": quoted, + "time": DateFormat('dd-MM-yyyy HH:mm').format(now), + } + ], + }; + + bool status = await AppSettings.requestPlan(payload); + Navigator.of(context, rootNavigator: true).pop(); + + if (status) { + _quotedAmountController.clear(); + Navigator.pop(context); + validTimes.add(now); + validTimes.sort(); + await _saveRequestTimes(validTimes); + sentRequests = validTimes.length; + + if (validTimes.isNotEmpty) { + final firstExpiry = validTimes.first.add(Duration(minutes: 15)); + final remaining = firstExpiry.difference(DateTime.now()).inSeconds; + if (remaining > 0) _startCountdownTimer(remaining); + } + + setState(() {}); + await getAllSuppliersPlans(); + AppSettings.longSuccessToast("Request Sent"); + } else { + AppSettings.longFailedToast("Request sending failed"); + } + /* final text = _quotedAmountController.text.trim(); + + if (text.isNotEmpty) { + final quoted = int.tryParse(text); + if (quoted == null) { + // Handle invalid number if needed + } else if (quoted > int.parse(details.matchedPrice) + 300 || quoted < int.parse(details.matchedPrice) - 300) { + AppSettings.longFailedToast("Please Enter below 300 or above 300 for the selected tanker price ${details.matchedPrice}"); + } else { + AppSettings.preLoaderDialog(context); + bool isOnline = await AppSettings.internetConnectivity(); + + if (isOnline) { + + var payload = { + "customerId": AppSettings.customerId, + "type_of_water": selectedWaterType, + "capacity": selectedCapacity, + "quantity": selectedQuantity, + "date": DateFormat('dd-MMM-yyyy').format(selectedDate!), // e.g., '2025-07-01' + "time": _timeRangeText, // e.g., '14:00' + "requested_suppliers": [ + { + "supplierId": details.supplier_id, + "quoted_amount": int.parse(_quotedAmountController.text), + "time": DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now()), + } + ], + }; + bool status = await AppSettings.requestOrder(payload); + if(status){ + _quotedAmountController.clear(); + Navigator.of(context, rootNavigator: true).pop(); + Navigator.pop(context); + await getAllSuppliers(); + AppSettings.longSuccessToast("Request Sent"); + } + else{ + _quotedAmountController.clear(); + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast("Request sending failed"); + } + } else { + Navigator.of(context, rootNavigator: true).pop(); + AppSettings.longFailedToast("Please Check internet"); + } + } + }*/ + + }, + child: Container( + decoration: BoxDecoration( + color: Color(0XFF1D7AFC), + border: Border.all(color: Color(0XFF1D7AFC), width: 1), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + 'Send Request', + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ); + }); + }); + } + + Future getAllSuppliersPlans() async { + AppSettings.preLoaderDialog(context); + + bool isOnline = await AppSettings.internetConnectivity(); + + String freq=''; + if(selectedFrequency.toString().toLowerCase()=='daily'){ + freq='daily'; + } + else if(selectedFrequency.toString().toLowerCase()=='weekly once'){ + freq='weekly_once'; + } + else if(selectedFrequency.toString().toLowerCase()=='weekly twice'){ + freq='weekly_twice'; + } + else if(selectedFrequency.toString().toLowerCase()=='weekly thrice'){ + freq='weekly_thrice'; + } + + if (isOnline) { + var payload = new Map(); + payload["type_of_water"] = selectedWaterType.toString(); + payload["capacity"] = selectedCapacity.toString(); + payload["quantity"] = selectedQuantity.toString(); + payload["frequency"] = freq; + payload["start_date"] = DateFormat('dd-MMM-yyyy').format(selectStartDate!); + payload["end_date"] = DateFormat('dd-MMM-yyyy').format(selectStopDate!); + + payload["radius_from"] = selectedRadius?.start.round().toString() ?? ""; + payload["radius_to"] = selectedRadius?.end.round().toString() ?? ""; + payload["rating_from"] = selectedRating?.start.toString() ?? ""; + payload["rating_to"] = selectedRating?.end.toString() ?? ""; + + payload["price_from"] = selectedPrice?.start.round().toString() ?? ""; + payload["price_to"] = selectedPrice?.end.round().toString() ?? ""; + payload["pump"] = selectedPump.toString(); + setState(() { + isDataLoading = true; + }); + try { + var response = await AppSettings.getSuppliersForPlan(payload); + + setState(() { + PlanSuppliersList = + ((jsonDecode(response)['suppliers']) as List) + .map((dynamic model) { + return PlanSuppliersModel.fromJson(model); + }).toList(); + // Clean the selected value: remove comma and " L" + String normalizedSelectedLitres = selectedCapacity!.replaceAll(",", "").replaceAll(" L", "").trim(); + + List suppliersJson = jsonDecode(response)['suppliers']; + + PlanSuppliersList = suppliersJson.map((supplierData) { + List tankers = supplierData['tankers']; + String? matchedPrice; + + for (var tanker in tankers) { + String capacity = tanker['capacity'].replaceAll(",", "").trim(); + if (capacity == normalizedSelectedLitres) { + matchedPrice = tanker['price']; + break; + } + } + + // Optional: attach matchedPrice to model if supported + var supplierModel = PlanSuppliersModel.fromJson(supplierData); + supplierModel.matchedPrice = matchedPrice; // <- Add this field to your model class + return supplierModel; + }).toList(); + isDataLoading = false; + Navigator.of(context, rootNavigator: true).pop(); + }); + } catch (e) { + setState(() { + isDataLoading = false; + Navigator.of(context, rootNavigator: true).pop(); + }); + } + } else { + AppSettings.longFailedToast('Please check internet connection'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Color(0XFFFFFFFF), + elevation: 0, + scrolledUnderElevation: 0, + titleSpacing: 0, + title: GestureDetector( + onTap: () { + _showLocationBottomSheet(); + }, + child:Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisSize: + MainAxisSize.min, // Ensures it doesn't take extra space + children: [ + Text( + 'Home', + style: + fontTextStyle(16, Color(0XFF232527), FontWeight.w600), + ), + SizedBox(width: 5), // Space between text and icon + Image.asset( + 'images/arrow_down.png', // Replace with your arrow image + width: 24, // Adjust size + height: 24, + ) + ], + ), + Text( + AppSettings.userAddress, + style: + fontTextStyle(10, Color(0XFF515253), FontWeight.w400), + ), + ], + ) + ), + iconTheme: IconThemeData(color: Color(0XFF2A2A2A)), + actions: [ + Row( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 10, 10, 10), + child: IconButton( + icon: Image.asset( + 'images/notification_appbar.png', // Example URL image + ), + onPressed: () {}, + ), + ) + ], + ) + ], + 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, // Adjust the fit + ), + ), + ), + ), + body: ListView( + padding: EdgeInsets.symmetric(horizontal: 0), + children: [ + Padding(padding: EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + children: [ + !isSearchEnabled ? _beforeDataScreenAbove() : Container(), + SizedBox(height: MediaQuery.of(context).size.height * .024), + _buildDropdownField( + hint: "Type of Water", + value: selectedWaterType, + items: waterTypes, + path: 'images/water.png', + onChanged: (val) => setState(() => selectedWaterType = val), + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + children: [ + Expanded( + child: _buildDropdownField( + hint: "Capacity", + value: selectedCapacity, + items: capacities, + path:'', + onChanged: (val) => + setState(() => selectedCapacity = val))), + SizedBox(width: MediaQuery.of(context).size.width * .024), + Expanded( + child: _buildDropdownField( + hint: "Quantity", + value: selectedQuantity, + items: quantities, + path:'', + onChanged: (val) => + setState(() => selectedQuantity = val))), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + _buildDropdownField( + hint: "Frequency", + value: selectedFrequency, + items: frequencyTypes, + path:'images/frequency.png', + onChanged: (val) => setState(() => selectedFrequency = val), + ), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + children: [ + Expanded( + child: _buildDatePickerField( + hint: "Start date", + value: selectStartDate != null + ? DateFormat('dd-MMM-yyyy').format(selectStartDate!) + : null, + onTap: () => _selectFromDate(context))), + SizedBox(width: MediaQuery.of(context).size.width * .024), + Expanded( + child: _buildDatePickerField( + hint: "End date", + value: selectStopDate != null + ? DateFormat('dd-MMM-yyyy').format(selectStopDate!) + : null, + onTap: () => _selectToDate(context))), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .024), + SizedBox( + width: double.infinity, + height: 41, + child: ElevatedButton( + onPressed: () async { + + // βœ… FIELD VALIDATIONS + if (selectedWaterType == null || selectedWaterType!.isEmpty) { + AppSettings.longFailedToast('Please select Water Type'); + return; + } + + if (selectedCapacity == null || selectedCapacity!.isEmpty) { + AppSettings.longFailedToast('Please select Capacity'); + return; + } + + if (selectedQuantity == null || selectedQuantity!.isEmpty) { + AppSettings.longFailedToast('Please select Quantity'); + return; + } + + if (selectedFrequency == null || selectedFrequency!.isEmpty) { + AppSettings.longFailedToast('Please select Frequency'); + return; + } + + if (selectStartDate == null) { + AppSettings.longFailedToast('Please select Start Date'); + return; + } + + if (selectStopDate == null) { + AppSettings.longFailedToast('Please select End Date'); + return; + } + + // βœ… END DATE VALIDATION + if (selectStopDate!.isBefore(selectStartDate!)) { + AppSettings.longFailedToast('End Date must be after Start Date'); + return; + } + + // βœ… SEARCH ENABLE + setState(() { + isSearchEnabled = true; + }); + + await getAllSuppliersPlans(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0XFF1D7AFC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + child: Text( + "Search", + style: fontTextStyle( + 14, Color(0xFFFFFFFF), FontWeight.w600), + ), + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .036), + ], + ), + ), + !isSearchEnabled ? _beforeDataScreen() : _suppliersDataScreen(), + ], + ), + ); + } + + Widget _buildDropdownField({ + required String hint, + required String? value, + required List items, + String? path, // βœ… Make path optional + required ValueChanged onChanged, + }) { + return DropdownButtonHideUnderline( + child: DropdownButton2( + isExpanded: true, + hint: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (path != null&&path!='') ...[ + Image.asset( + path, + width: 16, + height: 16, + color: Color(0XFF939495), + ), + SizedBox(width: 6), + ], + Text( + hint, + style: fontTextStyle(14, Color(0XFF939495), FontWeight.w400), + ), + ], + ), + value: value, + items: items.map((item) { + return DropdownMenuItem( + value: item, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: Text( + item, + style: fontTextStyle(14, Color(0xFF2D2E30), FontWeight.w400), + ), + ), + ); + }).toList(), + onChanged: onChanged, + buttonStyleData: ButtonStyleData( + height: 48, + padding: EdgeInsets.only(left: 0, right: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(12), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: EdgeInsets.only(right: 4), + child: Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF939495), + ), + ), + ), + dropdownStyleData: DropdownStyleData( + padding: EdgeInsets.zero, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + menuItemStyleData: MenuItemStyleData( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4), + ), + ), + ); + } + + Widget _buildDatePickerField({ + required String hint, + String? value, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 48, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Image.asset( + 'images/calender_supplier_landing.png', + fit: BoxFit.cover, + width: 16, // Match the diameter of the CircleAvatar + height: 16, + color: Color(0XFF939495), + ), + SizedBox( + width: + MediaQuery.of(context).size.width * .012, + ), + Text( + value ?? hint, + style: value != null + ? fontTextStyle(14, Color(0xFF2D2E30), FontWeight.w400) + : fontTextStyle(14, Color(0XFF939495), FontWeight.w400), + ), + ], + ), + Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF939495), + ), + ], + ), + ), + ); + } + + Widget _buildTimePickerField({ + required String hint, + String? value, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 48, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Color(0XFF939495)), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + value ?? hint, + style: value != null + ? fontTextStyle(14, Color(0xFF2D2E30), FontWeight.w400) + : fontTextStyle(14, Color(0XFF939495), FontWeight.w400), + overflow: TextOverflow.visible, + ), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .008), + Image.asset( + 'images/arrow_down.png', + width: 16, + height: 16, + color: Color(0XFF939495), + ), + ], + ), + ), + ); + } +} diff --git a/lib/supplier/plans/plan_requests.dart b/lib/supplier/plans/plan_requests.dart new file mode 100644 index 0000000..185aba6 --- /dev/null +++ b/lib/supplier/plans/plan_requests.dart @@ -0,0 +1,369 @@ +import 'dart:convert'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../common/settings.dart'; +import '../paln_requests_model.dart'; + +class PlanRequestsScreen extends StatefulWidget { + const PlanRequestsScreen({super.key}); + + @override + State createState() => _PlanRequestsScreenState(); +} + +class _PlanRequestsScreenState extends State { + + bool isPlanRequestsDataLoading = false; + + List planRequestsList = []; + List acceptedPlans = []; + List rejectedPlans = []; + + @override + void initState() { + super.initState(); + getAcceptedPlansFromSupplier(); + } + + /// API CALL + Future getAcceptedPlansFromSupplier() async { + + isPlanRequestsDataLoading = true; + + var response = await AppSettings.getAcceptedPlansFromSupplier(); + var json = jsonDecode(response); + + setState(() { + + planRequestsList = (json['data'] as List) + .map((model) => PlanRequestModel.fromJson(model)) + .toList(); + + groupPlansByStatus(); + + isPlanRequestsDataLoading = false; + }); + } + + /// GROUP DATA + void groupPlansByStatus() { + + acceptedPlans = planRequestsList + .where((plan) => plan.status.toLowerCase() == "accepted") + .toList(); + + rejectedPlans = planRequestsList + .where((plan) => plan.status.toLowerCase() == "rejected") + .toList(); + } + + /// TIME BADGE + String getOrderTimeOnly(String orderTimeStr) { + + if (orderTimeStr.isEmpty) return ""; + + try { + + final format = DateFormat("dd-MM-yyyy HH:mm"); + final orderTime = format.parse(orderTimeStr); + + final now = DateTime.now(); + final difference = now.difference(orderTime); + + if (difference.inDays < 2) { + return "New"; + } + else if (difference.inDays < 10) { + final remaining = 10 - difference.inDays; + return "Expires in ${remaining}d"; + } + else { + return "Expired"; + } + + } catch (e) { + return ""; + } + } + + /// PLAN CARD + Widget planCard(PlanRequestModel plan) { + + final statusTime = getOrderTimeOnly(plan.acceptedTime); + + Color statusColor = Color(0xFF1D7AFC); + + if (statusTime == "Expired") { + statusColor = Color(0xFF757575); + } + + int totalOrders = plan.dates.length; + + double biddingPrice = + double.tryParse(plan.biddingPrice) ?? 0; + + double totalAmount = totalOrders * biddingPrice; + + return Padding( + padding: EdgeInsets.all(10), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Color(0XFF1D7AFC)), + ), + child: Padding( + padding: EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + /// SUPPLIER + BADGE + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Text( + plan.supplierName, + style: fontTextStyle( + 14, Color(0XFF343637), FontWeight.w600), + ), + + if (statusTime.isNotEmpty) + Container( + padding: + EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: statusColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + statusTime, + style: fontTextStyle( + 10, Colors.white, FontWeight.w500), + ), + ) + ], + ), + + SizedBox(height: 6), + + /// ADDRESS + Text( + plan.displayAddress, + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + + SizedBox(height: 4), + + /// DATE RANGE + Text( + "${plan.startDate} to ${plan.endDate}", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + + SizedBox(height: 12), + + /// WATER TYPE + Container( + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: plan.typeofwater.toLowerCase() == "bore water" + ? Color(0xFF8877DD) + : Color(0xFFCA86B0), + borderRadius: BorderRadius.circular(4), + ), + child: AutoSizeText( + capitalizeFirst(plan.typeofwater), + style: fontTextStyle( + 12, Colors.white, FontWeight.w400), + ), + ), + + SizedBox(height: 8), + + /// ACTUAL PRICE + Row( + children: [ + Text( + "Actual Price: ", + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + Text( + "β‚Ή${AppSettings.formDouble(plan.quotedAmount)}/${plan.capacity}", + style: fontTextStyle( + 12, Color(0XFF515253), FontWeight.w600), + ) + ], + ), + + /// BIDDING PRICE + Row( + children: [ + Text( + "Bidding Price: ", + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + Text( + "β‚Ή${AppSettings.formDouble(plan.biddingPrice)}/${plan.capacity}", + style: fontTextStyle( + 12, Color(0XFF515253), FontWeight.w600), + ) + ], + ), + + /// FREQUENCY + Row( + children: [ + Text( + "Frequency: ", + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + Text( + "${plan.frequency}/week", + style: fontTextStyle( + 12, Color(0XFF2D2E30), FontWeight.w600), + ) + ], + ), + + SizedBox(height: 10), + + /// PAYMENT TYPE + Row( + children: [ + Text( + "Payment Type: ", + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + Text( + plan.paymentType, + style: fontTextStyle( + 12, Color(0XFF515253), FontWeight.w600), + ) + ], + ), + + /// ADVANCE / CREDIT + if (plan.paymentType != "after_delivery") + Row( + children: [ + Text( + plan.paymentType == "credit" + ? "Credit Limit: " + : "Advance Amount: ", + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + Text( + plan.advanceAmount, + style: fontTextStyle( + 12, Color(0XFF515253), FontWeight.w600), + ) + ], + ), + + SizedBox(height: 10), + + /// TOTAL PLAN AMOUNT + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "Total amount for this plan orders ", + style: fontTextStyle( + 12, + Color(0xFF939495), + FontWeight.w400, + ), + ), + TextSpan( + text: + "$totalOrders Γ— ${biddingPrice.toStringAsFixed(0)} = β‚Ή${totalAmount.toStringAsFixed(0)}", + style: fontTextStyle( + 12, + Color(0xFF515253), + FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + /// UI + @override + Widget build(BuildContext context) { + + return Scaffold( + backgroundColor: Colors.white, + + appBar: AppSettings.supplierAppBarWithoutActions( + "Plan Requests", context), + + body: isPlanRequestsDataLoading + ? Center( + child: CircularProgressIndicator( + color: primaryColor, + strokeWidth: 5, + )) + : planRequestsList.isEmpty + ? Center( + child: Text( + "No Data Available", + style: fontTextStyle( + 12, Colors.black, FontWeight.w500), + ), + ) + : ListView( + children: [ + + /// NEW REQUESTS + Padding( + padding: EdgeInsets.all(10), + child: Text( + "PLAN REQUESTS", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + + ...acceptedPlans + .map((plan) => planCard(plan)) + .toList(), + + /// REJECTED + if (rejectedPlans.isNotEmpty) ...[ + + Padding( + padding: EdgeInsets.all(10), + child: Text( + "REJECTED REQUESTS", + style: fontTextStyle( + 10, Color(0XFF646566), FontWeight.w400), + ), + ), + + ...rejectedPlans + .map((plan) => planCard(plan)) + .toList() + ] + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/supplier/plans/plan_suppliers_model.dart b/lib/supplier/plans/plan_suppliers_model.dart new file mode 100644 index 0000000..31f04e1 --- /dev/null +++ b/lib/supplier/plans/plan_suppliers_model.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:watermanagement/common/settings.dart'; + + +class PlanSuppliersModel { + String supplier_name = ''; + String status=''; + String supplier_address=''; + String supplier_phone_number=''; + String supplier_id=''; + Color text_color=Colors.black; + double lat=0; + double lng=0; + String distrubance_price=''; + String amount_difference=''; + double distanceInMeters=0; + String displayAddress=''; + bool isFavorite=false; + bool isRequetsedBooking=false; + String? matchedPrice; + + PlanSuppliersModel(); + + factory PlanSuppliersModel.fromJson(Map json){ + PlanSuppliersModel rtvm = new PlanSuppliersModel(); + + rtvm.supplier_name = json['supplier']['suppliername'] ?? ''; + rtvm.status = json['supplier']['status'] ?? ''; + rtvm.supplier_address = json['supplier']['profile']['office_address'] ?? ''; + rtvm.supplier_phone_number =json['supplier']['phone'] ?? ''; + rtvm.supplier_id = json['supplier']['supplierId'] ?? ''; + rtvm.isFavorite = json['isFavorite'] ?? false; + rtvm.isRequetsedBooking = json['requestedBooking']['status'] ?? false; + rtvm.lat = json['supplier']['latitude'] ?? 0; + rtvm.lng = json['supplier']['longitude'] ?? 0; + List parts = rtvm.supplier_address.split(','); + rtvm.displayAddress = parts[2].trim(); + rtvm.distanceInMeters = double.parse((Geolocator.distanceBetween( + rtvm.lat, + rtvm.lng, + AppSettings.userLatitude, + AppSettings.userLongitude + ) / 1000).toStringAsFixed(2)); + + + + + return rtvm; + } + +} \ No newline at end of file diff --git a/lib/supplier/profile.dart b/lib/supplier/profile.dart new file mode 100644 index 0000000..eba51e1 --- /dev/null +++ b/lib/supplier/profile.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; + +import '../common/UpdateProfile.dart'; +import '../common/settings.dart'; + + +class Profile extends StatefulWidget { + const Profile({super.key}); + + @override + State createState() => _ProfileState(); +} + +class _ProfileState extends State { + + // Section Header with title + subtitle + Widget _buildSectionHeader(String title, String subtitle) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: fontTextStyle(12, const Color(0XFF232527), FontWeight.w600), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: fontTextStyle(10, const Color(0XFF646464), FontWeight.w400), + ), + ], + ), + if (title != "My Transactions") + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ); + } + + + // List Tile with arrow + Widget _buildListTile(String title) { + return ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text(title, + style:fontTextStyle(10, Color(0XFF646464), FontWeight.w400)), + trailing:Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + onTap: () {}, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppSettings.supplierAppBarWithActionsText('Profile',context), + body: Padding( + padding: EdgeInsets.all(24), + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(height:MediaQuery.of(context).size.height * .008,), + Padding( padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AppSettings.loginType.toString().toLowerCase()=='user'? + GestureDetector( + onTap: () { + /*Navigator.push( + context, + new MaterialPageRoute( + builder: (__) => new ImageZoomPage( + imageName: 'Profile', + imageDetails: AppSettings.profilePictureUrl)));*/ + }, + child: Stack( + alignment: Alignment.center, // Centers the stack's children + children: [ + + // CircleAvatar background + ClipOval( + child: Container( + height: 56, // Ensure the container's height is the same as the profile image + width: 56, + + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, // Makes the container circular + image: DecorationImage( + image: (AppSettings.profilePictureUrl != + '' && + AppSettings.profilePictureUrl != + 'null') + ? NetworkImage(AppSettings.profilePictureUrl) + as ImageProvider + : AssetImage( + "images/profile_pic.png"), // picked file + fit: BoxFit.cover)), + ), + ), + // Positioned image on top of CircleAvatar + ], + ), + + ): + ClipOval( + child: Container( + height: 56, // Ensure the container's height is the same as the profile image + width: 56, + + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, // Makes the container circular + image: DecorationImage( + image: (AssetImage( + "images/profile_pic.png")), // picked file + fit: BoxFit.contain)), + ), + ), + SizedBox(width:MediaQuery.of(context).size.width * .032,), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + AppSettings.userName, + style: fontTextStyle(14,Color(0XFF343637),FontWeight.w500), + ), + Row( + children: [ + Text( + '+91 ${AppSettings.phoneNumber}', + style: fontTextStyle(10,Color(0XFF646566),FontWeight.w400), + ), + SizedBox(width:MediaQuery.of(context).size.width * .032,), + Visibility( + visible:AppSettings.loginType.toString().toLowerCase()=='user', + child: Text( + AppSettings.email, + style: fontTextStyle(10,Color(0XFF646566),FontWeight.w400), + ),), + + ], + ), + SizedBox(height:MediaQuery.of(context).size.height * .004,), + GestureDetector( + onTap: (){ + Navigator.push( + context, + MaterialPageRoute(builder: (context) => UpdateProfile()), + ); + }, + child: Text('Edit',style: fontTextStyle(14,Color(0XFF1D7AFC),FontWeight.w600),), + ) + ], + ) + ], + ), + ], + ),), + SizedBox(height:MediaQuery.of(context).size.height * .008,), + Divider( + height: 24, // space around divider + color: Color(0XFFC3C4C4), // divider color + thickness: 2, // πŸ‘ˆ increase thickness here + ), + Expanded(child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24), + children: [ + + // My Accounts Section + _buildSectionHeader("My Accounts", "Favorites & Suppliers"), + const Divider(thickness: 1, height: 24, color: Color(0xFFC3C4C4)), + + // My Transactions (Expandable) + ExpansionTile( + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + title: _buildSectionHeader( + "My Transactions", + "View all Payments, Credit accounts & Refunds", + ), + children: [ + _buildListTile("My Payments"), + _buildListTile("Credit Accounts"), + _buildListTile("Modes of Payment"), + _buildListTile("Refunds"), + ], + trailing: Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + // πŸ‘‡ This removes that unwanted grey line + shape: const Border(), // no top/bottom border + collapsedShape: const Border(), // even when collapsed + ), + const Divider(thickness: 1, height: 24, color: Color(0xFFC3C4C4)), + + // Addresses + _buildSectionHeader("Addresses", "Share, Edit and Add New Addresses"), + const Divider(thickness: 1, height: 24, color: Color(0xFFC3C4C4)), + + _buildSectionHeader("Account Statements", "Get statements for reimbursement or book keeping!"), + const Divider(thickness: 1, height: 24, color: Color(0xFFC3C4C4)), + + _buildSectionHeader("Settings", "Manage Account Settings"), + + Divider( + height: 48, // space around divider + color: Color(0XFFC3C4C4), // divider color + thickness: 2, // πŸ‘ˆ increase thickness here + ), + ], + ),) + + + + ], + ), + ), + ) + ); + } +} diff --git a/lib/supplier/supplier_calendar.dart b/lib/supplier/supplier_calendar.dart new file mode 100644 index 0000000..8dd991a --- /dev/null +++ b/lib/supplier/supplier_calendar.dart @@ -0,0 +1,530 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; +import '../common/settings.dart'; + +class SupplyCalendarScreen extends StatefulWidget { + var dates; + SupplyCalendarScreen({this.dates}); + @override + State createState() => _SupplyCalendarScreenState(); +} + +class _SupplyCalendarScreenState extends State { + late DateTime _focusedDay; + late DateTime _firstDay; + late DateTime _lastDay; + + Map>> calendarEvents = {}; + DateTime? _selectedDay; + + bool isLoading = true; + + @override + void initState() { + super.initState(); + + final now = DateTime.now(); + _focusedDay = now; + + _firstDay = DateTime(2025, 1, 1); + _lastDay = DateTime(now.year, now.month + 6, 0); + + fetchOrdersFromApi(); + } + + // =========================================================================== + // FETCH API β†’ BUILD TWO EVENTS (ORIGINAL + RESCHEDULED) + // =========================================================================== + Future fetchOrdersFromApi() async { + try { + final response = await AppSettings.getAllOrders(); + final decoded = jsonDecode(response); + + if (decoded == null || decoded['data'] == null) { + setState(() => isLoading = false); + return; + } + + final List orders = decoded['data']; + calendarEvents.clear(); + + for (var order in orders) { + String? originalDate = order["date"]; + String? newDate = order["reScheduleDateOfDelivery"]; + String? resStatus = order["rescheduleOrderStatus"]; + + bool isRescheduled = + newDate != null && newDate.isNotEmpty && resStatus != null && resStatus.isNotEmpty; + + // ===================== ORIGINAL DATE EVENT ===================== + if (originalDate != null && order["orderStatus"] != "cancelled") { + try { + DateTime d = DateTime.parse(originalDate); + DateTime key = DateTime(d.year, d.month, d.day); + + calendarEvents.putIfAbsent(key, () => []); + calendarEvents[key]!.add({ + "status": isRescheduled ? "rescheduled_from" : "delivery", + "_id": order["_id"], + "supplierName": order["supplierName"] ?? "Supplier", + "capacity": order["capacity"] ?? "", + "time": order["time"] ?? "", + "originalDate": originalDate, + "newDate": newDate, + }); + } catch (e) {} + } + + // ===================== NEW RESCHEDULED DATE EVENT ===================== + if (isRescheduled) { + try { + DateTime d2 = DateTime.parse(newDate); + DateTime key2 = DateTime(d2.year, d2.month, d2.day); + + calendarEvents.putIfAbsent(key2, () => []); + calendarEvents[key2]!.add({ + "status": "rescheduled_to", + "_id": order["_id"], + "supplierName": order["supplierName"] ?? "Supplier", + "capacity": order["capacity"] ?? "", + "time": order["time"] ?? "", + "originalDate": originalDate, + "newDate": newDate, + }); + } catch (e) {} + } + + // ===================== CANCELLED EVENT ===================== + if (order["orderStatus"] == "cancelled") { + try { + DateTime d = DateTime.parse(originalDate!); + DateTime key = DateTime(d.year, d.month, d.day); + + calendarEvents.putIfAbsent(key, () => []); + calendarEvents[key]!.add({ + "status": "cancelled", + "_id": order["_id"], + "supplierName": order["supplierName"] ?? "Supplier", + "capacity": order["capacity"] ?? "", + "time": order["time"] ?? "", + }); + } catch (e) {} + } + } + + setState(() => isLoading = false); + } catch (e) { + setState(() => isLoading = false); + } + } + + // =========================================================================== + // CANCEL ORDER API + // =========================================================================== + Future cancelDelivery(String id) async { + try { + + await AppSettings.cancelPlanOrder(id); // your endpoint + await fetchOrdersFromApi(); + + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Order cancelled successfully"))); + } catch (e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Cancel failed"))); + } + } + + void _confirmCancel(String orderId) { + showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text("Cancel Delivery"), + content: Text("Are you sure you want to cancel this delivery?"), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), child: Text("No")), + TextButton( + onPressed: () { + Navigator.pop(context); + cancelDelivery(orderId); + }, + child: Text("Yes, Cancel", style: TextStyle(color: Colors.red))), + ], + ), + ); + } + + // =========================================================================== + // HELPER FUNCTIONS + // =========================================================================== + String formatShort(String date) { + try { + final d = DateTime.parse(date); + return "${d.day} ${monthShort[d.month - 1]}"; + } catch (e) { + return date; + } + } + + List monthShort = [ + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" + ]; + + Widget _buildStatusIcon(String status) { + if (status == "rescheduled_from") { + return Icon(Icons.subdirectory_arrow_left, color: Colors.orange); + } + if (status == "rescheduled_to") { + return Icon(Icons.subdirectory_arrow_right, color: Colors.deepOrange); + } + if (status == "cancelled") { + return Icon(Icons.cancel, color: Colors.red); + } + return Icon(Icons.local_shipping, color: Colors.blue); + } + + String _buildStatusText(Map d) { + String status = d["status"]; + String? originalDate = d["originalDate"]; + String? newDate = d["newDate"]; + + if (status == "rescheduled_from") { + return "Rescheduled from this date β†’ ${formatShort(newDate!)}"; + } + if (status == "rescheduled_to") { + return "Rescheduled to this date (from ${formatShort(originalDate!)})"; + } + if (status == "cancelled") { + return "Cancelled"; + } + return status; + } + + // =========================================================================== + // OPEN RESCHEDULE CALENDAR + // =========================================================================== + Future openRescheduleCalendar(Map delivery) async { + DateTime now = DateTime.now(); + + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: now.add(Duration(days: 1)), + firstDate: now.add(Duration(days: 1)), + lastDate: DateTime(now.year + 1, 12, 31), + ); + + if (pickedDate == null) return; + + String formatted = + "${pickedDate.year}-${pickedDate.month.toString().padLeft(2,'0')}-${pickedDate.day.toString().padLeft(2,'0')}"; + + await sendRescheduleToServer(delivery["_id"], formatted); + } + + Future sendRescheduleToServer(String id, String newDate) async { + try { + final payload = {"reScheduleDateOfDelivery": newDate}; + + await AppSettings.rescheduleOrder(id, payload); + await fetchOrdersFromApi(); + + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Delivery rescheduled to $newDate"))); + } catch (e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Reschedule failed"))); + } + } + + // =========================================================================== + // SHOW DELIVERY LIST + // =========================================================================== + void showDeliveryList(DateTime date) { + final key = DateTime(date.year, date.month, date.day); + final events = calendarEvents[key]; + + if (events == null || events.isEmpty) return; + + bool isPast = date.isBefore(DateTime( + DateTime.now().year, DateTime.now().month, DateTime.now().day)); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) { + final list = events.toList(); + + return DraggableScrollableSheet( + initialChildSize: 0.9, + maxChildSize: 0.9, + minChildSize: 0.4, + builder: (_, controller) { + return Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + "Select Delivery", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Icon(Icons.close)), + ], + ), + + Expanded( + child: ListView( + controller: controller, + children: list.map((delivery) { + return ListTile( + leading: _buildStatusIcon(delivery["status"]), + title: Text(_buildStatusText(delivery)), + subtitle: Text( + "Capacity: ${delivery["capacity"]} β€’ ${delivery["time"]}"), + onTap: () { + Navigator.pop(context); + showActionsForSingleDelivery(delivery, isPast); + }, + ); + }).toList(), + ), + ) + ], + ), + ); + }, + ); + }, + ); + } + + // =========================================================================== + // ACTIONS SHEET + // =========================================================================== + void showActionsForSingleDelivery(Map delivery, bool isPast) { + String status = delivery["status"]; + + // ❌ ORIGINAL DATE β†’ NO ACTIONS + if (status == "rescheduled_from") { + showModalBottomSheet( + context: context, + builder: (_) { + return Container( + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Rescheduled from this date", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + SizedBox(height: 10), + Text("This delivery was moved to another date.", + style: TextStyle(color: Colors.grey)), + ], + ), + ); + }, + ); + return; + } + + // ❌ CANCELLED ORDER β†’ NO ACTIONS + if (status == "cancelled") { + showModalBottomSheet( + context: context, + builder: (_) => + Container( + padding: EdgeInsets.all(20), + child: Text("Order was cancelled", style: TextStyle(color: Colors.red)), + ), + ); + return; + } + + // NORMAL OR RESCHEDULED_TO (ACTIONS ENABLED) + showModalBottomSheet( + context: context, + builder: (_) { + return Container( + padding: EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${delivery["supplierName"]}", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + SizedBox(height: 16), + + if (!isPast) ...[ + ListTile( + leading: Icon(Icons.calendar_month, color: Colors.blue), + title: Text("Reschedule Delivery"), + onTap: () { + Navigator.pop(context); + openRescheduleCalendar(delivery); + }, + ), + + ListTile( + leading: Icon(Icons.delete_forever, color: Colors.red), + title: Text("Cancel Delivery"), + onTap: () { + Navigator.pop(context); + _confirmCancel(delivery["_id"]); + }, + ), + ], + + if (isPast) + Text("Past delivery β€” actions disabled", + style: TextStyle(color: Colors.grey)), + ], + ), + ); + }, + ); + } + + // =========================================================================== + // BUILD UI + // =========================================================================== + @override + Widget build(BuildContext context) { + if (isLoading) { + return Scaffold(body: Center(child: CircularProgressIndicator())); + } + + final height = MediaQuery.of(context).size.height; + + return Scaffold( + backgroundColor: Colors.white, + body: Column( + children: [ + SizedBox(height: 50), + + Text( + "${monthShort[_focusedDay.month - 1]} ${_focusedDay.year}", + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + + Expanded( + child: TableCalendar( + firstDay: _firstDay, + lastDay: _lastDay, + focusedDay: _focusedDay, + headerVisible: false, + calendarFormat: CalendarFormat.month, + rowHeight: (height - 200) / 6, + daysOfWeekHeight: 40, + + onPageChanged: (focused) { + setState(() => _focusedDay = focused); + }, + + onDaySelected: (selected, focused) { + _focusedDay = focused; + _selectedDay = selected; + showDeliveryList(selected); + setState(() {}); + }, + + calendarBuilders: CalendarBuilders( + defaultBuilder: (context, date, _) { + final key = DateTime(date.year, date.month, date.day); + final events = calendarEvents[key]; + + Color bg = Colors.transparent; + + if (events != null && events.isNotEmpty) { + String s = events.first["status"]; + if (s == "rescheduled_to") + bg = Colors.deepOrange.withOpacity(0.10); + else if (s == "rescheduled_from") + bg = Colors.orange.withOpacity(0.10); + else if (s == "cancelled") + bg = Colors.red.withOpacity(0.10); + else + bg = Colors.blue.withOpacity(0.08); + } + + // Grouping counts (delivery, cancelled, rescheduled) + Map grouped = {}; + if (events != null) { + for (var e in events) { + grouped[e["status"]] = (grouped[e["status"]] ?? 0) + 1; + } + } + + return Container( + decoration: BoxDecoration( + color: bg, + border: Border.all(color: Colors.grey.shade300)), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${date.day}", + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.bold)), + + if (events != null && events.isNotEmpty) + Wrap( + spacing: 3, + alignment: WrapAlignment.center, + children: grouped.entries.map((entry) { + IconData icon; + Color color; + + if (entry.key == "rescheduled_from") { + icon = Icons.subdirectory_arrow_left; + color = Colors.orange; + } + else if (entry.key == "rescheduled_to") { + icon = Icons.subdirectory_arrow_right; + color = Colors.deepOrange; + } + else if (entry.key == "cancelled") { + icon = Icons.cancel; + color = Colors.red; + } + else { + icon = Icons.local_shipping; + color = Colors.blue; + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 10, color: color), + Text("x${entry.value}", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: color)), + ], + ); + }).toList(), + ) + ], + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/supplier/supplier_details.dart b/lib/supplier/supplier_details.dart new file mode 100644 index 0000000..411dc44 --- /dev/null +++ b/lib/supplier/supplier_details.dart @@ -0,0 +1,620 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/models/supplier_tankers_model.dart'; +import 'package:watermanagement/supplier/cart_summary.dart'; + +class SupplierScreen extends StatefulWidget { + var details; + SupplierScreen({this.details}); + + @override + State createState() => _SupplierScreenState(); +} + +class _SupplierScreenState extends State { + DateTime _focusedDay = DateTime.now(); + DateTime? _selectedDay; + bool isTankerDataLoading = false; + bool isCartDataLoading = false; + List tankersList = []; + bool isSereverIssue = false; + List cart = []; + Map localQuantities = {}; + Map localTotalPrices = {}; + + Future getTankers() async { + isTankerDataLoading = true; + try { + var tankerResponse = await AppSettings.getAllTankers(widget.details.supplier_id); + + setState(() { + tankersList = + ((jsonDecode(tankerResponse)['data']) as List).map((dynamic model) { + return SupplierTankersModel.fromJson(model); + }).toList(); + + + + isTankerDataLoading = false; + }); + } catch (e) { + setState(() { + isTankerDataLoading = false; + isSereverIssue = true; + }); + /* AppSettings.longFailedToast('There is an issue at server side please try after some time'); + Navigator.pop(context);*/ + } + } + + + Future getCart() async { + isCartDataLoading = true; + try { + var tankerResponse = await AppSettings.getCartItems(); + + setState(() { + cart = ((jsonDecode(tankerResponse)['data']['items'])); + + + isCartDataLoading = false; + }); + } catch (e) { + setState(() { + isCartDataLoading = false; + isSereverIssue = true; + }); + /* AppSettings.longFailedToast('There is an issue at server side please try after some time'); + Navigator.pop(context);*/ + } + } + + @override + void initState() { + super.initState(); + _selectedDay = _focusedDay; + getTankers(); + getCart(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar:AppSettings.SupplierAppBar('Place Order',context), + body: Padding( + padding: EdgeInsets.fromLTRB(12, 8, 12, 8), + child: ListView( + children: [ + // Supplier Card + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 2, + color: Colors.white, + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + visible: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + visible: widget.details.supplier_name != '', + child: Text( + widget.details.supplier_name.toString(), + style: fontTextStyle( + 16, Color(0XFF2D2E30), FontWeight.w600), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'images/star.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + Visibility( + visible: true, + child: Text( + '4.2 (20K+ Ratings)', + style: fontTextStyle(9, + Color(0XFF515253), FontWeight.w400), + ), + ), + ], + ) + ], + ), + ), + Visibility( + visible: true, + child: Text( + 'Drinking | Bore Water', + style: fontTextStyle( + 12, Color(0XFF4692FD), FontWeight.w500), + ), + ), + Visibility( + visible: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.details.displayAddress + + ' ' + + widget.details.distanceInMeters + .toString() + + ' Km', + style: fontTextStyle(12, Color(0XFF515253), + FontWeight.w400)), + Image.asset( + 'images/heart_outline.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.zero, + child: Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(12), + ), // match Card border + gradient: LinearGradient( + colors: [ + Color(0xFFFFE8A3), + Color(0xFFFFF8DF), + Color(0xFFFFFFFF), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset( + 'images/ring.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + Text('Best water quality', + style: fontTextStyle( + 10, + Color(0XFF2D2E30), + FontWeight.w400)), + ], + ), + Row( + children: [ + Image.asset( + 'images/ring.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + Text('Steel casing tankers', + style: fontTextStyle( + 10, + Color(0XFF2D2E30), + FontWeight.w400)), + ], + ) + ], + ), + )), + ), + ], + )), + + Card( + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 0), + child: Column( + children: [ + // Header Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + DateFormat.yMMM().format(_focusedDay), + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w600), + ), + Image.asset( + 'images/calender.png', + fit: BoxFit.cover, + width: 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ), + Divider( + color: Colors.grey.shade300, + ), + // Weekly Calendar + TableCalendar( + focusedDay: _focusedDay, + firstDay: DateTime.now(), + lastDay: DateTime.now().add(Duration(days: 15)), + calendarFormat: CalendarFormat.week, + startingDayOfWeek: StartingDayOfWeek.sunday, + availableCalendarFormats: const { + CalendarFormat.week: 'Week', + }, + selectedDayPredicate: (day) { + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + }, + headerVisible: false, + calendarStyle: CalendarStyle( + todayDecoration: BoxDecoration(), // No box for today + todayTextStyle:fontTextStyle(12, Color(0XFF1D7AFC), FontWeight.w400), + + selectedDecoration: BoxDecoration( + color: Color(0XFF1D7AFC), + shape: BoxShape.circle, + ), + selectedTextStyle: fontTextStyle(12, Color(0XFFFFFFFF), FontWeight.w400), + weekendTextStyle: TextStyle(color: Colors.black), + defaultTextStyle: TextStyle(color: Colors.black), + outsideDaysVisible: false, + ), + daysOfWeekStyle: DaysOfWeekStyle( + weekdayStyle: fontTextStyle(12, Color(0XFF343637), FontWeight.w400), + weekendStyle: fontTextStyle(12, Color(0XFF343637), FontWeight.w400), + ), + ), + ], + ), + )), + + SizedBox(height: MediaQuery.of(context).size.height * .016), + + Padding(padding: EdgeInsets.fromLTRB(6, 0, 6, 0), + child: Container( + width: double.infinity, + padding: + EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Color(0XFFFCF0E7), + borderRadius: BorderRadius.circular(6), + border: Border.all + ( + color: Color(0XFFEFA168)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + 'images/warning.png', + fit: BoxFit.cover, + width: + 22, // Match the diameter of the CircleAvatar + height: 22, + ), + SizedBox(width: MediaQuery.of(context).size.width * .020), + Expanded(child: Text( + 'The prices shown below are NOT inclusive of the transport charges. Transport charges are calculated based on the distance between the sourcing location and the delivery location.', + style: fontTextStyle( + 10, + Color(0XFF444444), + FontWeight.w400), + ),) + ], + ) + + + ),), + SizedBox(height: MediaQuery.of(context).size.height * .016), + // Supply Locations + Text('SUPPLY LOCATIONS', + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400),), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + color: Color(0XFFFFFFFF), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...[ + {'name': 'Gandipet', 'type': 'Drinking water', 'km': '4.5 Km'}, + {'name': 'Nizampet', 'type': 'Drinking water', 'km': '7.3 Km'}, + {'name': 'Secunderabad', 'type': 'Bore water', 'km': '12.4 Km'}, + ].map((location) => Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: ListBody( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(location['name']!, style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500),), + Text(location['km']!,style: fontTextStyle(10, Color(0XFF2D2E30), FontWeight.w500),), + ], + ), + Text(location['type']!, style: fontTextStyle(10, Color(0XFF515253), FontWeight.w400),), + + ], + ), + )), + ], + ), + ), + ), + + // Tankers + SizedBox(height: MediaQuery.of(context).size.height * .016), + Text('TANKERS', + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400),), + SizedBox(height: MediaQuery.of(context).size.height * .016), + GridView.builder( + padding: const EdgeInsets.all(0), + itemCount: tankersList.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.6, + ), + itemBuilder: (context, index) { + final tanker = tankersList[index]; + final isAvailable = tankersList[index].status == 'Available'; + final isEnabled =tankersList[index].enabled; + + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + elevation: 2, + color: Color(0XFFFFFFFF), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + tankersList[index].capacity+' Litres', + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500), + ), + Text( + '\u20B9 '+AppSettings.formDouble(tankersList[index].price), + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + ], + ), + Text( + tankersList[index].type_of_water, + style: fontTextStyle(12, Color(0XFF515253), FontWeight.w400), + ), + + Expanded(child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: + EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isAvailable ? Color(0XFF098603) : Colors.grey,), + ), + child: Text( + tankersList[index].status, + style: fontTextStyle( + 12, + isAvailable ? Color(0XFF098603) : Colors.grey, + FontWeight.w500), + ), + ), + ElevatedButton( + onPressed: isEnabled + ? () async { + AppSettings.preLoaderDialog(context); + + final product = tankersList[index]; + final productId = product.id; + final name = product.tanker_name; + + // Convert price safely from string like "1,000" + final priceString = product.price; + final unitPrice = int.parse(priceString.replaceAll(',', '')); + + // Get current quantity, or 0 if not added yet + int currentQuantity = localQuantities[productId] ?? 0; + int newQuantity = currentQuantity + 1; + + // Calculate total price for UI use (unitPrice * quantity) + int totalPrice = unitPrice * newQuantity; + + // Prepare payload to send unit price and quantity only + final payload = { + "productId": productId, + "name": name, + "quantity": newQuantity, + "price": unitPrice, // unit price ONLY + }; + + try { + final response = await http.post( + Uri.parse('${AppSettings.host}cart/${AppSettings.customerId}/add'), + headers: await AppSettings.buildRequestHeaders(), + body: jsonEncode(payload), + ); + + if (response.statusCode == 200 || response.statusCode == 201) { + setState(() { + Navigator.of(context, rootNavigator: true).pop(); + + // Update local tracking maps + localQuantities[productId] = newQuantity; + localTotalPrices[productId] = totalPrice; + + // Update or add the cart list entry + int existingIndex = cart.indexWhere((item) => item['productId'] == productId); + if (existingIndex != -1) { + cart[existingIndex]['quantity'] = newQuantity; + cart[existingIndex]['price'] = unitPrice; // unit price + cart[existingIndex]['total'] = totalPrice; // total price for UI + } else { + cart.add({ + "productId": productId, + "name": name, + "quantity": newQuantity, + "price": unitPrice, + "total": totalPrice, + }); + } + }); + + // Optional: refresh cart from server if you want + await getCart(); + } else { + Navigator.of(context, rootNavigator: true).pop(); + print('Failed to add to cart: ${response.body}'); + } + } catch (e) { + Navigator.of(context, rootNavigator: true).pop(); + print('Error adding to cart: $e'); + } + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: isEnabled ? primaryColor : Colors.grey[300], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: Size(48, 32), + padding: EdgeInsets.zero, + ), + child: Text( + 'Add', + style: fontTextStyle(12, Color(0XFFFFFFFF), FontWeight.w400), + ), + ), + + + + + + ], + )) + ], + ), + ), + ); + }, + ), + + + ], + ), + ), + bottomNavigationBar: cart.isNotEmpty + ? Padding( + padding: EdgeInsets.all(0), + child: Container( + height: 56, + width: double.infinity, + decoration: BoxDecoration( + color: Color(0XFFF5CD47), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Order updated", + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CartSummary(details: cart,supplierDetails: widget.details,)), + ); + }, + child: Row( + children: [ + Text( + "View summary", + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 20, // Match the diameter of the CircleAvatar + height: 20, + color: Color(0XFF343637), + ), + + ], + ), + ), + ], + ), + ), + ) + : null, + + ); + } +} diff --git a/lib/supplier/supplier_details_from_search.dart b/lib/supplier/supplier_details_from_search.dart new file mode 100644 index 0000000..baf5c46 --- /dev/null +++ b/lib/supplier/supplier_details_from_search.dart @@ -0,0 +1,687 @@ +import 'dart:convert'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/widgets.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter/material.dart'; +import 'package:watermanagement/common/settings.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:intl/intl.dart'; +import 'package:watermanagement/models/supplier_tankers_model.dart'; +import 'package:watermanagement/supplier/cart_summary.dart'; + +class SupplierScreenFromSearch extends StatefulWidget { + var details; + SupplierScreenFromSearch({this.details}); + + @override + State createState() => _SupplierScreenFromSearchState(); +} + +class _SupplierScreenFromSearchState extends State { + DateTime _focusedDay = DateTime.now(); + DateTime? _selectedDay; + bool isTankerDataLoading = false; + bool isCartDataLoading = false; + List tankersList = []; + bool isSereverIssue = false; + List cart = []; + Map localQuantities = {}; + Map localTotalPrices = {}; + + Future getTankers() async { + isTankerDataLoading = true; + try { + var tankerResponse = await AppSettings.getAllTankers(widget.details.supplier_id); + + setState(() { + tankersList = + ((jsonDecode(tankerResponse)['data']) as List).map((dynamic model) { + return SupplierTankersModel.fromJson(model); + }).toList(); + + + + isTankerDataLoading = false; + }); + } catch (e) { + setState(() { + isTankerDataLoading = false; + isSereverIssue = true; + }); + /* AppSettings.longFailedToast('There is an issue at server side please try after some time'); + Navigator.pop(context);*/ + } + } + + @override + void initState() { + super.initState(); + _selectedDay = _focusedDay; + getTankers(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar:AppSettings.SupplierAppBar('',context), + body: Padding( + padding: EdgeInsets.fromLTRB(0, 8, 0, 8), + child: Column( + children: [ + // βœ… Fixed Content + Expanded( + flex: 0, + child: Column( + children: [ + Padding(padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Visibility( + visible: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Visibility( + visible: widget.details.supplier_name != '', + child: Text( + widget.details.supplier_name.toString(), + style: fontTextStyle( + 16, Color(0XFF2D2E30), FontWeight.w600), + ), + ), + SizedBox( + width: + MediaQuery.of(context).size.width * + .012, + ), + widget.details.isFavorite?Image.asset( + 'images/heart_active.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ): + Image.asset( + 'images/heart_outline.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + ], + ), + Container( + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: Color(0XFFFDF3D3), + border: Border.all( + width: 0, + color: Color(0XFFFDF3D3), + ), + borderRadius: + BorderRadius.circular( + 12, + )), + child: Padding( + padding: EdgeInsets.fromLTRB(10,2,10,2), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'images/star.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + Visibility( + visible: true, + child: Text( + '4.2', + style: fontTextStyle(11, + Color(0XFF232527), FontWeight.w600), + ), + ), + ], + ), + ), + ) + ], + ), + ), + Visibility( + visible: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.details.displayAddress + + ' ' + + widget.details.distanceInMeters + .toString() + + ' Km', + style: fontTextStyle(12, Color(0XFF515253), + FontWeight.w400)), + + ], + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .004,), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Color(0xFF8877DD), + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 1, + color:Color(0xFF8877DD), + ), + ), + child: AutoSizeText( + capitalizeFirst('Bore Water'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0xFFFFFFFF), FontWeight.w400), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .016,), + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Color(0xFFCA86B0), + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 1, + color:Color(0xFFCA86B0), + ), + ), + child: AutoSizeText( + capitalizeFirst('Drinking Water'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0xFFFFFFFF), FontWeight.w400), + ), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .016,), + Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + width: 1, + color:Color(0xFF1D7AFC), + ), + ), + child: Row( + children: [ + Image.asset( + 'images/ring.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + AutoSizeText( + capitalizeFirst('Best water quality'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(10, Color(0xFF2D2E30), FontWeight.w400), + ), + ], + ), + + + ), + SizedBox(width: MediaQuery.of(context).size.width * .016,), + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + width: 1, + color:Color(0xFF1D7AFC), + ), + ), + child: Row( + children: [ + Image.asset( + 'images/ring.png', + fit: BoxFit.cover, + width: + 16, // Match the diameter of the CircleAvatar + height: 16, + ), + AutoSizeText( + capitalizeFirst('Steel casing tankers'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(10, Color(0xFF2D2E30), FontWeight.w400), + ), + ], + ), + + + ), + ], + ), + ], + ), + ), + + ], + ), + ), + + Padding( + padding: EdgeInsets.zero, + child: Container( + width: double + .infinity, // makes it expand within the Card's width + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(0), + ), // match Card border + gradient: LinearGradient( + colors: [ + Color(0xFFFFE8A3), + Color(0xFFFFF8DF), + Color(0xFFFFFFFF), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + padding: EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Start regular deliveries?', + style: fontTextStyle( + 16, + Color(0XFF2D2E30), + FontWeight.w600)), + SizedBox(height: MediaQuery.of(context).size.height * .012), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: Text( + 'Want to get water regularly from this supplier? Send a plan request.', + style: fontTextStyle(10, Color(0XFF515253), FontWeight.w400), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * .036), + Expanded( + flex: 1, + child: GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Color(0XFFFFFFFF), + border: Border.all(width: 1, color: Color(0XFF1D7AFC)), + borderRadius: BorderRadius.circular(24), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 8), + child: Text( + 'Request', + style: fontTextStyle(12, Color(0XFF1D7AFC), FontWeight.w600), + ), + ), + ), + ), + ], + ) + ], + ) + , + )), + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + + ], + ), + ), + + // βœ… Scrollable + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB(0, 0, 0, 0), + child: Column( + children: [ + Padding(padding: EdgeInsets.fromLTRB(18, 0, 18, 0), + child: Container( + width: double.infinity, + padding: + EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Color(0XFFFCF0E7), + borderRadius: BorderRadius.circular(6), + border: Border.all + ( + color: Color(0XFFEFA168)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + 'images/warning.png', + fit: BoxFit.cover, + width: + 22, // Match the diameter of the CircleAvatar + height: 22, + ), + SizedBox(width: MediaQuery.of(context).size.width * .020), + Expanded(child: Text( + 'The prices shown below are NOT inclusive of the transport charges. Transport charges are calculated based on the distance between the sourcing location and the delivery location.', + style: fontTextStyle( + 10, + Color(0XFF444444), + FontWeight.w400), + ),) + ], + ) + + + ),), + SizedBox(height: MediaQuery.of(context).size.height * .016), + + Padding( + padding: EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'SOURCE LOCATIONS', + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w400), + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + color: Color(0XFFFFFFFF), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...[ + {'name': 'Gandipet', 'type': 'Drinking water', 'km': '4.5 Km'}, + {'name': 'Nizampet', 'type': 'Drinking water', 'km': '7.3 Km'}, + {'name': 'Secunderabad', 'type': 'Bore water', 'km': '12.4 Km'}, + ].map((location) => Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(location['name']!, + style: fontTextStyle(12, Color(0XFF2D2E30), FontWeight.w500)), + Text(location['km']!, + style: fontTextStyle(10, Color(0XFF2D2E30), FontWeight.w500)), + ], + ), + Text(location['type']!, + style: fontTextStyle(10, Color(0XFF515253), FontWeight.w400)), + ], + ), + )), + ], + ), + ), + ), + + // Tankers + SizedBox(height: MediaQuery.of(context).size.height * .016), + Text( + 'TANKERS', + style: fontTextStyle(12, Color(0XFF646566), FontWeight.w400), + ), + SizedBox(height: MediaQuery.of(context).size.height * .016), + + ListView.builder( + itemCount: tankersList.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final tanker = tankersList[index]; + final isAvailable = tanker.status == 'Available'; + final isEnabled = tanker.enabled; + + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + elevation: 2, + color: Color(0XFFFFFFFF), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${tanker.capacity} Litres', + style: fontTextStyle(14, Color(0XFF2D2E30), FontWeight.w500), + ), + Text( + '\u20B9 ${AppSettings.formDouble(tanker.price)}', + style: fontTextStyle(10, Color(0XFF515253), FontWeight.w400), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).size.height * .004,), + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: tanker.type_of_water.toString().toLowerCase()=='bore water'?Color(0xFF8877DD): Color(0xFFCA86B0), + borderRadius: BorderRadius.circular(4), + border: Border.all( + width: 1, + color:tanker.type_of_water.toString().toLowerCase()=='bore water'?Color(0xFF8877DD): Color(0xFFCA86B0), + ), + ), + child: AutoSizeText( + capitalizeFirst(tanker.type_of_water), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(12, Color(0xFFFFFFFF), FontWeight.w400), + ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .008,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isAvailable ? Color(0XFF098603) : Colors.grey, + ), + ), + child: Text( + tanker.status, + style: fontTextStyle( + 12, + isAvailable ? Color(0XFF098603) : Colors.grey, + FontWeight.w500, + ), + ), + ), + /* ElevatedButton( + onPressed: isEnabled + ? () async { + AppSettings.preLoaderDialog(context); + + final productId = tanker.id; + final name = tanker.tanker_name; + final priceString = tanker.price; + final unitPrice = int.parse(priceString.replaceAll(',', '')); + int currentQuantity = localQuantities[productId] ?? 0; + int newQuantity = currentQuantity + 1; + int totalPrice = unitPrice * newQuantity; + + final payload = { + "productId": productId, + "name": name, + "quantity": newQuantity, + "price": unitPrice, + }; + + try { + final response = await http.post( + Uri.parse('${AppSettings.host}cart/${AppSettings.customerId}/add'), + headers: await AppSettings.buildRequestHeaders(), + body: jsonEncode(payload), + ); + + Navigator.of(context, rootNavigator: true).pop(); + + if (response.statusCode == 200 || response.statusCode == 201) { + setState(() { + localQuantities[productId] = newQuantity; + localTotalPrices[productId] = totalPrice; + + int existingIndex = cart.indexWhere((item) => item['productId'] == productId); + if (existingIndex != -1) { + cart[existingIndex]['quantity'] = newQuantity; + cart[existingIndex]['price'] = unitPrice; + cart[existingIndex]['total'] = totalPrice; + } else { + cart.add({ + "productId": productId, + "name": name, + "quantity": newQuantity, + "price": unitPrice, + "total": totalPrice, + }); + } + }); + } else { + print('Failed to add to cart: ${response.body}'); + } + } catch (e) { + Navigator.of(context, rootNavigator: true).pop(); + print('Error adding to cart: $e'); + } + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: isEnabled ? primaryColor : Colors.grey[300], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: Size(64, 36), + padding: EdgeInsets.zero, + ), + child: Text( + 'Add', + style: fontTextStyle(12, Colors.white, FontWeight.w400), + ), + ),*/ + ], + ) + ], + ), + ), + ); + }, + ), + ], + ), + ) + ], + ), + ), + ), + ], + ) + ), + bottomNavigationBar: cart.isNotEmpty + ? Padding( + padding: EdgeInsets.all(0), + child: Container( + height: 56, + width: double.infinity, + decoration: BoxDecoration( + color: Color(0XFFF5CD47), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + padding: EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Order updated", + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CartSummary(details: cart,supplierDetails: widget.details,)), + ); + }, + child: Row( + children: [ + Text( + "View summary", + style: fontTextStyle(12, Color(0XFF232527), FontWeight.w500), + ), + Image.asset( + 'images/arrow-right.png', + fit: BoxFit.cover, + width: + 20, // Match the diameter of the CircleAvatar + height: 20, + color: Color(0XFF343637), + ), + + ], + ), + ), + ], + ), + ), + ) + : null, + + ); + } +} diff --git a/lib/supplier/supplier_orders_model.dart b/lib/supplier/supplier_orders_model.dart new file mode 100644 index 0000000..1678b34 --- /dev/null +++ b/lib/supplier/supplier_orders_model.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:intl/intl.dart'; + +import '../common/settings.dart'; + + +class SupplierOrdersModel { + String typeofwater=''; + String supplierName = ''; + String supplierId = ''; + String date = ''; + String dbId = ''; + String time = ''; + String acceptedTime = ''; + String address=''; + String displayAddress = ''; + String capacity = ''; + String quantity = ''; + String quotedAmount=''; + String bookingCharges=''; + String advancePaid=''; + String status=''; + double lat=0; + double lng=0; + double distanceInMeters=0; + Color cardColor=Colors.white; + + SupplierOrdersModel(); + + factory SupplierOrdersModel.fromJson(Map json){ + SupplierOrdersModel rtvm = new SupplierOrdersModel(); + + rtvm.status = json['status'] ?? ''; + rtvm.typeofwater = json['type_of_water'] ?? ''; + rtvm.date = json['date'] ?? ''; + rtvm.time = json['time'] ?? ''; + rtvm.dbId=json['_id']?? ''; + rtvm.capacity = json['capacity'] ?? ''; + rtvm.quantity = json['quantity'] ?? ''; + final suppliers = json['requested_suppliers'] as List?; + + if (suppliers != null && suppliers.isNotEmpty) { + final supplier = suppliers[0]; + rtvm.quotedAmount = supplier['quoted_amount']?.toString() ?? '0.00'; + rtvm.bookingCharges = supplier['delivery_charges']?.toString() ?? '0.00'; + rtvm.advancePaid = supplier['advance_paid']?.toString() ?? '0.00'; + rtvm.supplierName = supplier['supplier_details']?['supplierName'] ?? ''; + rtvm.supplierId = supplier['supplier_details']?['supplierId'] ?? ''; + rtvm.address = supplier['supplier_details']?['address'] ?? ''; + rtvm.acceptedTime = supplier['time'] ?? ''; + rtvm.lat =supplier['supplier_details']?['latitude'] ?? ''; + rtvm.lng = supplier['supplier_details']?['longitude'] ?? ''; + } else { + rtvm.quotedAmount = ''; + rtvm.bookingCharges=''; + rtvm.supplierName = ''; + rtvm.address = ''; + rtvm.acceptedTime = ''; + rtvm.supplierId=''; + rtvm.lat =0.0; + rtvm.lng =0.0; + } + List parts = rtvm.address.split(','); + if (parts.length > 4) { + rtvm.displayAddress = parts[2].trim(); + } else { + rtvm.displayAddress = rtvm.address; // fallback + } + + + + rtvm.distanceInMeters = double.parse((Geolocator.distanceBetween( + rtvm.lat, + rtvm.lng, + AppSettings.userLatitude, + AppSettings.userLongitude + ) / 1000).toStringAsFixed(2)); + + return rtvm; + } + +} \ No newline at end of file diff --git a/lib/supplier/suppliers_model.dart b/lib/supplier/suppliers_model.dart new file mode 100644 index 0000000..27d2a00 --- /dev/null +++ b/lib/supplier/suppliers_model.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:watermanagement/common/settings.dart'; + + +class SuppliersModel { + String supplier_name = ''; + String status=''; + String supplier_address=''; + String supplier_phone_number=''; + String supplier_id=''; + Color text_color=Colors.black; + double lat=0; + double lng=0; + String distrubance_price=''; + String amount_difference=''; + double distanceInMeters=0; + String displayAddress=''; + bool isFavorite=false; + bool isRequetsedBooking=false; + String? matchedPrice; + + SuppliersModel(); + + factory SuppliersModel.fromJson(Map json){ + SuppliersModel rtvm = new SuppliersModel(); + + rtvm.supplier_name = json['supplier']['suppliername'] ?? ''; + rtvm.status = json['supplier']['status'] ?? ''; + rtvm.supplier_address = json['supplier']['profile']['office_address'] ?? ''; + rtvm.supplier_phone_number =json['supplier']['phone'] ?? ''; + rtvm.supplier_id = json['supplier']['supplierId'] ?? ''; + rtvm.isFavorite = json['isFavorite'] ?? false; + rtvm.isRequetsedBooking = json['requestedBooking']['status'] ?? false; + rtvm.lat = json['supplier']['latitude'] ?? 0; + rtvm.lng = json['supplier']['longitude'] ?? 0; + List parts = rtvm.supplier_address.split(','); + rtvm.displayAddress = parts[2].trim(); + rtvm.distanceInMeters = double.parse((Geolocator.distanceBetween( + rtvm.lat, + rtvm.lng, + AppSettings.userLatitude, + AppSettings.userLongitude + ) / 1000).toStringAsFixed(2)); + + + + + return rtvm; + } + +} \ No newline at end of file diff --git a/lib/supplier/todaysprice/custom_calendar.dart b/lib/supplier/todaysprice/custom_calendar.dart new file mode 100644 index 0000000..955204a --- /dev/null +++ b/lib/supplier/todaysprice/custom_calendar.dart @@ -0,0 +1,413 @@ +import 'package:flutter/material.dart'; + +import '../../common/settings.dart'; + + +class AdvancedDateRangePicker extends StatefulWidget { + final DateTimeRange? initialRange; + + const AdvancedDateRangePicker({super.key, this.initialRange}); + + @override + State createState() => _AdvancedDateRangePickerState(); +} + +class _AdvancedDateRangePickerState extends State { + late DateTime _currentMonth; + DateTime? _start; + DateTime? _end; + + late PageController _pageController; + int _initialPage = 500; // large buffer for infinite scroll + + @override + void initState() { + super.initState(); + + _initialPage = 500; + _pageController = PageController(initialPage: _initialPage); + + _currentMonth = DateTime(DateTime.now().year, DateTime.now().month, 1); + + if (widget.initialRange != null) { + _start = widget.initialRange!.start; + _end = widget.initialRange!.end; + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, // White background + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + padding: const EdgeInsets.all(5), + width: 420, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeader(), + + const SizedBox(height: 12), + + _buildWeekDays(), + + SizedBox( + height: 270, + child: PageView.builder( + controller: _pageController, + onPageChanged: (pageIndex) { //to update the current month state when user swipes + setState(() { + int diff = pageIndex - _initialPage; + _currentMonth = DateTime( + DateTime.now().year, + DateTime.now().month + diff, + 1, + ); + }); + }, + itemBuilder: (context, index) { //to build the calendar for each month + int diff = index - _initialPage; + DateTime monthToShow = DateTime( + DateTime.now().year, + DateTime.now().month + diff, + 1, + ); + + return _buildCalendarGrid(monthToShow); + }, + ), + ), + + + const SizedBox(height: 12), + + _buildSelectionText(), + + const SizedBox(height: 16), + + _buildFooter() + ], + ), + ), + ); + } + + // ----------------------------------------------------------- + // πŸ”΅ HEADER (Month – Year + Arrows) + // ----------------------------------------------------------- + Widget _buildHeader() { + return Column( + children: [ + // Title + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Text( + "Select a Date Range", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.arrow_left, color: Colors.black), + onPressed: () { + _pageController.previousPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + }, + + ), + + GestureDetector( + onTap: () => _showYearPicker(context), + child: Text( + "${_monthName(_currentMonth.month)} ${_currentMonth.year}", + style: fontTextStyle(18, Colors.black, FontWeight.w400), + ), + ), + + IconButton( + icon: const Icon(Icons.arrow_right, color: Colors.black), + onPressed: () { + _pageController.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + }, + ), + ], + ) + ], + ); + } + + // ----------------------------------------------------------- + // πŸ”΅ WEEK DAY ROW + // ----------------------------------------------------------- + Widget _buildWeekDays() { + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: days + .map((d) => Expanded( + child: Center( + child: Text( + d, + style: fontTextStyle(14, Colors.black, FontWeight.w600), + ), + ), + )) + .toList(), + ); + } + + // ----------------------------------------------------------- + // πŸ”΅ BUILD CALENDAR GRID + // ----------------------------------------------------------- + Widget _buildCalendarGrid(DateTime month) { + List rows = []; + + DateTime firstDay = DateTime(month.year, month.month, 1); + int startingWeekday = firstDay.weekday % 7; // Sunday=0 + int daysInMonth = DateTime(month.year, month.month + 1, 0).day; + + List dayCells = []; + + // Empty cells before month starts + for (int i = 0; i < startingWeekday; i++) { + dayCells.add(const Expanded(child: SizedBox())); + } + + // Month days + for (int d = 1; d <= daysInMonth; d++) { + DateTime date = DateTime(month.year, month.month, d); + + bool isToday = _isSame(date, DateTime.now()); // Highlight today + bool isSelectedStart = _isSame(date, _start); + bool isSelectedEnd = _isSame(date, _end); + bool inRange = + _start != null && _end != null && date.isAfter(_start!) && date.isBefore(_end!); + + dayCells.add( + Expanded( + child: GestureDetector( + onTap: () => _onDateTap(date), + child: Container( + margin: const EdgeInsets.all(4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSelectedStart || isSelectedEnd + ? Colors.black + : inRange + ? Colors.black12 + : Colors.transparent, + shape: BoxShape.circle, + border: isToday && !isSelectedStart && !isSelectedEnd + ? Border.all(color: Colors.blueAccent, width: 2) // Highlight today + : null, + ), + child: Center( + child: Text( + "$d", + style: TextStyle( + color: isSelectedStart || isSelectedEnd + ? Colors.white + : Colors.black, + fontWeight: isToday ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ), + ), + ), + ); + + // Create a row after each week + if ((dayCells.length) % 7 == 0) { + rows.add(Row(children: dayCells)); + dayCells = []; + } + } + + // Last row fill remaining empty cells + if (dayCells.isNotEmpty) { + while (dayCells.length < 7) { + dayCells.add(const Expanded(child: SizedBox())); + } + rows.add(Row(children: dayCells)); + } + + return Column(children: rows); + } + + + // ----------------------------------------------------------- + // πŸ”΅ DATE TAP LOGIC + // ----------------------------------------------------------- + void _onDateTap(DateTime date) { + setState(() { + if (_start == null || (_start != null && _end != null)) { + _start = date; + _end = null; + } else if (date.isBefore(_start!)) { + _end = _start; + _start = date; + } else { + _end = date; + } + }); + } + + // ----------------------------------------------------------- + // πŸ”΅ SELECTED TEXT + // ----------------------------------------------------------- + // Widget _buildSelectionText() { + // return Text( + // _start == null + // ? "Choose Start Date" + // : _end == null + // ? "Choose End Date" + // : "Selected: ${_fmt(_start!)} β†’ ${_fmt(_end!)}", + // style: fontTextStyle(14, Colors.blueAccent, FontWeight.w600), + // ); + // } + + Widget _buildSelectionText() { + if (_start == null || _end == null) { + return const SizedBox(); // Show nothing + } + + return Text( + "Selected: ${_fmt(_start!)} β†’ ${_fmt(_end!)}", + style: fontTextStyle(14, Colors.blueAccent, FontWeight.w600), + ); + } + + + // ----------------------------------------------------------- + // πŸ”΅ FOOTER BUTTONS + // ----------------------------------------------------------- + Widget _buildFooter() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + child: Text("Cancel", style: fontTextStyle(14, Colors.black, FontWeight.w600)), + onPressed: () => Navigator.pop(context), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + ), + onPressed: (_start != null && _end != null) + ? () => Navigator.pop( + context, + DateTimeRange(start: _start!, end: _end!), + ) + : null, + child: Text("Apply", style: fontTextStyle(14, Colors.white, FontWeight.bold)), + ), + ], + ); + } + + // ----------------------------------------------------------- + // πŸ”§ HELPERS + // ----------------------------------------------------------- + bool _isSame(DateTime? a, DateTime? b) { + if (a == null || b == null) return false; + return a.year == b.year && a.month == b.month && a.day == b.day; + } + + String _fmt(DateTime d) => "${d.year}-${d.month}-${d.day}"; + + String _monthName(int m) { + const names = [ + "January","February","March","April","May","June", + "July","August","September","October","November","December" + ]; + return names[m - 1]; + } + + // ----------------------------------------------------------- + // πŸ”΅ YEAR PICKER SHEET + // ----------------------------------------------------------- + void _showYearPicker(BuildContext context) { + int currentYear = DateTime.now().year; + int startYear = currentYear - 25; + int endYear = currentYear + 25; + int selectedYear = _currentMonth.year; + + ScrollController scrollController = ScrollController( + initialScrollOffset: (selectedYear - startYear) * 48.0, // 56 is approx ListTile height + ); + + showDialog( + context: context, + builder: (_) => Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: SizedBox( + height: 300, + child: ListView.builder( + controller: scrollController, + itemCount: endYear - startYear + 1, + itemBuilder: (context, index) { + int y = startYear + index; + bool isSelected = y == selectedYear; + + return Center( + child: GestureDetector( + onTap: () { + setState(() { + _currentMonth = DateTime(y, _currentMonth.month, 1); + }); + Navigator.pop(context); + }, + + child: Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: isSelected ? Colors.black : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.black12), + ), + + child: Text( + "$y", + style: fontTextStyle( + 16, + isSelected ? Colors.white : Colors.black, + FontWeight.bold, + ), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } + + //To clear the selected dates inside your custom calendar grid + void _clearCalendarSelection() { + setState(() { + _start = null; + _end = null; + }); + } + +} diff --git a/lib/supplier/todaysprice/graph_data2.dart b/lib/supplier/todaysprice/graph_data2.dart new file mode 100644 index 0000000..4adbf4e --- /dev/null +++ b/lib/supplier/todaysprice/graph_data2.dart @@ -0,0 +1,864 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:watermanagement/supplier/todaysprice/custom_calendar.dart'; + +import '../../common/settings.dart'; + +// Contains date picker + filtered graph +class GraphScreen extends StatefulWidget { + final int selectedType; + + const GraphScreen({super.key, required this.selectedType}); + + @override + State createState() => _GraphScreenState(); +} + +class _GraphScreenState extends State { + List> filteredDrinking = []; + List> filteredBore = []; + + DateTimeRange? selectedRange; + + @override + void initState() { + super.initState(); + _loadLastWeek(); // Load last week by default + } + + void _loadLastWeek() { + DateTime today = DateTime.now(); + DateTime lastWeek = today.subtract(const Duration(days: 7)); + debugPrint('$lastWeek'); + + // Update selectedRange so the card shows last week + selectedRange = DateTimeRange(start: lastWeek, end: today); + + _filterData(lastWeek, today); + + setState(() {}); // trigger rebuild + } + + void _pickDateRange() async { + final result = await showDialog( + context: context, + builder: (context) => AdvancedDateRangePicker( + initialRange: selectedRange, + ), + ); + + if (result != null) { + selectedRange = result; + _filterData(result.start, result.end); + setState(() { + selectedRange = result; // store this for showing in card above graph + }); + } + } + + void _filterData(DateTime start, DateTime end) { + setState(() { + filteredDrinking = weeklyPriceData["Drinking Water"]! + .where((d) { + DateTime date = DateTime.parse(d["date"]); + return !date.isBefore(start) && !date.isAfter(end); + }).toList(); + + filteredBore = weeklyPriceData["bore Water"]! + .where((d) { + DateTime date = DateTime.parse(d["date"]); + return !date.isBefore(start) && !date.isAfter(end); + }).toList(); + }); + } + + void _openDateMenu() { + showMenu( + context: context, + color: Colors.white, + elevation: 6, // smooth shadow + position: const RelativeRect.fromLTRB(1000, 100, 10, 0), // position near icon + items: [ + PopupMenuItem( + value: "week", + child: Text("Last Week", + style: fontTextStyle(14, Colors.black, FontWeight.w500), + ), + ), + PopupMenuItem( + value: "month", + child: Text("Last Month", + style: fontTextStyle(14, Colors.black, FontWeight.w500), + ), + ), + PopupMenuItem( + value: "custom", + child: Text("Custom Dates", + style: fontTextStyle(14, Colors.black, FontWeight.w500), + ), + ), + ], + ).then((value) { + if (value == null) return; + if (value == "week") { + DateTime today = DateTime.now(); + DateTime lastWeek = today.subtract(const Duration(days: 7)); + setState(() { + selectedRange = DateTimeRange(start: lastWeek, end: today); + _filterData(lastWeek, today); + }); + } + else if (value == "month") { + DateTime today = DateTime.now(); + DateTime lastMonth = DateTime(today.year, today.month - 1, today.day); + setState(() { + selectedRange = DateTimeRange(start: lastMonth, end: today); + _filterData(lastMonth, today); + }); + } + else if (value == "custom") { + _pickDateRange(); + } + }); + } + + // For showing date in card above graph + String _formattedDateRange() { + if (selectedRange == null) return "Select Date"; + + final start = selectedRange!.start; + final end = selectedRange!.end; + + const months = [ + "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ]; + + if (start.month == end.month) { + return "${start.day} – ${end.day} ${months[start.month]}"; + } else { + return "${start.day} ${months[start.month]} – ${end.day} ${months[end.month]}"; + } + } + + String _formattedAvgPrice() { + List> activeList = + widget.selectedType == 0 ? filteredDrinking : filteredBore; + + debugPrint('$activeList'); + + if (activeList.isEmpty) return "-"; + + double total = 0; + for (var item in activeList) { + total += (item["price"] as num).toDouble(); + } + + double avg = total / activeList.length; + return "${avg.toStringAsFixed(2)}"; + } + + + + @override + Widget build(BuildContext context) { + return Column( + children: [ + + // πŸ”₯ TOP ROW (LEFT: BOX, RIGHT: CALENDAR) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + // LEFT BOX + Padding( + padding: const EdgeInsets.only(top: 14, left: 10), + child: Container( + height: 100, + width: 180, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 8, + spreadRadius: 2, + offset: const Offset(2, 4), + ), + ], + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0.2, 0), // Soft slide from right + end: Offset.zero, + ).animate(animation), + child: child, + ), + ); + }, + child: Column( + key: ValueKey(widget.selectedType), // IMPORTANT πŸ”‘ + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.selectedType == 0 ? "Drinking Water" : "Bore Water", + style: fontTextStyle(14, Colors.black, FontWeight.w600), + ), + + const SizedBox(height: 4), + + Text( + _formattedDateRange(), + style: fontTextStyle(14, Colors.grey.shade700, FontWeight.w500), + ), + + const SizedBox(height: 4), + + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${_formattedAvgPrice()} ", + style: fontTextStyle(20, Colors.black, FontWeight.bold), + ), + TextSpan( + text: "/5000 L", + style: fontTextStyle(14, Colors.grey.shade600, FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + ), + ), + + // RIGHT CALENDAR BUTTON + Padding( + padding: const EdgeInsets.only(top: 4, right: 8), + child: InkWell( + onTap: () => _openDateMenu(), + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey.shade300), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: const [ + Icon(Icons.calendar_month, size: 28, color: Colors.blueAccent), + Icon(Icons.arrow_drop_down, size: 28, color: Colors.blueAccent), + ], + ), + ), + ), + ), + ], + ), + + const SizedBox(height: 5), + + // Show graph only if data is available + if (filteredDrinking.isNotEmpty || filteredBore.isNotEmpty) + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 10), + child: DoublePriceBarGraph( + drinkingData: filteredDrinking, + boreData: filteredBore, + ), + ), + ) + else + Padding( + padding: EdgeInsets.all(30), + child: Text("No data available for the selected dates", + style: fontTextStyle(13, Colors.black87, FontWeight.w600), + ), + ), + ], + ); + } +} + + +Widget buildLegend() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Drinking Water Legend + Row( + children: [ + Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.blueAccent, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 6), + + Text( + "Drinking Water", + style: fontTextStyle(12, Colors.black87, FontWeight.w600), + ), + ], + ), + + const SizedBox(width: 20), + + // Bore Water Legend + Row( + children: [ + Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.orangeAccent, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(width: 6), + + Text( + "Bore Water", + style: fontTextStyle(12, Colors.black87, FontWeight.w600), + ), + ], + ), + ], + ); +} + + +class DoublePriceBarGraph extends StatelessWidget { + final List> drinkingData; + final List> boreData; + + const DoublePriceBarGraph({ + super.key, + required this.drinkingData, + required this.boreData, + }); + + @override + Widget build(BuildContext context) { + final drinkMap = {for (var d in drinkingData) d["date"]: d}; + final boreMap = {for (var b in boreData) b["date"]: b}; + + final allDates = [...drinkMap.keys, ...boreMap.keys].toSet().toList() + ..sort((a, b) => + DateTime.parse(a).compareTo(DateTime.parse(b))); // FIXED + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: allDates.map((date) { + final d = drinkMap[date]; + final b = boreMap[date]; + + double drinkPrice = d?["price"]?.toDouble() ?? 0.0; + double borePrice = b?["price"]?.toDouble() ?? 0.0; + + String day = DateTime.parse(date).day.toString(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Drinking + _buildBar(drinkPrice, Colors.blueAccent), + + const SizedBox(width: 0), + + // Bore + _buildBar(borePrice, Colors.orangeAccent) + ], + ), + const SizedBox(height: 6), + Text(day, style: fontTextStyle(12, Colors.black87, FontWeight.w600)), + ], + ), + ); + }).toList(), + ); + } + + Widget _buildBar(double price, Color color) { + return Column( + children: [ + Text( + "β‚Ή${price.toInt()}", + style: fontTextStyle(9, color, FontWeight.bold), + ), + Container( + width: 20, + height: price / 15, // Adjust scaling if bars too short + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(5), + ), + ), + ], + ); + } +} + + + + + +final Map>> weeklyPriceData = { + + "Drinking Water": [ + { + "date": "2025-11-01", + "price": 1570, + "minPrice": 1550, + "maxPrice": 1650, + "avgPrice": 1600 + }, + { + "date": "2025-11-02", + "price": 1590, + "minPrice": 1560, + "maxPrice": 1660, + "avgPrice": 1610 + }, + { + "date": "2025-11-03", + "price": 1550, + "minPrice": 1520, + "maxPrice": 1600, + "avgPrice": 1560 + }, + { + "date": "2025-11-04", + "price": 1610, + "minPrice": 1580, + "maxPrice": 1670, + "avgPrice": 1620 + }, + { + "date": "2025-11-05", + "price": 1630, + "minPrice": 1600, + "maxPrice": 1700, + "avgPrice": 1650 + }, + { + "date": "2025-11-06", + "price": 1580, + "minPrice": 1550, + "maxPrice": 1650, + "avgPrice": 1600 + }, + { + "date": "2025-11-07", + "price": 1600, + "minPrice": 1580, + "maxPrice": 1670, + "avgPrice": 1620 + }, + { + "date": "2025-11-08", + "price": 1640, + "minPrice": 1600, + "maxPrice": 1700, + "avgPrice": 1650 + }, + { + "date": "2025-11-09", + "price": 1620, + "minPrice": 1590, + "maxPrice": 1680, + "avgPrice": 1630 + }, + { + "date": "2025-11-10", + "price": 1575, + "minPrice": 1550, + "maxPrice": 1640, + "avgPrice": 1600 + }, + + { + "date": "2025-11-11", + "price": 1585, + "minPrice": 1550, + "maxPrice": 1650, + "avgPrice": 1605 + }, + { + "date": "2025-11-12", + "price": 1615, + "minPrice": 1580, + "maxPrice": 1670, + "avgPrice": 1625 + }, + { + "date": "2025-11-13", + "price": 1590, + "minPrice": 1560, + "maxPrice": 1650, + "avgPrice": 1610 + }, + { + "date": "2025-11-14", + "price": 1645, + "minPrice": 1610, + "maxPrice": 1710, + "avgPrice": 1660 + }, + { + "date": "2025-11-15", + "price": 1625, + "minPrice": 1590, + "maxPrice": 1680, + "avgPrice": 1635 + }, + { + "date": "2025-11-16", + "price": 1580, + "minPrice": 1550, + "maxPrice": 1640, + "avgPrice": 1595 + }, + { + "date": "2025-11-17", + "price": 1570, + "minPrice": 1540, + "maxPrice": 1630, + "avgPrice": 1580 + }, + { + "date": "2025-11-18", + "price": 1600, + "minPrice": 1580, + "maxPrice": 1660, + "avgPrice": 1620 + }, + { + "date": "2025-11-19", + "price": 1630, + "minPrice": 1600, + "maxPrice": 1690, + "avgPrice": 1650 + }, + { + "date": "2025-11-20", + "price": 1650, + "minPrice": 1610, + "maxPrice": 1720, + "avgPrice": 1670 + }, + + { + "date": "2025-11-21", + "price": 1610, + "minPrice": 1580, + "maxPrice": 1670, + "avgPrice": 1630 + }, + { + "date": "2025-11-22", + "price": 1590, + "minPrice": 1560, + "maxPrice": 1650, + "avgPrice": 1605 + }, + { + "date": "2025-11-23", + "price": 1620, + "minPrice": 1590, + "maxPrice": 1680, + "avgPrice": 1630 + }, + { + "date": "2025-11-24", + "price": 1670, + "minPrice": 1630, + "maxPrice": 1730, + "avgPrice": 1680 + }, + { + "date": "2025-11-25", + "price": 1690, + "minPrice": 1650, + "maxPrice": 1750, + "avgPrice": 1700 + }, + { + "date": "2025-11-26", + "price": 1580, + "minPrice": 1550, + "maxPrice": 1640, + "avgPrice": 1600 + }, + { + "date": "2025-11-27", + "price": 1600, + "minPrice": 1570, + "maxPrice": 1670, + "avgPrice": 1620 + }, + { + "date": "2025-11-28", + "price": 1615, + "minPrice": 1580, + "maxPrice": 1680, + "avgPrice": 1630 + }, + { + "date": "2025-11-29", + "price": 1635, + "minPrice": 1600, + "maxPrice": 1700, + "avgPrice": 1650 + }, + { + "date": "2025-11-30", + "price": 1605, + "minPrice": 1580, + "maxPrice": 1670, + "avgPrice": 1620 + }, + ], + + "bore Water": [ + { + "date": "2025-11-01", + "price": 780, + "minPrice": 760, + "maxPrice": 810, + "avgPrice": 790 + }, + { + "date": "2025-11-02", + "price": 800, + "minPrice": 780, + "maxPrice": 840, + "avgPrice": 810 + }, + { + "date": "2025-11-03", + "price": 770, + "minPrice": 750, + "maxPrice": 800, + "avgPrice": 780 + }, + { + "date": "2025-11-04", + "price": 820, + "minPrice": 800, + "maxPrice": 850, + "avgPrice": 830 + }, + { + "date": "2025-11-05", + "price": 830, + "minPrice": 810, + "maxPrice": 860, + "avgPrice": 840 + }, + { + "date": "2025-11-06", + "price": 785, + "minPrice": 760, + "maxPrice": 820, + "avgPrice": 795 + }, + { + "date": "2025-11-07", + "price": 810, + "minPrice": 790, + "maxPrice": 840, + "avgPrice": 820 + }, + { + "date": "2025-11-08", + "price": 825, + "minPrice": 800, + "maxPrice": 860, + "avgPrice": 835 + }, + { + "date": "2025-11-09", + "price": 795, + "minPrice": 770, + "maxPrice": 830, + "avgPrice": 810 + }, + { + "date": "2025-11-10", + "price": 820, + "minPrice": 790, + "maxPrice": 860, + "avgPrice": 830 + }, + + { + "date": "2025-11-11", + "price": 810, + "minPrice": 780, + "maxPrice": 850, + "avgPrice": 820 + }, + { + "date": "2025-11-12", + "price": 830, + "minPrice": 800, + "maxPrice": 870, + "avgPrice": 840 + }, + { + "date": "2025-11-13", + "price": 785, + "minPrice": 770, + "maxPrice": 820, + "avgPrice": 795 + }, + { + "date": "2025-11-14", + "price": 750, + "minPrice": 730, + "maxPrice": 790, + "avgPrice": 760 + }, + { + "date": "2025-11-15", + "price": 770, + "minPrice": 750, + "maxPrice": 810, + "avgPrice": 780 + }, + { + "date": "2025-11-16", + "price": 825, + "minPrice": 800, + "maxPrice": 870, + "avgPrice": 840 + }, + { + "date": "2025-11-17", + "price": 840, + "minPrice": 820, + "maxPrice": 880, + "avgPrice": 850 + }, + { + "date": "2025-11-18", + "price": 810, + "minPrice": 790, + "maxPrice": 840, + "avgPrice": 820 + }, + { + "date": "2025-11-19", + "price": 790, + "minPrice": 770, + "maxPrice": 830, + "avgPrice": 800 + }, + { + "date": "2025-11-20", + "price": 830, + "minPrice": 800, + "maxPrice": 870, + "avgPrice": 840 + }, + + { + "date": "2025-11-21", + "price": 850, + "minPrice": 820, + "maxPrice": 890, + "avgPrice": 860 + }, + { + "date": "2025-11-22", + "price": 780, + "minPrice": 760, + "maxPrice": 810, + "avgPrice": 790 + }, + { + "date": "2025-11-23", + "price": 790, + "minPrice": 770, + "maxPrice": 820, + "avgPrice": 800 + }, + { + "date": "2025-11-24", + "price": 835, + "minPrice": 810, + "maxPrice": 870, + "avgPrice": 845 + }, + { + "date": "2025-11-25", + "price": 820, + "minPrice": 800, + "maxPrice": 860, + "avgPrice": 830 + }, + { + "date": "2025-11-26", + "price": 760, + "minPrice": 740, + "maxPrice": 800, + "avgPrice": 780 + }, + { + "date": "2025-11-27", + "price": 810, + "minPrice": 780, + "maxPrice": 850, + "avgPrice": 820 + }, + { + "date": "2025-11-28", + "price": 795, + "minPrice": 770, + "maxPrice": 830, + "avgPrice": 805 + }, + { + "date": "2025-11-29", + "price": 835, + "minPrice": 810, + "maxPrice": 870, + "avgPrice": 845 + }, + { + "date": "2025-11-30", + "price": 820, + "minPrice": 790, + "maxPrice": 860, + "avgPrice": 830 + }, + ] +}; diff --git a/lib/supplier/todaysprice/todays_price.dart b/lib/supplier/todaysprice/todays_price.dart new file mode 100644 index 0000000..d8a0f19 --- /dev/null +++ b/lib/supplier/todaysprice/todays_price.dart @@ -0,0 +1,481 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:watermanagement/supplier/todaysprice/graph_data2.dart'; + +import '../../common/settings.dart'; + + +class TodaysPriceMainScreen extends StatefulWidget { + const TodaysPriceMainScreen({super.key}); + + @override + State createState() => _TodaysPriceMainScreenState(); +} + +class _TodaysPriceMainScreenState extends State { + int selectedType = 0; + late PageController waterController; + + @override + void initState() { + super.initState(); + waterController = PageController(); + } + + @override + void dispose() { + waterController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 3, + + leading: Padding( + padding: const EdgeInsets.only(left: 10), + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + padding: const EdgeInsets.all(6), + child: const Icon(Icons.arrow_back_ios_new, + size: 18, color: Colors.black + ), + ), + ), + ), + + title: Text( + "Today's Prices", + style: fontTextStyle(20, Colors.black, FontWeight.w600), + ), + ), + + body: Column( + children: [ + // TOP CARD WITH CHART (80% height) + Container( + height: MediaQuery.of(context).size.height* 0.38, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30) + ) + ), + + child: Column( + children: [ + + // DOUBLE BAR GRAPH + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: GraphScreen(selectedType: selectedType), + ), + ), + + const SizedBox(height: 1), + + // LEGEND + buildLegend(), + const SizedBox(height: 10), + ], + ), + ), + + const SizedBox(height: 15), + + // ------------ WATER TABS ------------ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + waterTab(0), + const SizedBox(width: 20), + waterTab(1), + ], + ), + const SizedBox(height: 10), + + // ----------- PAGE VIEW – SWIPE FOR BOTH ----------- + Expanded( + child: PageView.builder( + itemCount: 2, // 0 = Drinking, 1 = Bore + controller: waterController, // πŸ”₯ added controller + scrollDirection: Axis.horizontal, + onPageChanged: (i) => setState(() => selectedType = i), + itemBuilder: (context, pageIndex) { + final title = pageIndex == 0 ? "Drinking Water" : "Bore Water"; + final list = pageIndex == 0 + ? drinkingWaterSuppliers + : boreWaterSuppliers; + + return Stack( + children: [ + // SCROLLING SUPPLIER LIST UNDER WATER CARD + Positioned.fill( + child: ListView.builder( + padding: EdgeInsets.only(top: 150), + itemCount: list.length, + itemBuilder: (context, i) { + final s = list[i]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SupplierCard( + name: s["name"]!, + distance: s["distance"]!, + rating: s["rating"]!, + reviews: s["reviews"]!, + price: s["price"]!, + ), + ); + }, + ), + ), + + // WATER CARD FIXED ON TOP + Positioned( + top: 0, + left: 25, + right: 25, + child: WaterCard( + title: selectedType == 0 ? "Drinking Water" : "Bore Water", + priceRange: selectedType == 0 + ? "β‚Ή1,575 – β‚Ή1,700" + : "β‚Ή740 – β‚Ή850", + ), + ) + ], + ); + + } + ) + ) + ], + ) + ); + } + + Widget waterTab(int i) { + return GestureDetector( + onTap: () { + waterController.animateToPage( + i, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + setState(() => selectedType = i); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + width: 160, + height: 4, + decoration: BoxDecoration( + color: selectedType == i ? Colors.white : Colors.white24, + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + +} + + +class WaterSection extends StatefulWidget { + final Function(int) onTabChanged; + + const WaterSection({super.key, required this.onTabChanged}); + + @override + State createState() => _WaterSectionState(); +} + +class _WaterSectionState extends State { + final PageController controller = PageController(); + int index = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // ------------ TABS ------------ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + tabBar(0), + const SizedBox(width: 20), + tabBar(1), + ], + ), + + // const SizedBox(height: 20), + + // ------------ PAGE VIEW ------------ + Expanded( + child: Padding( + padding: EdgeInsets.only(top: 10), + child: PageView( + controller: controller, + onPageChanged: (i) { + setState(() => index = i); + widget.onTabChanged(i); // πŸ”₯ send tab index to parent + }, + children: const [ + WaterCard( + title: "Drinking Water", + priceRange: "β‚Ή1,200 – β‚Ή1,500", + ), + WaterCard( + title: "Bore Water", + priceRange: "β‚Ή740 – β‚Ή850", + ), + ], + + ), + ), + ) + ], + ); + } + + // ---------------- TAB BAR --------------- + Widget tabBar(int i) { + return GestureDetector( + onTap: () { + controller.animateToPage( + i, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + setState(() => index = i); + widget.onTabChanged(i); // πŸ”₯ send index + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + width: 160, + height: 4, + decoration: BoxDecoration( + color: index == i ? Colors.white : Colors.white24, + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } +} + +class WaterCard extends StatelessWidget { + final String title; + final String priceRange; + final String volume; + + const WaterCard({ + super.key, + required this.title, + required this.priceRange, + this.volume = "/5,000L", // Default value + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 6), + padding: const EdgeInsets.all(18), + height: 130, + width: 350, + decoration: BoxDecoration( + color: const Color(0xFF2E2E2E), + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 8, + offset: const Offset(0, 3), + ) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: fontTextStyle(16, Colors.white.withOpacity(0.8), FontWeight.w600), + ), + + const SizedBox(height: 6), + + Text( + priceRange, + style: fontTextStyle(26, Colors.white, FontWeight.w700), + ), + + const SizedBox(height: 4), + + Text( + volume, + style: fontTextStyle(14, Colors.white54, FontWeight.w400), + ), + ], + ), + ); + } +} + +final List> drinkingWaterSuppliers = [ + { + "name": "Balaji Suppliers", + "distance": "2.5 km", + "rating": "4.2", + "reviews": "20k+", + "price": "β‚Ή1575" + }, + { + "name": "Manikanta Water", + "distance": "4.2 km", + "rating": "4.5", + "reviews": "18k+", + "price": "β‚Ή1650" + }, + { + "name": "Sai Durga Water", + "distance": "3.1 km", + "rating": "4.1", + "reviews": "25k+", + "price": "β‚Ή1500" + }, + { + "name": "Tirupati Suppliers", + "distance": "1.5 km", + "rating": "4.2", + "reviews": "20k+", + "price": "β‚Ή1500" + }, +]; + +final List> boreWaterSuppliers = [ + { + "name": "Green Borewell", + "distance": "3.4 km", + "rating": "4.0", + "reviews": "12k+", + "price": "β‚Ή799" + }, + { + "name": "Crystal Bore Water", + "distance": "5.1 km", + "rating": "4.3", + "reviews": "9k+", + "price": "β‚Ή850" + }, + { + "name": "PureDrop Bore", + "distance": "2.8 km", + "rating": "4.1", + "reviews": "14k+", + "price": "β‚Ή820" + }, + { + "name": "Blue Borewell", + "distance": "4.4 km", + "rating": "4.3", + "reviews": "10k+", + "price": "β‚Ή899" + }, +]; + + +class SupplierCard extends StatelessWidget { + final String name; + final String distance; + final String rating; + final String reviews; + final String price; + + const SupplierCard({ + super.key, + required this.name, + required this.distance, + required this.rating, + required this.reviews, + required this.price, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFF2F2F2).withOpacity(0.2), // light grey + borderRadius: BorderRadius.circular(22), + border: Border.all(color: Colors.black12, width: 1), + ), + + child: Row( + children: [ + // Avatar + const CircleAvatar( + radius: 22, + backgroundColor: Colors.white, + child: Icon(Icons.person, color: Colors.grey), + ), + + const SizedBox(width: 12), + + // Name + Location + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: fontTextStyle(17, Colors.white.withOpacity(0.95), FontWeight.w700), + ), + const SizedBox(height: 4), + Text( + "Location: $distance", + style: fontTextStyle(12, Colors.white, FontWeight.w500), + ), + ], + ), + ), + + // Rating badge + Price + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.star, color: Colors.amber, size: 14), + Text( + " $rating (${reviews})", + style: fontTextStyle(12, Colors.black87, FontWeight.w600), + ), + ], + ), + ), + + const SizedBox(height: 10), + + Text( + price, + style: fontTextStyle(18, Colors.white.withOpacity(0.95), FontWeight.w700), + ), + ], + ), + ], + ), + ); + } +}