From c47bddf151214dbe8908a4206940220c9d7f4964 Mon Sep 17 00:00:00 2001 From: Sneha Date: Fri, 20 Mar 2026 11:52:19 +0530 Subject: [PATCH] changes --- lib/common/empty.dart | 1185 ++++++++++++++++ lib/common/settings.dart | 143 ++ lib/core/auth_manager.dart | 37 + lib/resources/driver_details.dart | 1887 +++++++++++++++++--------- lib/resources/drivers_model.dart | 6 + lib/resources/resources_drivers.dart | 1173 ++++++++++++---- lib/resources/resources_fleet.dart | 785 ++++++++++- lib/resources/tanker_details.dart | 561 +++++++- lib/resources/tankers_model.dart | 3 + 9 files changed, 4897 insertions(+), 883 deletions(-) create mode 100644 lib/common/empty.dart create mode 100644 lib/core/auth_manager.dart diff --git a/lib/common/empty.dart b/lib/common/empty.dart new file mode 100644 index 0000000..b837f6b --- /dev/null +++ b/lib/common/empty.dart @@ -0,0 +1,1185 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:supplier_new/resources/driver_trips_model.dart'; +import 'package:supplier_new/resources/resources_drivers.dart'; +import '../common/settings.dart'; + +class DriverDetailsPage extends StatefulWidget { + var driverDetails; + var status; + DriverDetailsPage({this.driverDetails, this.status}); + + @override + State createState() => _DriverDetailsPageState(); +} + +class _DriverDetailsPageState extends State { + + // tweak if you want a different size/offset + double _avSize = 80; // avatar diameter + double _cardHPad = 16; // horizontal padding + double _cardTPad = 16; // top inner padding + double _avatarOverlap = 28; // how much avatar rises above the card + + final _nameCtrl = TextEditingController(); + final _mobileCtrl = TextEditingController(); + final _altMobileCtrl = TextEditingController(); + final _locationCtrl = TextEditingController(); + final _licenseController = TextEditingController(); + final _experienceController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + // Unused in UI but kept if you later need them + final _commissionCtrl = TextEditingController(); + final _joinDateCtrl = TextEditingController(); + bool isLoading=false; + + List driverTripsList = []; + bool isTripsLoading = false; + + // Dropdown state + String? _status; // 'available' | 'on delivery' | 'offline' + final List _statusOptions = const [ + 'Available', + 'On delivery', + 'Offline' + ]; + + /*String? selectedLicense; + final List licenseNumbers = const [ + 'DL-042019-9876543', + 'DL-052020-1234567', + 'DL-072021-7654321', + ]; +*/ + + String? _required(String? v, {String field = "This field"}) { + if (v == null || v.trim().isEmpty) return "$field is required"; + return null; + } + + String? _validatePhone(String? v, {String label = "Phone"}) { + if (v == null || v.trim().isEmpty) return "$label is required"; + if (v.trim().length != 10) return "$label must be 10 digits"; + return null; + } + + + @override + void initState(){ + + super.initState(); + + _fetchDriverTrips(); + + } + + Future _fetchDriverTrips() async { + + setState(() => isTripsLoading = true); + + try{ + + String response = + await AppSettings.getDriverTrips( + widget.driverDetails.phone_number + ); + + final data = + (jsonDecode(response)['data'] as List) + .map((e)=>DriverTripsModel.fromJson(e)) + .toList(); + + setState(() { + + driverTripsList = data; + + isTripsLoading = false; + + }); + + } + catch(e){ + + print(e); + + setState(() => + isTripsLoading = false); + + } + + } + + String? fitToOption(String? incoming, List options) { + if (incoming == null) return null; + final inc = incoming.trim(); + if (inc.isEmpty) return null; + + final match = options.firstWhere( + (o) => o.toLowerCase() == inc.toLowerCase(), + orElse: () => '', + ); + return match.isEmpty ? null : match; // return the exact option string + } + + Future _refreshDriverDetails() async { + try { + setState(() => isLoading = true); + + final updatedDetails = await AppSettings.getDriverDetailsByPhone( + _mobileCtrl.text.trim(), + ); + + if (updatedDetails != null) { + setState(() { + widget.driverDetails = updatedDetails; + }); + } else { + AppSettings.longFailedToast("Failed to fetch updated driver details"); + } + } catch (e) { + debugPrint("⚠️ Error refreshing driver details: $e"); + AppSettings.longFailedToast("Error refreshing driver details"); + } finally { + setState(() => isLoading = false); + } + } + + Future _updateDriver() async { + // run all validators, including the dropdowns + final ok = _formKey.currentState?.validate() ?? false; + if (!ok) { + setState(() {}); // ensure error texts render + return; + } + + // Build payload (adjust keys to your API if needed) + final payload = { + "name": _nameCtrl.text.trim(), + "license_number": _licenseController.text ?? "", + "address": _locationCtrl.text.trim().isEmpty + ? AppSettings.userAddress + : _locationCtrl.text.trim(), + "supplier_name": AppSettings.userName, + "phone": _mobileCtrl.text.trim(), + "alternativeContactNumber": _altMobileCtrl.text.trim(), + "years_of_experience": _experienceController.text ?? "", + "status": _status ?? "available", + }; + + try { + final bool created = await AppSettings.updateDrivers(payload,_mobileCtrl.text); + if (!mounted) return; + + if (created) { + AppSettings.longSuccessToast("Driver Updated successfully"); + Navigator.pop(context, true); // close sheet + _refreshDriverDetails(); + } else { + Navigator.pop(context, true); + AppSettings.longFailedToast("failed to update driver details"); + } + } catch (e) { + debugPrint("⚠️ addDrivers error: $e"); + if (!mounted) return; + AppSettings.longFailedToast("Something went wrong"); + } + } + + Future _openDriverFormSheet(BuildContext context) async { + + _nameCtrl.text = widget.driverDetails.driver_name ?? ''; + _mobileCtrl.text = widget.driverDetails.phone_number ?? ''; + _altMobileCtrl.text = widget.driverDetails.alt_phone_number ?? ''; + _locationCtrl.text = widget.driverDetails.address ?? ''; + _experienceController.text = widget.driverDetails.years_of_experience ?? ''; + _licenseController.text = widget.driverDetails.license_number ?? ''; + _status = fitToOption(widget.driverDetails.status, _statusOptions); + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, // style inner container + builder: (context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return FractionallySizedBox( + heightFactor: 0.75, // fixed height (75% of screen) + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + bottomInset), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Handle + close (optional, simple close icon) + Row( + children: [ + Expanded( + child: Center( + child: Container( + width: 86, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: const Color(0xFFE0E0E0), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + ], + ), + + _LabeledField( + label: "Driver Name *", + child: TextFormField( + controller: _nameCtrl, + validator: (v) => _required(v, field: "Driver Name"), + textCapitalization: TextCapitalization.none, + inputFormatters: const [ + FirstCharUppercaseFormatter(), // << live first-letter caps + ], + decoration: InputDecoration( + hintText: "Full Name", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: "Driver License Number *", + child: TextFormField( + controller: _licenseController, + textCapitalization: TextCapitalization.characters, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: "Enter Driving License Number", + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), + + style: fontTextStyle( + 14, const Color(0xFF2A2A2A), FontWeight.w400), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Driver License required"; + } + return null; + }, + ), + ), + + _LabeledField( + label: "Years of Experience *", + child: TextFormField( + controller: _experienceController, + keyboardType: TextInputType.number, + autovalidateMode: AutovalidateMode.onUserInteraction, + + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, // Only numbers + LengthLimitingTextInputFormatter(2), // Max 2 digits + ], + + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: "Enter Years", + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), + + style: fontTextStyle( + 14, const Color(0xFF2A2A2A), FontWeight.w400), + + validator: (value) { + + if (value == null || value.trim().isEmpty) { + return "Experience is required"; + } + + if (value.length > 2) { + return "Only 2 digits allowed"; + } + + return null; + }, + ), + ), + + _LabeledField( + label: "Phone Number *", + child: TextFormField( + controller: _mobileCtrl, + validator: (v) => + _validatePhone(v, label: "Phone Number"), + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: InputDecoration( + hintText: "Mobile Number", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: "Alternate Phone Number", + child: TextFormField( + controller: _altMobileCtrl, + validator: (v) { + if (v == null || v.trim().isEmpty) + return null; // optional + return _validatePhone(v, + label: "Alternate Phone Number"); + }, + keyboardType: TextInputType.phone, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: InputDecoration( + hintText: "Mobile Number", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: "Location *", + child: TextFormField( + controller: _locationCtrl, + validator: (v) => _required(v, field: "Location"), + textCapitalization: TextCapitalization.none, + inputFormatters: const [ + FirstCharUppercaseFormatter(), // << live first-letter caps + ], + decoration: InputDecoration( + hintText: "Area / locality", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.done, + ), + ), + + _LabeledField( + label: "Status *", + child: DropdownButtonFormField( + value: _status, + items: _statusOptions + .map((s) => + DropdownMenuItem(value: s, child: Text(s))) + .toList(), + onChanged: (v) => setState(() => _status = v), + validator: (v) => v == null || v.isEmpty + ? "Status is required" + : null, + isExpanded: true, + alignment: Alignment.centerLeft, + hint: Text( + "Select status", + style: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + ), + icon: const Icon(Icons.keyboard_arrow_down_rounded), + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: false, + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + ), + ), + ), + + const SizedBox(height: 20), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF8270DB), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + onPressed: (){ + _updateDriver(); + }, + child: Text( + "Update", + style: fontTextStyle( + 14, Colors.white, FontWeight.w600), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + + showDeleteDriverDialog(BuildContext context) 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('Delete Driver?' ,style: fontTextStyle(16,Color(0XFF3B3B3B),FontWeight.w600),), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text('Do u want to delete "${widget.driverDetails.driver_name}"',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{ + + bool status = await AppSettings.deleteDriver(widget.driverDetails.phone_number,); + if(status){ + AppSettings.longSuccessToast('Driver deleted successfully'); + Navigator.of(context).pop(true); + Navigator.of(context).pop(true); + } + else{ + Navigator.of(context).pop(true); + AppSettings.longFailedToast('Failed to delete driver'); + } + + + }, + 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( + 'Delete', + style: fontTextStyle( + 12, + Color(0XFFFFFFFF), + FontWeight.w600)), + ), + ), + ) + ),) + + + ], + ), + ), + ], + ); + }); + }, + ); + } + + @override + Widget build(BuildContext context) { + + final Map statusColors = { + 'available' : Color(0xFF0A9E04), + 'on delivery' : Color(0xFFD0AE3C), + 'offline' : Color(0xFF939495), + }; + + final statusColor = + statusColors[widget.driverDetails.status.toLowerCase().trim()] ?? Colors.grey; + + return WillPopScope( + onWillPop: () async { + Navigator.pop(context, true); + return false; // prevent default pop since we manually handled it + }, + child: + Scaffold( + backgroundColor: Color(0XFFFFFFFF), + + appBar: AppSettings.supplierAppBarWithActionsText( + + widget.driverDetails.driver_name.isNotEmpty + ? widget.driverDetails.driver_name[0].toUpperCase() + + widget.driverDetails.driver_name.substring(1) + : '', + + context, + + result: true // ⭐ ADD THIS + + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + SizedBox(height:MediaQuery.of(context).size.height * .1,), + // 👤 Driver Profile Card + Stack( + clipBehavior: Clip.none, + children: [ + // Main card (give extra top padding so text starts below the avatar) + Container( + padding: EdgeInsets.fromLTRB( + _cardHPad, + _cardTPad + _avSize - _avatarOverlap, // space for avatar + _cardHPad, + 16, + ), + decoration: const BoxDecoration( + color: Color(0xFFF3F1FB), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // status chip (just under avatar, aligned left) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: statusColor), + ), + child: Text(widget.driverDetails.status, + style: fontTextStyle( + 10, statusColor, FontWeight.w400)), + ), + const SizedBox(height: 6), + + Text( + widget.driverDetails.driver_name, + style: fontTextStyle( + 20, Color(0XFF2D2E30), FontWeight.w500), + ), + const SizedBox(height: 2), + + Text( + "+91 "+widget.driverDetails.phone_number, + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + ], + ),), + PopupMenuButton( + // 🔁 Use `child:` so you can place any widget (your 3-dots image) + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: Image.asset( + 'images/popup_menu.png', // your 3-dots image + width: 22, + height: 22, + // If you want to tint it like an icon: + color: Color(0XFF939495), // remove if you want original colors + colorBlendMode: BlendMode.srcIn, + ), + ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + offset: const Offset(0, 40), + color: Colors.white, + elevation: 4, + onSelected: (value) { + if (value == 'edit') { + _openDriverFormSheet(context); + } else if (value == 'disable') { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Disable selected')), + ); + } else if (value == 'delete') { + showDeleteDriverDialog(context); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Image.asset( + 'images/edit.png', + width: 20, + height: 20, + color: Color(0XFF646566), // tint (optional) + colorBlendMode: BlendMode.srcIn, + ), + const SizedBox(width: 12), + Text( + 'Edit', + style: fontTextStyle(14, const Color(0XFF646566), FontWeight.w400), + ), + ], + ), + ), + PopupMenuItem( + value: 'disable', + child: Row( + children: [ + Image.asset( + 'images/disable.png', + width: 20, + height: 20, + color: Color(0XFF646566), // tint (optional) + colorBlendMode: BlendMode.srcIn, + ), + const SizedBox(width: 12), + Text( + 'Disable', + style: fontTextStyle(14, const Color(0XFF646566), FontWeight.w400), + ), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Image.asset( + 'images/delete.png', + width: 20, + height: 20, + color: Color(0XFFE2483D), // red like your example + colorBlendMode: BlendMode.srcIn, + ), + const SizedBox(width: 12), + Text( + 'Delete', + style: fontTextStyle(14, const Color(0xFFE2483D), FontWeight.w400), + ), + ], + ), + ), + ], + ) + ], + ), + + const SizedBox(height: 8), + + // buttons row + /* OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Color(0XFF515253), + backgroundColor: Color(0xFFF3F1FB), + side: const BorderSide( + color: Color(0xFF939495), + width: 0.5, + ), + padding: EdgeInsets.symmetric(vertical: 10), + // uniform height + ), + onPressed: () { + + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Change Status", + style: fontTextStyle( + 14, const Color(0XFF515253), FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 24),*/ + + // 🪪 License Card + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + ), + child: Stack( + children: [ + // background pattern + /*Image.asset( + 'images/license_bg.png', + fit: BoxFit.cover, + width: double.infinity, + height: 140, + ),*/ + Container( + height: 140, + width: double.infinity, + padding: const EdgeInsets.all(16), + color: Colors.black.withOpacity(0.3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "DRIVING LICENSE", + style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w400), + ), + Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.driverDetails.driver_name, + style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500), + ), + Text( + widget.driverDetails.license_number, + style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500), + ), + ], + ), + /*Align( + alignment: Alignment.bottomRight, + child: Text( + "Expires on 29/02/2028", + style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w300), + ), + ),*/ + ], + ) + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + + // Floating avatar (top-left, overlapping the card) + Positioned( + top: -_avatarOverlap, + left: _cardHPad, + child: ClipRRect( + borderRadius: BorderRadius.circular(_avSize / 2), + child: Image.asset( + 'images/avatar.png', + width: _avSize, + height: _avSize, + fit: BoxFit.cover, + ), + ), + ), + + // Call icon aligned with the card’s top-right + /*Positioned( + top: _cardTPad - _avatarOverlap + 4, // aligns near chip row + right: _cardHPad - 4, + child: IconButton( + onPressed: () { + // 📞 Call action + }, + icon: const Icon(Icons.call, color: Color(0xFF4F46E5)), + ), + ),*/ + ], + ), + + const SizedBox(height:24), + + Padding( + padding: const EdgeInsets.all(16), + + child: Column( + + crossAxisAlignment: + CrossAxisAlignment.start, + + children: [ + + Text( + "RECENT TRIPS", + style: fontTextStyle( + 10, + const Color(0xFF343637), + FontWeight.w600), + ), + + const SizedBox(height:12), + + isTripsLoading + + ? const Center( + child: + CircularProgressIndicator()) + + : driverTripsList.isEmpty + + ? Center( + + child: Padding( + padding: + const EdgeInsets.symmetric( + vertical:12), + + child: Text( + "No trips found", + style: fontTextStyle( + 12, + const Color(0xFF939495), + FontWeight.w500), + ), + + ), + + ) + + : ListView.separated( + + shrinkWrap:true, + + physics: + const NeverScrollableScrollPhysics(), + + itemCount: + driverTripsList.length, + + separatorBuilder: + (_,__) => + const SizedBox(height:10), + + itemBuilder:(context,index){ + + final trip = + driverTripsList[index]; + + return _buildTripCard(trip); + + }, + + ) + + ], + + ), + + ), + + ], + ), + ), + ), + )); + } + + Widget _buildTripCard( + DriverTripsModel trip){ + + Color statusColor = + trip.status == "delivered" + ? Colors.green + : trip.status == "cancelled" + ? Colors.red + : Colors.orange; + + return Container( + + padding: + const EdgeInsets.all(12), + + decoration: BoxDecoration( + + color: + const Color(0xFFFFFFFF), + + borderRadius: + BorderRadius.circular(8), + + border: Border.all( + color: + const Color(0XFFC3C4C4) + ), + + ), + + child: Column( + + crossAxisAlignment: + CrossAxisAlignment.start, + + children: [ + + Row( + + children: [ + + Image.asset( + 'images/recent_trips.png', + width:28, + height:28), + + const SizedBox(width:10), + + Expanded( + + child: Column( + + crossAxisAlignment: + CrossAxisAlignment.start, + + children: [ + + Text( + + trip.tankerName, + + style: fontTextStyle( + 14, + const Color(0xFF2D2E30), + FontWeight.w500), + + ), + + Text( + + trip.customerName, + + style: fontTextStyle( + 11, + const Color(0xFF646566), + FontWeight.w400), + + ), + + ], + + ), + + ), + + Container( + + padding: + const EdgeInsets.symmetric( + horizontal:6, + vertical:2), + + decoration: BoxDecoration( + + borderRadius: + BorderRadius.circular(4), + + border: Border.all( + color: statusColor + ) + + ), + + child: Text( + + trip.status, + + style: fontTextStyle( + 10, + statusColor, + FontWeight.w400), + + ), + + ) + + ], + + ), + + const SizedBox(height:6), + + Text( + + "${trip.date} • ${trip.time}", + + style: fontTextStyle( + 11, + const Color(0xFF939495), + FontWeight.w400), + + ), + + const SizedBox(height:4), + + Text( + + trip.address, + + maxLines:2, + + overflow: + TextOverflow.ellipsis, + + style: fontTextStyle( + 11, + const Color(0xFF646566), + FontWeight.w400), + + ), + + const SizedBox(height:8), + + Row( + + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + + children: [ + + Text( + + "₹${trip.price}", + + style: fontTextStyle( + 13, + const Color(0xFF2D2E30), + FontWeight.w600), + + ), + + Text( + + "Paid ₹${trip.amountPaid}", + + style: fontTextStyle( + 11, + Colors.green, + FontWeight.w500), + + ), + + ], + + ) + + ], + + ), + + ); + + } +} + +class _LabeledField extends StatelessWidget { + final String label; + final Widget child; + const _LabeledField({required this.label, required this.child}); + String _capFirstWord(String input) { + if (input.isEmpty) return input; + final i = input.indexOf(RegExp(r'\S')); + if (i == -1) return input; + return input.replaceRange(i, i + 1, input[i].toUpperCase()); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _capFirstWord(label), + style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w600), + ), + const SizedBox(height: 6), + child, + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/common/settings.dart b/lib/common/settings.dart index e448e74..e89cc68 100644 --- a/lib/common/settings.dart +++ b/lib/common/settings.dart @@ -661,6 +661,101 @@ class AppSettings{ } } + static Future addTankersWithResponse(payload) async { + + var uri = Uri.parse(addTankerUrl + '/' + supplierId); + + var response = await http.post( + + uri, + + body: json.encode(payload), + + headers: await buildRequestHeaders() + + ); + + if (response.statusCode == 200) { + + try { + + var _response = json.decode(response.body); + + print(_response); + + /// RETURN FULL DATA + return _response; + + } + catch (e) { + + return null; + + } + + } + + else if (response.statusCode == 2083) { + + AppSettings.longFailedToast( + 'Tanker name already exists' + ); + + return null; + + } + + else if (response.statusCode == 401) { + + bool status = + await AppSettings.resetToken(); + + if (status) { + + response = await http.post( + + uri, + + body: json.encode(payload), + + headers: await buildRequestHeaders() + + ); + + if (response.statusCode == 200) { + + var _response = + json.decode(response.body); + + return _response["data"]; + + } + + else { + + return null; + + } + + } + + else { + + return null; + + } + + } + + else { + + return null; + + } + + } + + static Future updateTanker(payload,tankerName) async { var uri = Uri.parse(updateTankerUrl+'/'+supplierId); uri = uri.replace(query: 'tankerName=$tankerName'); @@ -1670,6 +1765,54 @@ class AppSettings{ return response.body; } + + static Future deleteTankerImage( + + String tankerId, + String imageUrl + + ) async { + + try{ + + var response = await http.delete( + + Uri.parse( + + AppSettings.host+ + "delete_tanker_image/$tankerId" + + ), + + headers:{ + + 'Content-Type':'application/json' + + }, + + body: jsonEncode({ + + "imageUrl": imageUrl + + }), + + ); + + print(response.body); + + return response.statusCode==200; + + } + catch(e){ + + print(e); + + return false; + + } + + } + /*Apis ends here*/ //save data local diff --git a/lib/core/auth_manager.dart b/lib/core/auth_manager.dart new file mode 100644 index 0000000..f86ce7f --- /dev/null +++ b/lib/core/auth_manager.dart @@ -0,0 +1,37 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:supplier_new/common/settings.dart'; + +class AuthManager { + + static final FlutterSecureStorage storage = FlutterSecureStorage( + aOptions: AndroidOptions( + resetOnError: true, + encryptedSharedPreferences: true, + ), + ); + + static Future decideStartScreen() async { + + String? token = await storage.read(key: 'authToken'); + + if(token == null){ + return "login"; + } + + await AppSettings.loadDataFromMemory(); + + String? onboarding = await storage.read(key: 'onboardingCompleted'); + + if(onboarding == null){ + await storage.write( + key: 'onboardingCompleted', + value: 'true', + ); + + return "resources"; + } + + return "dashboard"; + } + +} \ No newline at end of file diff --git a/lib/resources/driver_details.dart b/lib/resources/driver_details.dart index f143996..0347c05 100644 --- a/lib/resources/driver_details.dart +++ b/lib/resources/driver_details.dart @@ -1,10 +1,13 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:supplier_new/resources/driver_trips_model.dart'; import 'package:supplier_new/resources/resources_drivers.dart'; import '../common/settings.dart'; +import 'package:http/http.dart' as http; class DriverDetailsPage extends StatefulWidget { var driverDetails; @@ -16,15 +19,15 @@ class DriverDetailsPage extends StatefulWidget { } class _DriverDetailsPageState extends State { - // tweak if you want a different size/offset - double _avSize = 80; // avatar diameter - double _cardHPad = 16; // horizontal padding - double _cardTPad = 16; // top inner padding + double _avSize = 80; // avatar diameter + double _cardHPad = 16; // horizontal padding + double _cardTPad = 16; // top inner padding double _avatarOverlap = 28; // how much avatar rises above the card final _nameCtrl = TextEditingController(); final _mobileCtrl = TextEditingController(); + final _ageCtrl = TextEditingController(); final _altMobileCtrl = TextEditingController(); final _locationCtrl = TextEditingController(); final _licenseController = TextEditingController(); @@ -33,7 +36,7 @@ class _DriverDetailsPageState extends State { // Unused in UI but kept if you later need them final _commissionCtrl = TextEditingController(); final _joinDateCtrl = TextEditingController(); - bool isLoading=false; + bool isLoading = false; List driverTripsList = []; bool isTripsLoading = false; @@ -65,50 +68,49 @@ class _DriverDetailsPageState extends State { return null; } + final ImagePicker _picker = ImagePicker(); - @override - void initState(){ + XFile? newProfileImage; + List newLicenseImages = []; + List tankerImageUrls = []; + final int maxImages = 2; + + @override + void initState() { super.initState(); + if(widget.driverDetails.licenseImages != null){ - _fetchDriverTrips(); + tankerImageUrls = + List.from( + widget.driverDetails.licenseImages ?? [] + ); + } + _fetchDriverTrips(); } Future _fetchDriverTrips() async { - setState(() => isTripsLoading = true); - try{ - + try { String response = - await AppSettings.getDriverTrips( - widget.driverDetails.phone_number - ); + await AppSettings.getDriverTrips(widget.driverDetails.phone_number); - final data = - (jsonDecode(response)['data'] as List) - .map((e)=>DriverTripsModel.fromJson(e)) + final data = (jsonDecode(response)['data'] as List) + .map((e) => DriverTripsModel.fromJson(e)) .toList(); setState(() { - driverTripsList = data; isTripsLoading = false; - }); - - } - catch(e){ - + } catch (e) { print(e); - setState(() => - isTripsLoading = false); - + setState(() => isTripsLoading = false); } - } String? fitToOption(String? incoming, List options) { @@ -117,33 +119,76 @@ class _DriverDetailsPageState extends State { if (inc.isEmpty) return null; final match = options.firstWhere( - (o) => o.toLowerCase() == inc.toLowerCase(), + (o) => o.toLowerCase() == inc.toLowerCase(), orElse: () => '', ); return match.isEmpty ? null : match; // return the exact option string } - Future _refreshDriverDetails() async { - try { + Future _refreshDriverDetails({bool showToast = true}) async { + + try{ + + if(!mounted) return; + setState(() => isLoading = true); - final updatedDetails = await AppSettings.getDriverDetailsByPhone( - _mobileCtrl.text.trim(), + final updatedDetails = + await AppSettings.getDriverDetailsByPhone( + + widget.driverDetails.phone_number /// ⭐ FIXED + ); - if (updatedDetails != null) { - setState(() { + if(!mounted) return; + + if(updatedDetails != null){ + + setState((){ + widget.driverDetails = updatedDetails; + + /// ⭐ FIX IMAGE REFRESH BUG + tankerImageUrls = + List.from( + updatedDetails.licenseImages ?? [] + ); + }); - } else { - AppSettings.longFailedToast("Failed to fetch updated driver details"); + } - } catch (e) { - debugPrint("⚠️ Error refreshing driver details: $e"); - AppSettings.longFailedToast("Error refreshing driver details"); - } finally { - setState(() => isLoading = false); + else{ + + if(showToast){ + + AppSettings.longFailedToast( + "Failed to fetch updated driver details"); + + } + + } + + } + catch(e){ + + if(showToast){ + + AppSettings.longFailedToast( + "Error refreshing driver details"); + + } + + } + finally{ + + if(mounted){ + + setState(() => isLoading = false); + + } + } + } Future _updateDriver() async { @@ -163,17 +208,23 @@ class _DriverDetailsPageState extends State { : _locationCtrl.text.trim(), "supplier_name": AppSettings.userName, "phone": _mobileCtrl.text.trim(), + "age": _ageCtrl.text.trim(), "alternativeContactNumber": _altMobileCtrl.text.trim(), "years_of_experience": _experienceController.text ?? "", "status": _status ?? "available", }; try { - final bool created = await AppSettings.updateDrivers(payload,_mobileCtrl.text); + final bool created = + await AppSettings.updateDrivers(payload, _mobileCtrl.text); if (!mounted) return; if (created) { AppSettings.longSuccessToast("Driver Updated successfully"); + + await uploadProfileImage(); + + await uploadUpdatedImages(); Navigator.pop(context, true); // close sheet _refreshDriverDetails(); } else { @@ -188,9 +239,9 @@ class _DriverDetailsPageState extends State { } Future _openDriverFormSheet(BuildContext context) async { - _nameCtrl.text = widget.driverDetails.driver_name ?? ''; _mobileCtrl.text = widget.driverDetails.phone_number ?? ''; + _ageCtrl.text = widget.driverDetails.age ?? ''; _altMobileCtrl.text = widget.driverDetails.alt_phone_number ?? ''; _locationCtrl.text = widget.driverDetails.address ?? ''; _experienceController.text = widget.driverDetails.years_of_experience ?? ''; @@ -266,10 +317,9 @@ class _DriverDetailsPageState extends State { decoration: const InputDecoration( border: OutlineInputBorder(), hintText: "Enter Driving License Number", - contentPadding: - EdgeInsets.symmetric(horizontal: 12, vertical: 14), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), ), - style: fontTextStyle( 14, const Color(0xFF2A2A2A), FontWeight.w400), autovalidateMode: AutovalidateMode.onUserInteraction, @@ -282,30 +332,48 @@ class _DriverDetailsPageState extends State { ), ), + _LabeledField( + label: "Age *", + child: TextFormField( + controller: _ageCtrl, + validator: (v) => + _validatePhone(v, label: "Age"), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: InputDecoration( + hintText: "Age Of driver", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), + ), + _LabeledField( label: "Years of Experience *", child: TextFormField( controller: _experienceController, keyboardType: TextInputType.number, autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, // Only numbers - LengthLimitingTextInputFormatter(2), // Max 2 digits + FilteringTextInputFormatter + .digitsOnly, // Only numbers + LengthLimitingTextInputFormatter(2), // Max 2 digits ], - decoration: const InputDecoration( border: OutlineInputBorder(), hintText: "Enter Years", - contentPadding: - EdgeInsets.symmetric(horizontal: 12, vertical: 14), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), ), - style: fontTextStyle( 14, const Color(0xFF2A2A2A), FontWeight.w400), - validator: (value) { - if (value == null || value.trim().isEmpty) { return "Experience is required"; } @@ -393,7 +461,7 @@ class _DriverDetailsPageState extends State { value: _status, items: _statusOptions .map((s) => - DropdownMenuItem(value: s, child: Text(s))) + DropdownMenuItem(value: s, child: Text(s))) .toList(), onChanged: (v) => setState(() => _status = v), validator: (v) => v == null || v.isEmpty @@ -429,7 +497,7 @@ class _DriverDetailsPageState extends State { borderRadius: BorderRadius.circular(24), ), ), - onPressed: (){ + onPressed: () { _updateDriver(); }, child: Text( @@ -451,46 +519,96 @@ class _DriverDetailsPageState extends State { } showDeleteDriverDialog(BuildContext context) 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('Delete Driver?' ,style: fontTextStyle(16,Color(0XFF3B3B3B),FontWeight.w600),), - ), - content: SingleChildScrollView( - child: ListBody( - children: [ - Container( - child: Text('Do u want to delete "${widget.driverDetails.driver_name}"',style: fontTextStyle(14,Color(0XFF101214),FontWeight.w600),), - ), - ], + return AlertDialog( + backgroundColor: + Color(0XFFFFFFFF), // Set your desired background color + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), // Optional: Rounded corners + ), + title: Center( + child: Text( + 'Delete Driver?', + style: fontTextStyle(16, Color(0XFF3B3B3B), FontWeight.w600), + ), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text( + 'Do u want to delete "${widget.driverDetails.driver_name}"', + style: + fontTextStyle(14, Color(0XFF101214), FontWeight.w600), + ), ), - ), - actions: [ - Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded(child: GestureDetector( - onTap: (){ - Navigator.pop(context); + ], + ), + ), + 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 { + bool status = await AppSettings.deleteDriver( + widget.driverDetails.phone_number, + ); + if (status) { + AppSettings.longSuccessToast( + 'Driver deleted successfully'); + Navigator.of(context).pop(true); + Navigator.of(context).pop(true); + } else { + Navigator.of(context).pop(true); + AppSettings.longFailedToast( + 'Failed to delete driver'); + } }, child: Container( decoration: BoxDecoration( shape: BoxShape.rectangle, - color: Color(0XFFFFFFFF), + color: Color(0XFFE2483D), border: Border.all( - width: 1, - color: Color(0XFF1D7AFC)), + width: 1, color: Color(0XFFE2483D)), borderRadius: BorderRadius.circular( 12, )), @@ -498,659 +616,1207 @@ class _DriverDetailsPageState extends State { child: Visibility( visible: true, child: Padding( - padding: EdgeInsets.fromLTRB(16,12,16,12), - child: Text('Cancel', style: fontTextStyle(12, Color(0XFF1D7AFC), FontWeight.w600)), + padding: EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Text('Delete', + style: fontTextStyle(12, Color(0XFFFFFFFF), + FontWeight.w600)), ), ), - ), - ),), - SizedBox(width:MediaQuery.of(context).size.width * .016,), - Expanded(child: GestureDetector( - onTap: ()async{ - - bool status = await AppSettings.deleteDriver(widget.driverDetails.phone_number,); - if(status){ - AppSettings.longSuccessToast('Driver deleted successfully'); - Navigator.of(context).pop(true); - Navigator.of(context).pop(true); - } - else{ - Navigator.of(context).pop(true); - AppSettings.longFailedToast('Failed to delete driver'); - } - - - }, - 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( - 'Delete', - style: fontTextStyle( - 12, - Color(0XFFFFFFFF), - FontWeight.w600)), - ), - ), - ) - ),) - - - ], - ), - ), - ], - ); - }); + )), + ) + ], + ), + ), + ], + ); + }); }, ); } - @override - Widget build(BuildContext context) { + Future pickProfileImage() async { + final XFile? image = + await _picker.pickImage(source: ImageSource.gallery, imageQuality: 70); - final Map statusColors = { - 'available' : Color(0xFF0A9E04), - 'on delivery' : Color(0xFFD0AE3C), - 'offline' : Color(0xFF939495), - }; + if (image != null) { + setState(() { + newProfileImage = image; + }); - final statusColor = - statusColors[widget.driverDetails.status.toLowerCase().trim()] ?? Colors.grey; + /// ⭐ UPLOAD IMMEDIATELY + await uploadProfileImage(); - return WillPopScope( - onWillPop: () async { - Navigator.pop(context, true); - return false; // prevent default pop since we manually handled it - }, - child: - Scaffold( - backgroundColor: Color(0XFFFFFFFF), + /// ⭐ REFRESH DATA + if (mounted) { + await _refreshDriverDetails(showToast: false); + } - appBar: AppSettings.supplierAppBarWithActionsText( + AppSettings.longSuccessToast("Profile updated"); + } + } - widget.driverDetails.driver_name.isNotEmpty - ? widget.driverDetails.driver_name[0].toUpperCase() + - widget.driverDetails.driver_name.substring(1) - : '', + Future pickNewImages() async { + int totalImages = tankerImageUrls.length + newLicenseImages.length; - context, + if (totalImages >= maxImages) { + AppSettings.longFailedToast("Maximum 2 images allowed"); - result: true // ⭐ ADD THIS + return; + } - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + showModalBottomSheet( + context: context, + builder: (context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo), + title: Text("Gallery"), + onTap: () async { + Navigator.pop(context); + + await _pickFromGallery(); + }, + ), + ListTile( + leading: Icon(Icons.camera_alt), + title: Text("Camera"), + onTap: () async { + Navigator.pop(context); + + await _pickFromCamera(); + }, + ), + ], + ), + ); + }); + } - SizedBox(height:MediaQuery.of(context).size.height * .1,), - // 👤 Driver Profile Card - Stack( - clipBehavior: Clip.none, - children: [ - // Main card (give extra top padding so text starts below the avatar) - Container( - padding: EdgeInsets.fromLTRB( - _cardHPad, - _cardTPad + _avSize - _avatarOverlap, // space for avatar - _cardHPad, - 16, - ), - decoration: const BoxDecoration( - color: Color(0xFFF3F1FB), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // status chip (just under avatar, aligned left) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - border: Border.all(color: statusColor), - ), - child: Text(widget.driverDetails.status, - style: fontTextStyle( - 10, statusColor, FontWeight.w400)), - ), - const SizedBox(height: 6), + Future _pickFromGallery() async { + final picker = ImagePicker(); - Text( - widget.driverDetails.driver_name, - style: fontTextStyle( - 20, Color(0XFF2D2E30), FontWeight.w500), - ), - const SizedBox(height: 2), + final images = await picker.pickMultiImage(); - Text( - "+91 "+widget.driverDetails.phone_number, - style: fontTextStyle( - 12, Color(0XFF646566), FontWeight.w400), - ), - ], - ),), - PopupMenuButton( - // 🔁 Use `child:` so you can place any widget (your 3-dots image) - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), - child: Image.asset( - 'images/popup_menu.png', // your 3-dots image - width: 22, - height: 22, - // If you want to tint it like an icon: - color: Color(0XFF939495), // remove if you want original colors - colorBlendMode: BlendMode.srcIn, - ), - ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - offset: const Offset(0, 40), - color: Colors.white, - elevation: 4, - onSelected: (value) { - if (value == 'edit') { - _openDriverFormSheet(context); - } else if (value == 'disable') { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Disable selected')), - ); - } else if (value == 'delete') { - showDeleteDriverDialog(context); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'edit', - child: Row( - children: [ - Image.asset( - 'images/edit.png', - width: 20, - height: 20, - color: Color(0XFF646566), // tint (optional) - colorBlendMode: BlendMode.srcIn, - ), - const SizedBox(width: 12), - Text( - 'Edit', - style: fontTextStyle(14, const Color(0XFF646566), FontWeight.w400), - ), - ], - ), - ), - PopupMenuItem( - value: 'disable', - child: Row( - children: [ - Image.asset( - 'images/disable.png', - width: 20, - height: 20, - color: Color(0XFF646566), // tint (optional) - colorBlendMode: BlendMode.srcIn, - ), - const SizedBox(width: 12), - Text( - 'Disable', - style: fontTextStyle(14, const Color(0XFF646566), FontWeight.w400), - ), - ], - ), - ), - PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Image.asset( - 'images/delete.png', - width: 20, - height: 20, - color: Color(0XFFE2483D), // red like your example - colorBlendMode: BlendMode.srcIn, - ), - const SizedBox(width: 12), - Text( - 'Delete', - style: fontTextStyle(14, const Color(0xFFE2483D), FontWeight.w400), - ), - ], - ), - ), - ], - ) - ], - ), + if (images.isEmpty) return; - const SizedBox(height: 8), + int remaining = + maxImages - + (tankerImageUrls.length + newLicenseImages.length); - // buttons row - /* OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: Color(0XFF515253), - backgroundColor: Color(0xFFF3F1FB), - side: const BorderSide( - color: Color(0xFF939495), - width: 0.5, - ), - padding: EdgeInsets.symmetric(vertical: 10), - // uniform height - ), - onPressed: () { + if (images.length > remaining) { + AppSettings.longFailedToast("You can upload only $remaining more images"); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Change Status", - style: fontTextStyle( - 14, const Color(0XFF515253), FontWeight.w500), - ), - ], - ), - ), - const SizedBox(height: 24),*/ + newLicenseImages = images.take(remaining).toList(); + } else { + newLicenseImages = images; + } - // 🪪 License Card - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - width: double.infinity, - decoration: const BoxDecoration( - color: Colors.black, - ), - child: Stack( - children: [ - // background pattern - /*Image.asset( - 'images/license_bg.png', - fit: BoxFit.cover, - width: double.infinity, - height: 140, - ),*/ - Container( - height: 140, - width: double.infinity, - padding: const EdgeInsets.all(16), - color: Colors.black.withOpacity(0.3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "DRIVING LICENSE", - style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w400), - ), - Spacer(), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.driverDetails.driver_name, - style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500), - ), - Text( - widget.driverDetails.license_number, - style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500), - ), - ], - ), - /*Align( - alignment: Alignment.bottomRight, - child: Text( - "Expires on 29/02/2028", - style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w300), - ), - ),*/ - ], - ) - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), + await uploadUpdatedImages(); - // Floating avatar (top-left, overlapping the card) - Positioned( - top: -_avatarOverlap, - left: _cardHPad, - child: ClipRRect( - borderRadius: BorderRadius.circular(_avSize / 2), - child: Image.asset( - 'images/avatar.png', - width: _avSize, - height: _avSize, - fit: BoxFit.cover, - ), - ), - ), + if(mounted){ - // Call icon aligned with the card’s top-right - /*Positioned( - top: _cardTPad - _avatarOverlap + 4, // aligns near chip row - right: _cardHPad - 4, - child: IconButton( - onPressed: () { - // 📞 Call action - }, - icon: const Icon(Icons.call, color: Color(0xFF4F46E5)), - ), - ),*/ - ], - ), + await _refreshDriverDetails( + showToast:false); - const SizedBox(height:24), + } + } - Padding( - padding: const EdgeInsets.all(16), + Future _pickFromCamera() async { + if (tankerImageUrls.length >= maxImages) { + AppSettings.longFailedToast("Maximum 5 images allowed"); - child: Column( + return; + } - crossAxisAlignment: - CrossAxisAlignment.start, + final picker = ImagePicker(); - children: [ + final image = await picker.pickImage( + source: ImageSource.camera, + imageQuality: 70, + maxWidth: 1280, + maxHeight: 1280, + ); - Text( - "RECENT TRIPS", - style: fontTextStyle( - 10, - const Color(0xFF343637), - FontWeight.w600), - ), + if (image == null) return; - const SizedBox(height:12), + newLicenseImages = [image]; - isTripsLoading + await uploadUpdatedImages(); - ? const Center( - child: - CircularProgressIndicator()) + if(mounted){ - : driverTripsList.isEmpty + await _refreshDriverDetails( + showToast:false); - ? Center( + } + } - child: Padding( - padding: - const EdgeInsets.symmetric( - vertical:12), + /*Future pickLicenseImages() async { + final images = await _picker.pickMultiImage(imageQuality: 70); - child: Text( - "No trips found", - style: fontTextStyle( - 12, - const Color(0xFF939495), - FontWeight.w500), - ), + if (images != null && images.isNotEmpty) { + setState(() { + newLicenseImages = images.take(2).toList(); + }); - ), + /// ⭐ UPLOAD IMMEDIATELY + await uploadUpdatedImages(); - ) + /// ⭐ REFRESH DATA + if (mounted) { + await _refreshDriverDetails(showToast: false); + } - : ListView.separated( + AppSettings.longSuccessToast("License updated"); + } + }*/ - shrinkWrap:true, + Future uploadProfileImage() async { + if (newProfileImage == null) return; - physics: - const NeverScrollableScrollPhysics(), + var request = http.MultipartRequest( + 'POST', + Uri.parse( + AppSettings.host + + "uploads_delievry_profile/${widget.driverDetails.phone_number}", + ), + ); - itemCount: - driverTripsList.length, + request.headers.addAll(await AppSettings.buildRequestHeaders()); - separatorBuilder: - (_,__) => - const SizedBox(height:10), + request.files.add( + await http.MultipartFile.fromPath( + 'file', + newProfileImage!.path, + ), + ); - itemBuilder:(context,index){ + await request.send(); + } - final trip = - driverTripsList[index]; + Future uploadUpdatedImages() async { - return _buildTripCard(trip); + try{ - }, + AppSettings.preLoaderDialog(context); - ) + var request = http.MultipartRequest( + 'POST', + Uri.parse( + AppSettings.host + + "uploads_delivery_liosence_images/${widget.driverDetails.phone_number}", + ), + ); - ], + request.headers.addAll( + await AppSettings.buildRequestHeaders() + ); - ), + for(var img in newLicenseImages){ - ), + request.files.add( - ], - ), - ), - ), - )); - } + await http.MultipartFile.fromPath( - Widget _buildTripCard( - DriverTripsModel trip){ + 'files', - Color statusColor = - trip.status == "delivered" - ? Colors.green - : trip.status == "cancelled" - ? Colors.red - : Colors.orange; + img.path - return Container( + ) - padding: - const EdgeInsets.all(12), + ); - decoration: BoxDecoration( + } - color: - const Color(0xFFFFFFFF), + var response = + await request.send(); - borderRadius: - BorderRadius.circular(8), + Navigator.pop(context); - border: Border.all( - color: - const Color(0XFFC3C4C4) - ), + var resp = + await http.Response.fromStream(response); - ), + if(response.statusCode==200){ - child: Column( + AppSettings.longSuccessToast( + "Images Updated" + ); - crossAxisAlignment: - CrossAxisAlignment.start, + newLicenseImages.clear(); - children: [ + /// ⭐ FORCE REFRESH + await _refreshDriverDetails(showToast:false); - Row( + } + else{ - children: [ + print(resp.body); - Image.asset( - 'images/recent_trips.png', - width:28, - height:28), + AppSettings.longFailedToast( + "Upload failed" + ); - const SizedBox(width:10), + } - Expanded( + } + catch(e){ - child: Column( + Navigator.pop(context); - crossAxisAlignment: - CrossAxisAlignment.start, + AppSettings.longFailedToast( + "Upload failed" + ); - children: [ + } - Text( + } - trip.tankerName, + /* Future uploadLicenseImages() async { + if (newLicenseImages.isEmpty) return; - style: fontTextStyle( - 14, - const Color(0xFF2D2E30), - FontWeight.w500), + var request = http.MultipartRequest( + 'POST', + Uri.parse( + AppSettings.host + + "uploads_delivery_liosence_images/${widget.driverDetails.phone_number}", + ), + ); - ), + request.headers.addAll(await AppSettings.buildRequestHeaders()); - Text( + for (var img in newLicenseImages) { + request.files.add( + await http.MultipartFile.fromPath( + 'files', + img.path, + ), + ); + } - trip.customerName, + await request.send(); - style: fontTextStyle( - 11, - const Color(0xFF646566), - FontWeight.w400), + if (mounted) { + await _refreshDriverDetails(showToast: false); + } + }*/ - ), + @override + Widget build(BuildContext context) { + final Map statusColors = { + 'available': Color(0xFF0A9E04), + 'on delivery': Color(0xFFD0AE3C), + 'offline': Color(0xFF939495), + }; - ], + final statusColor = + statusColors[widget.driverDetails.status.toLowerCase().trim()] ?? + Colors.grey; - ), + return WillPopScope( + onWillPop: () async { + Navigator.pop(context, true); + return false; // prevent default pop since we manually handled it + }, + child: Scaffold( + backgroundColor: Color(0XFFFFFFFF), + appBar: AppSettings.supplierAppBarWithActionsText( + widget.driverDetails.driver_name.isNotEmpty + ? widget.driverDetails.driver_name[0].toUpperCase() + + widget.driverDetails.driver_name.substring(1) + : '', + context, + result: true // ⭐ ADD THIS ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * .1, + ), + // 👤 Driver Profile Card + Stack( + clipBehavior: Clip.none, + children: [ + // Main card (give extra top padding so text starts below the avatar) + Container( + padding: EdgeInsets.fromLTRB( + _cardHPad, + _cardTPad + + _avSize - + _avatarOverlap, // space for avatar + _cardHPad, + 16, + ), + decoration: const BoxDecoration( + color: Color(0xFFF3F1FB), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // status chip (just under avatar, aligned left) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(4), + border: + Border.all(color: statusColor), + ), + child: Text(widget.driverDetails.status, + style: fontTextStyle(10, + statusColor, FontWeight.w400)), + ), + const SizedBox(height: 6), + Text( + widget.driverDetails.driver_name, + style: fontTextStyle(20, + Color(0XFF2D2E30), FontWeight.w500), + ), + const SizedBox(height: 2), + Text( + "+91 " + + widget.driverDetails.phone_number, + style: fontTextStyle(12, + Color(0XFF646566), FontWeight.w400), + ), + ], + ), + ), + PopupMenuButton( + // 🔁 Use `child:` so you can place any widget (your 3-dots image) + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 8.0), + child: Image.asset( + 'images/popup_menu.png', // your 3-dots image + width: 22, + height: 22, + // If you want to tint it like an icon: + color: Color( + 0XFF939495), // remove if you want original colors + colorBlendMode: BlendMode.srcIn, + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + offset: const Offset(0, 40), + color: Colors.white, + elevation: 4, + onSelected: (value) { + if (value == 'edit') { + _openDriverFormSheet(context); + } else if (value == 'disable') { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text('Disable selected')), + ); + } else if (value == 'delete') { + showDeleteDriverDialog(context); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Image.asset( + 'images/edit.png', + width: 20, + height: 20, + color: Color( + 0XFF646566), // tint (optional) + colorBlendMode: BlendMode.srcIn, + ), + const SizedBox(width: 12), + Text( + 'Edit', + style: fontTextStyle( + 14, + const Color(0XFF646566), + FontWeight.w400), + ), + ], + ), + ), + PopupMenuItem( + value: 'disable', + child: Row( + children: [ + Image.asset( + 'images/disable.png', + width: 20, + height: 20, + color: Color( + 0XFF646566), // tint (optional) + colorBlendMode: BlendMode.srcIn, + ), + const SizedBox(width: 12), + Text( + 'Disable', + style: fontTextStyle( + 14, + const Color(0XFF646566), + FontWeight.w400), + ), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Image.asset( + 'images/delete.png', + width: 20, + height: 20, + color: Color( + 0XFFE2483D), // red like your example + colorBlendMode: BlendMode.srcIn, + ), + const SizedBox(width: 12), + Text( + 'Delete', + style: fontTextStyle( + 14, + const Color(0xFFE2483D), + FontWeight.w400), + ), + ], + ), + ), + ], + ) + ], + ), - Container( + const SizedBox(height: 8), - padding: - const EdgeInsets.symmetric( - horizontal:6, - vertical:2), + // buttons row + /* OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Color(0XFF515253), + backgroundColor: Color(0xFFF3F1FB), + side: const BorderSide( + color: Color(0xFF939495), + width: 0.5, + ), + padding: EdgeInsets.symmetric(vertical: 10), + // uniform height + ), + onPressed: () { - decoration: BoxDecoration( + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Change Status", + style: fontTextStyle( + 14, const Color(0XFF515253), FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 24),*/ - borderRadius: - BorderRadius.circular(4), + // 🪪 License Card + /*ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GestureDetector( + onTap: pickLicenseImages, - border: Border.all( - color: statusColor - ) + /// tap to update images - ), + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + ), + child: Stack( + children: [ + /// LICENSE IMAGE BACKGROUND + Container( + height: 140, + width: double.infinity, + decoration: BoxDecoration( + image: + + /// PRIORITY 1 → NEW IMAGE + newLicenseImages.isNotEmpty + ? DecorationImage( + image: FileImage( + File(newLicenseImages[0] + .path), + ), + fit: BoxFit.cover, + ) + + /// PRIORITY 2 → API IMAGE + : widget.driverDetails + .licenseImages != + null && + widget + .driverDetails + .licenseImages + .isNotEmpty + ? DecorationImage( + image: NetworkImage( + widget.driverDetails + .licenseImages[0], + ), + fit: BoxFit.cover, + ) + + /// PRIORITY 3 → NO IMAGE + : null, + color: newLicenseImages.isEmpty && + (widget.driverDetails + .licenseImages == + null || + widget + .driverDetails + .licenseImages + .isEmpty) + ? Colors.black.withOpacity(0.3) + : null, + ), + child: Container( + padding: const EdgeInsets.all(16), + + /// DARK OVERLAY + decoration: newLicenseImages + .isNotEmpty || + (widget.driverDetails + .licenseImages != + null && + widget + .driverDetails + .licenseImages + .isNotEmpty) + ? BoxDecoration( + color: Colors.black + .withOpacity(0.45), + ) + : null, + + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Text( + "DRIVING LICENSE", + style: fontTextStyle( + 10, + const Color(0xFFFFFFFF), + FontWeight.w400), + ), + + /// CAMERA ICON + Container( + padding: + EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.white + .withOpacity(.9), + shape: BoxShape.circle, + ), + child: Center( + child: Image.asset( + 'images/edit.png', + width: 14, + height: 14, + fit: BoxFit.contain, + color: primaryColor, + ), + )) + ], + ), + Spacer(), + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment + .start, + children: [ + Text( + widget.driverDetails + .driver_name, + style: fontTextStyle( + 12, + const Color( + 0xFFFFFFFF), + FontWeight.w500), + ), + SizedBox(height: 2), + Text( + widget.driverDetails + .license_number, + style: fontTextStyle( + 12, + const Color( + 0xFFFFFFFF), + FontWeight.w500), + ), + ], + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ), + ),*/ - child: Text( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ - trip.status, + Text( + "Images (${tankerImageUrls.length}/2)", + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w600 + ), + ), - style: fontTextStyle( - 10, - statusColor, - FontWeight.w400), + Text( + "Max 2 allowed", + style: fontTextStyle( + 10, + Color(0XFF939495), + FontWeight.w400 + ), + ), - ), + ], + ), - ) - ], + SizedBox(height:8), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SizedBox( - ), + height:180, - const SizedBox(height:6), + child:tankerImageUrls.isEmpty - Text( + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + ), + child: Stack( + children: [ + // background pattern + /*Image.asset( + 'images/license_bg.png', + fit: BoxFit.cover, + width: double.infinity, + height: 140, + ),*/ + Container( + height: 140, + width: double.infinity, + padding: const EdgeInsets.all(16), + color: Colors.black.withOpacity(0.3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "DRIVING LICENSE", + style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w400), + ), + Spacer(), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.driverDetails.driver_name, + style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500), + ), + Text( + widget.driverDetails.license_number, + style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500), + ), + ], + ), + /*Align( + alignment: Alignment.bottomRight, + child: Text( + "Expires on 29/02/2028", + style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w300), + ), + ),*/ + ], + ) + ], + ), + ), + ], + ), + ), + ) - "${trip.date} • ${trip.time}", + : ListView.builder( - style: fontTextStyle( - 11, - const Color(0xFF939495), - FontWeight.w400), + scrollDirection:Axis.horizontal, - ), + itemCount:tankerImageUrls.length, - const SizedBox(height:4), + itemBuilder:(context,index){ - Text( + return Padding( - trip.address, + padding:EdgeInsets.only(right:8), - maxLines:2, + child:ClipRRect( - overflow: - TextOverflow.ellipsis, + borderRadius: + BorderRadius.circular(12), - style: fontTextStyle( - 11, - const Color(0xFF646566), - FontWeight.w400), + child:Stack( - ), + children:[ - const SizedBox(height:8), + Image.network( - Row( + tankerImageUrls[index], - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + width:250, + height:180, - children: [ + fit:BoxFit.cover, - Text( + errorBuilder:(context,error,stack){ - "₹${trip.price}", + return Container( - style: fontTextStyle( - 13, - const Color(0xFF2D2E30), - FontWeight.w600), + width:250, + height:180, - ), + color:Colors.grey[300], - Text( + child:Icon( + Icons.image_not_supported, + size:40, + color:Colors.grey, + ), - "Paid ₹${trip.amountPaid}", + ); + + }, + + loadingBuilder:(context,child,progress){ + + if(progress == null) + return child; + + return Container( + + width:250, + height:180, + + alignment:Alignment.center, + + child:CircularProgressIndicator(), + + ); + + }, + + ), + + /// DELETE BUTTON + Positioned( + + top:6, + + right:6, + + child:GestureDetector( + + onTap:(){ + + showDialog( + + context:context, + + builder:(context){ + + return AlertDialog( + + title:Text("Delete Image?"), + + actions:[ + + TextButton( + + onPressed:(){ + + Navigator.pop(context); + + }, + + child:Text("Cancel") + + ), + + TextButton( + + onPressed:(){ + + Navigator.pop(context); - style: fontTextStyle( - 11, - Colors.green, - FontWeight.w500), + //deleteImage(index); + + }, + + child:Text("Delete") + + ) + + ], + + ); + + } + + ); + + }, + + child:Container( + + decoration:BoxDecoration( + + color:Colors.white, + + shape:BoxShape.circle + + ), + + padding:EdgeInsets.all(6), + + child:Container( + + decoration: BoxDecoration( + shape: BoxShape.circle, + ), + + padding: EdgeInsets.all(6), + + child: Image.asset( + + 'images/delete.png', + + width: 18, + height: 18, + colorBlendMode: BlendMode.srcIn, + + ), + + ), + + ), + + ), + + ), + + ], + + ), + + ), + + ); + + }, + + ), + + ), + ), + SizedBox(height:8), + ElevatedButton( + + style: ElevatedButton.styleFrom( + + backgroundColor: const Color(0xFF8270DB), + + foregroundColor: Colors.white, + + minimumSize: const Size(double.infinity, 44), + + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + + ), + onPressed: pickNewImages, + + child: Text( + "Update License Images", + style: fontTextStyle(14, Colors.white, FontWeight.w600), + ), + + ) + ], + ), + ), + + // Floating avatar (top-left, overlapping the card) + Positioned( + top: -_avatarOverlap, + left: _cardHPad, + child: GestureDetector( + onTap: pickProfileImage, + child: Stack( + children: [ + ClipRRect( + borderRadius: + BorderRadius.circular(_avSize / 2), + child: newProfileImage != null + ? Image.file( + File(newProfileImage!.path), + width: _avSize, + height: _avSize, + fit: BoxFit.cover, + ) + : widget.driverDetails.picture != null && + widget.driverDetails.picture + .isNotEmpty + ? Image.network( + widget.driverDetails.picture, + width: _avSize, + height: _avSize, + fit: BoxFit.cover, + ) + : Image.asset( + 'images/avatar.png', + width: _avSize, + height: _avSize, + fit: BoxFit.cover, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Center( + child: Image.asset( + 'images/edit.png', + width: 14, + height: 14, + fit: BoxFit.contain, + color: primaryColor, + ), + )), + ) + ], + ), + ), + ), + + // Call icon aligned with the card’s top-right + /*Positioned( + top: _cardTPad - _avatarOverlap + 4, // aligns near chip row + right: _cardHPad - 4, + child: IconButton( + onPressed: () { + // 📞 Call action + }, + icon: const Icon(Icons.call, color: Color(0xFF4F46E5)), + ), + ),*/ + ], + ), + + const SizedBox(height: 24), + + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "RECENT TRIPS", + style: fontTextStyle( + 10, const Color(0xFF343637), FontWeight.w600), + ), + const SizedBox(height: 12), + isTripsLoading + ? const Center(child: CircularProgressIndicator()) + : driverTripsList.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12), + child: Text( + "No trips found", + style: fontTextStyle( + 12, + const Color(0xFF939495), + FontWeight.w500), + ), + ), + ) + : ListView.separated( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemCount: driverTripsList.length, + separatorBuilder: (_, __) => + const SizedBox(height: 10), + itemBuilder: (context, index) { + final trip = driverTripsList[index]; + + return _buildTripCard(trip); + }, + ) + ], + ), + ), + ], ), + ), + ), + )); + } - ], + Widget _buildTripCard(DriverTripsModel trip) { + Color statusColor = trip.status == "delivered" + ? Colors.green + : trip.status == "cancelled" + ? Colors.red + : Colors.orange; + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0XFFC3C4C4)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset('images/recent_trips.png', width: 28, height: 28), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + trip.tankerName, + style: fontTextStyle( + 14, const Color(0xFF2D2E30), FontWeight.w500), + ), + Text( + trip.customerName, + style: fontTextStyle( + 11, const Color(0xFF646566), FontWeight.w400), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all(color: statusColor)), + child: Text( + trip.status, + style: fontTextStyle(10, statusColor, FontWeight.w400), + ), + ) + ], + ), + const SizedBox(height: 6), + Text( + "${trip.date} • ${trip.time}", + style: fontTextStyle(11, const Color(0xFF939495), FontWeight.w400), + ), + const SizedBox(height: 4), + Text( + trip.address, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: fontTextStyle(11, const Color(0xFF646566), FontWeight.w400), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "₹${trip.price}", + style: + fontTextStyle(13, const Color(0xFF2D2E30), FontWeight.w600), + ), + Text( + "Paid ₹${trip.amountPaid}", + style: fontTextStyle(11, Colors.green, FontWeight.w500), + ), + ], ) - ], - ), - ); - } } @@ -1183,4 +1849,3 @@ class _LabeledField extends StatelessWidget { ); } } - diff --git a/lib/resources/drivers_model.dart b/lib/resources/drivers_model.dart index 6c13492..60885a4 100644 --- a/lib/resources/drivers_model.dart +++ b/lib/resources/drivers_model.dart @@ -10,7 +10,10 @@ class DriversModel { String phone_number=''; String alt_phone_number=''; String license_number=''; + String age=''; String license_expiry_date=''; + String picture=''; + List licenseImages= []; DriversModel(); factory DriversModel.fromJson(Map json) { @@ -25,7 +28,10 @@ class DriversModel { rtvm.years_of_experience = json['years_of_experience'] ?? ''; rtvm.license_number = json['license_number'] ?? ''; rtvm.deliveries = json['deliveries'] ?? ''; + rtvm.picture = json['picture'] ?? ''; rtvm.license_expiry_date=json['license_number'] ?? ''; + rtvm.age=json['age'] ?? ''; + rtvm.licenseImages = json['images'] ?? []; return rtvm; } } \ No newline at end of file diff --git a/lib/resources/resources_drivers.dart b/lib/resources/resources_drivers.dart index 60d28f5..8273452 100644 --- a/lib/resources/resources_drivers.dart +++ b/lib/resources/resources_drivers.dart @@ -1,10 +1,13 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:supplier_new/common/settings.dart'; import 'package:supplier_new/resources/driver_details.dart'; import 'package:supplier_new/resources/drivers_model.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:http/http.dart' as http; class FirstCharUppercaseFormatter extends TextInputFormatter { const FirstCharUppercaseFormatter(); @@ -55,6 +58,8 @@ class _ResourcesDriverScreenState extends State { // Text controllers final _nameCtrl = TextEditingController(); final _mobileCtrl = TextEditingController(); + final _ageCtrl = TextEditingController(); + final _altMobileCtrl = TextEditingController(); final _locationCtrl = TextEditingController(); final _licenseController = TextEditingController(); @@ -276,6 +281,13 @@ class _ResourcesDriverScreenState extends State { } + final ImagePicker _picker = ImagePicker(); + + List drivingLicenseImages = []; + + final int maxImages = 2; + XFile? driverImage; + @override void initState() { super.initState(); @@ -286,6 +298,7 @@ class _ResourcesDriverScreenState extends State { void dispose() { _nameCtrl.dispose(); _mobileCtrl.dispose(); + _ageCtrl.dispose(); _altMobileCtrl.dispose(); _locationCtrl.dispose(); _commissionCtrl.dispose(); @@ -297,10 +310,15 @@ class _ResourcesDriverScreenState extends State { _formKey.currentState?.reset(); _nameCtrl.clear(); _mobileCtrl.clear(); + _ageCtrl.clear(); _altMobileCtrl.clear(); _locationCtrl.clear(); _commissionCtrl.clear(); _joinDateCtrl.clear(); + _licenseController.clear(); + _experienceController.clear(); + drivingLicenseImages.clear(); + driverImage= null; selectedLicense = null; selectedExperience = null; _status = null; @@ -348,44 +366,306 @@ class _ResourcesDriverScreenState extends State { // ---------- submit ---------- Future _addDriver() async { - // run all validators, including the dropdowns + final ok = _formKey.currentState?.validate() ?? false; + if (!ok) { - setState(() {}); // ensure error texts render + setState(() {}); return; } - // Build payload (adjust keys to your API if needed) final payload = { + "Name": _nameCtrl.text.trim(), - "license_number": _licenseController.text ?? "", + + "license_number": _licenseController.text, + "address": _locationCtrl.text.trim().isEmpty ? AppSettings.userAddress : _locationCtrl.text.trim(), + "supplier_name": AppSettings.userName, + "phone": _mobileCtrl.text.trim(), + + "age": _ageCtrl.text.trim(), + "alternativeContactNumber": _altMobileCtrl.text.trim(), - "years_of_experience": _experienceController.text ?? "", + + "years_of_experience": _experienceController.text, + "status": _status ?? "available", + }; - try { - final bool created = await AppSettings.addDrivers(payload); - if (!mounted) return; - if (created) { - AppSettings.longSuccessToast("Driver created successfully"); - Navigator.pop(context, true); // close sheet + try{ + + AppSettings.preLoaderDialog(context); + + final bool created = + await AppSettings.addDrivers(payload); + + if(!mounted) return; + + if(created){ + + /// ⭐ UPLOAD DRIVER IMAGE AFTER CREATE + if(driverImage != null){ + + await uploadDriverImage( + _mobileCtrl.text.trim().toString() + ); + + } + + /// Upload license images + if(drivingLicenseImages.isNotEmpty){ + + await uploadLicenseImages( + _mobileCtrl.text.trim().toString() + ); + + } + + + + Navigator.pop(context); + + AppSettings.longSuccessToast( + "Driver created successfully" + ); + + Navigator.pop(context,true); + _resetForm(); - _fetchDrivers(); // refresh - } else { - AppSettings.longFailedToast("Driver creation failed"); + + _fetchDrivers(); + } - } catch (e) { - debugPrint("⚠️ addDrivers error: $e"); - if (!mounted) return; - AppSettings.longFailedToast("Something went wrong"); + else{ + + Navigator.pop(context); + + AppSettings.longFailedToast( + "Driver creation failed" + ); + + } + + } + catch(e){ + + Navigator.pop(context); + + AppSettings.longFailedToast( + "Something went wrong" + ); + + } + + } + + Future uploadDriverImage( + String phone + ) async{ + + try{ + + var request = + http.MultipartRequest( + + 'POST', + + Uri.parse( + + AppSettings.host+ + "uploads_delievry_profile/$phone" + + ) + + ); + + /// headers if required + request.headers.addAll( + await AppSettings.buildRequestHeaders() + ); + + /// file field name usually "file" + request.files.add( + + await http.MultipartFile.fromPath( + + 'file', /// check backend field name + + driverImage!.path + + ) + + ); + + var response = + await request.send(); + + var resp = + await http.Response.fromStream(response); + + if(response.statusCode==200){ + + print("Driver image uploaded"); + + } + else{ + + print(resp.body); + + } + } + catch(e){ + + print(e); + + } + + } + + Future uploadLicenseImages(String phone) async { + + try { + + var request = http.MultipartRequest( + 'POST', + Uri.parse( + AppSettings.host + + "uploads_delivery_liosence_images/$phone" + ), + ); + + request.headers.addAll( + await AppSettings.buildRequestHeaders() + ); + + /// MULTIPLE FILES + for (var image in drivingLicenseImages) { + + request.files.add( + await http.MultipartFile.fromPath( + 'files', /// IMPORTANT → backend expects "files" + image.path, + ), + ); + + } + + var response = await request.send(); + + var resp = await http.Response.fromStream(response); + + if(response.statusCode == 200){ + + print("License images uploaded"); + + }else{ + + print(resp.body); + + } + + } + catch(e){ + + print("License upload error $e"); + + } + + } + + Future pickTankerImages(Function modalSetState) async { + + if(drivingLicenseImages.length>=2){ + + AppSettings.longFailedToast("Maximum 5 images allowed"); + + return; + + } + + final images = + await _picker.pickMultiImage(imageQuality:70); + + if(images!=null){ + + int remaining = 2 - drivingLicenseImages.length; + + drivingLicenseImages.addAll( + images.take(remaining) + ); + + modalSetState((){}); + + } + + } + + Future pickFromCamera(Function modalSetState) async { + + if(drivingLicenseImages.length>=2){ + + AppSettings.longFailedToast("Maximum 5 images allowed"); + + return; + + } + + final image = + await _picker.pickImage( + source:ImageSource.camera, + imageQuality:70, + ); + + if(image!=null){ + + drivingLicenseImages.add(image); + + modalSetState((){}); + + } + + } + + Future pickDriverImage(Function modalSetState) async { + + final XFile? image = await _picker.pickImage( + source: ImageSource.gallery, + imageQuality:70, + ); + + if(image != null){ + + driverImage = image; + + modalSetState((){}); + + } + + } + + Future pickDriverImageCamera(Function modalSetState) async { + + final XFile? image = await _picker.pickImage( + source: ImageSource.camera, + imageQuality:70, + ); + + if(image != null){ + + driverImage = image; + + modalSetState((){}); + + } + } // ---------- bottom sheet ---------- @@ -395,290 +675,633 @@ class _ResourcesDriverScreenState extends State { isScrollControlled: true, backgroundColor: Colors.transparent, // style inner container builder: (context) { - final bottomInset = MediaQuery.of(context).viewInsets.bottom; + return StatefulBuilder( - return FractionallySizedBox( - heightFactor: 0.75, // fixed height (75% of screen) - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - child: Padding( - padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + bottomInset), - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Handle + close (optional, simple close icon) - Row( + builder: (context, modalSetState) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return FractionallySizedBox( + heightFactor: 0.75, // fixed height (75% of screen) + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Padding( + padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + bottomInset), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Expanded( - child: Center( - child: Container( - width: 86, - height: 4, - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: const Color(0xFFE0E0E0), - borderRadius: BorderRadius.circular(2), + // Handle + close (optional, simple close icon) + Row( + children: [ + Expanded( + child: Center( + child: Container( + width: 86, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: const Color(0xFFE0E0E0), + borderRadius: BorderRadius.circular(2), + ), + ), ), ), - ), + ], ), - ], - ), - _LabeledField( - label: "Driver Name *", - child: TextFormField( - controller: _nameCtrl, - validator: (v) => _required(v, field: "Driver Name"), - textCapitalization: TextCapitalization.none, - autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: const [ - FirstCharUppercaseFormatter(), // << live first-letter caps - ], - decoration: InputDecoration( - hintText: "Full Name", - hintStyle: fontTextStyle( - 14, const Color(0xFF939495), FontWeight.w400), - border: const OutlineInputBorder(), - isDense: true, + _LabeledField( + label: "Driver Name *", + child: TextFormField( + controller: _nameCtrl, + validator: (v) => _required(v, field: "Driver Name"), + textCapitalization: TextCapitalization.none, + autovalidateMode: AutovalidateMode.onUserInteraction, + inputFormatters: const [ + FirstCharUppercaseFormatter(), // << live first-letter caps + ], + decoration: InputDecoration( + hintText: "Full Name", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), ), - textInputAction: TextInputAction.next, - ), - ), - _LabeledField( - label: "Driver License Number *", - child: TextFormField( - controller: _licenseController, - textCapitalization: TextCapitalization.characters,// create controller - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: "Enter Driving License Number", - contentPadding: - EdgeInsets.symmetric(horizontal: 12, vertical: 14), + _LabeledField( + label: "Driver License Number *", + child: TextFormField( + controller: _licenseController, + textCapitalization: TextCapitalization.characters,// create controller + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: "Enter Driving License Number", + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), + + style: fontTextStyle( + 14, const Color(0xFF2A2A2A), FontWeight.w400), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Driver License required"; + } + return null; + }, + ), ), - style: fontTextStyle( - 14, const Color(0xFF2A2A2A), FontWeight.w400), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return "Driver License required"; - } - return null; - }, - ), - ), + _LabeledField( + label: "Age *", + child: TextFormField( + controller: _ageCtrl, + keyboardType: TextInputType.number, + textCapitalization: TextCapitalization.characters,// create controller + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: "Enter Age Of Driver", + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), - _LabeledField( - label: "Years of Experience *", - child: TextFormField( - controller: _experienceController, - keyboardType: TextInputType.number, - autovalidateMode: AutovalidateMode.onUserInteraction, - - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, // Only numbers - LengthLimitingTextInputFormatter(2), // Max 2 digits - ], - - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: "Enter Years", - contentPadding: - EdgeInsets.symmetric(horizontal: 12, vertical: 14), + style: fontTextStyle( + 14, const Color(0xFF2A2A2A), FontWeight.w400), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Driver Age required"; + } + return null; + }, + ), ), - style: fontTextStyle( - 14, const Color(0xFF2A2A2A), FontWeight.w400), + _LabeledField( + label: "Years of Experience *", + child: TextFormField( + controller: _experienceController, + keyboardType: TextInputType.number, + autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, // Only numbers + LengthLimitingTextInputFormatter(2), // Max 2 digits + ], - if (value == null || value.trim().isEmpty) { - return "Experience is required"; - } + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: "Enter Years", + contentPadding: + EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), - if (value.length > 2) { - return "Only 2 digits allowed"; - } + style: fontTextStyle( + 14, const Color(0xFF2A2A2A), FontWeight.w400), - return null; - }, - ), - ), + validator: (value) { - _LabeledField( - label: "Phone Number *", - child: TextFormField( - controller: _mobileCtrl, - keyboardType: TextInputType.phone, - autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(10), - - // Restrict first digit 6-9 - TextInputFormatter.withFunction( - (oldValue, newValue) { - - if (newValue.text.isEmpty) { - return newValue; + if (value == null || value.trim().isEmpty) { + return "Experience is required"; } - // First digit must be 6-9 - if (!RegExp(r'^[6-9]').hasMatch(newValue.text)) { - return oldValue; + if (value.length > 2) { + return "Only 2 digits allowed"; } - return newValue; + return null; }, ), - ], + ), - validator: (value) { + _LabeledField( + label: "Phone Number *", + child: TextFormField( + controller: _mobileCtrl, + keyboardType: TextInputType.phone, + autovalidateMode: AutovalidateMode.onUserInteraction, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + + // Restrict first digit 6-9 + TextInputFormatter.withFunction( + (oldValue, newValue) { + + if (newValue.text.isEmpty) { + return newValue; + } + + // First digit must be 6-9 + if (!RegExp(r'^[6-9]').hasMatch(newValue.text)) { + return oldValue; + } + + return newValue; + }, + ), + ], - if (value == null || value.isEmpty) { - return "Phone Number required"; - } + validator: (value) { - if (!RegExp(r'^[6-9]').hasMatch(value)) { - return "Enter digits starting 6,7,8,9"; - } + if (value == null || value.isEmpty) { + return "Phone Number required"; + } - if (value.length != 10) { - return "Enter valid 10 digit number"; - } + if (!RegExp(r'^[6-9]').hasMatch(value)) { + return "Enter digits starting 6,7,8,9"; + } - return null; - }, + if (value.length != 10) { + return "Enter valid 10 digit number"; + } - decoration: InputDecoration( - hintText: "Mobile Number", - hintStyle: fontTextStyle( - 14, const Color(0xFF939495), FontWeight.w400), - border: const OutlineInputBorder(), - isDense: true, - ), + return null; + }, - textInputAction: TextInputAction.next, - ), - ), + decoration: InputDecoration( + hintText: "Mobile Number", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), - _LabeledField( - label: "Alternate Phone Number", - child: TextFormField( - controller: _altMobileCtrl, - validator: (v) { - if (v == null || v.trim().isEmpty) - return null; // optional - return _validatePhone(v, - label: "Alternate Phone Number"); - }, - keyboardType: TextInputType.phone, - autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(10), - ], - decoration: InputDecoration( - hintText: "Mobile Number", - hintStyle: fontTextStyle( - 14, const Color(0xFF939495), FontWeight.w400), - border: const OutlineInputBorder(), - isDense: true, + textInputAction: TextInputAction.next, + ), ), - textInputAction: TextInputAction.next, - ), - ), - _LabeledField( - label: "Location *", - child: TextFormField( - controller: _locationCtrl, - validator: (v) => _required(v, field: "Location"), - textCapitalization: TextCapitalization.none, - autovalidateMode: AutovalidateMode.onUserInteraction, - inputFormatters: const [ - FirstCharUppercaseFormatter(), // << live first-letter caps - ], - decoration: InputDecoration( - hintText: "Area / locality", - hintStyle: fontTextStyle( - 14, const Color(0xFF939495), FontWeight.w400), - border: const OutlineInputBorder(), - isDense: true, + _LabeledField( + label: "Alternate Phone Number", + child: TextFormField( + controller: _altMobileCtrl, + validator: (v) { + if (v == null || v.trim().isEmpty) + return null; // optional + return _validatePhone(v, + label: "Alternate Phone Number"); + }, + keyboardType: TextInputType.phone, + autovalidateMode: AutovalidateMode.onUserInteraction, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ], + decoration: InputDecoration( + hintText: "Mobile Number", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), ), - textInputAction: TextInputAction.done, - ), - ), - _LabeledField( - label: "Status *", - child: DropdownButtonFormField( - value: _status, - dropdownColor: Colors.white, - items: _statusOptions - .map((s) => - DropdownMenuItem(value: s, child: Text(s))) - .toList(), - onChanged: (v) => setState(() => _status = v), - validator: (v) => v == null || v.isEmpty - ? "Status is required" - : null, - isExpanded: true, - alignment: Alignment.centerLeft, - hint: Text( - "Select status", + _LabeledField( + label: "Location *", + child: TextFormField( + controller: _locationCtrl, + validator: (v) => _required(v, field: "Location"), + textCapitalization: TextCapitalization.none, + autovalidateMode: AutovalidateMode.onUserInteraction, + inputFormatters: const [ + FirstCharUppercaseFormatter(), // << live first-letter caps + ], + decoration: InputDecoration( + hintText: "Area / locality", + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.done, + ), + ), - style: fontTextStyle( - 14, const Color(0xFF939495), FontWeight.w400), + _LabeledField( + label: "Status *", + child: DropdownButtonFormField( + value: _status, + dropdownColor: Colors.white, + items: _statusOptions + .map((s) => + DropdownMenuItem(value: s, child: Text(s))) + .toList(), + onChanged: (v) => setState(() => _status = v), + validator: (v) => v == null || v.isEmpty + ? "Status is required" + : null, + isExpanded: true, + alignment: Alignment.centerLeft, + hint: Text( + "Select status", + + style: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + icon: const Icon(Icons.keyboard_arrow_down_rounded), + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: false, + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + ), + ), ), - autovalidateMode: AutovalidateMode.onUserInteraction, - icon: const Icon(Icons.keyboard_arrow_down_rounded), - decoration: const InputDecoration( - border: OutlineInputBorder(), - isDense: false, - contentPadding: EdgeInsets.symmetric( - horizontal: 12, vertical: 14), + _LabeledField( + label: "Driving License (Max 2)", + + child: Column( + + children: [ + + /// IMAGE GRID + if(drivingLicenseImages.isNotEmpty) + + GridView.builder( + + shrinkWrap:true, + + physics:NeverScrollableScrollPhysics(), + + itemCount:drivingLicenseImages.length, + + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + + crossAxisCount:3, + + crossAxisSpacing:8, + + mainAxisSpacing:8, + + ), + + itemBuilder:(context,index){ + + return Stack( + + children:[ + + ClipRRect( + + borderRadius:BorderRadius.circular(8), + + child:Image.file( + + File(drivingLicenseImages[index].path), + + width:double.infinity, + + height:double.infinity, + + fit:BoxFit.cover, + + ), + + ), + + Positioned( + + right:4, + + top:4, + + child:GestureDetector( + + onTap:(){ + + drivingLicenseImages.removeAt(index); + + modalSetState((){}); /// FIXED + + }, + + child:Container( + + decoration:const BoxDecoration( + + color:Colors.red, + + shape:BoxShape.circle, + + ), + + child:const Icon( + + Icons.close, + + color:Colors.white, + + size:18, + + ), + + ), + + ), + + ) + + ], + + ); + + }, + + ), + + const SizedBox(height:10), + + /// BUTTONS + Row( + + children:[ + + Expanded( + + child:OutlinedButton.icon( + + onPressed:(){ + + pickTankerImages(modalSetState); /// FIXED + + }, + + icon:Icon(Icons.photo), + + label:Text("Gallery"), + + ), + + ), + + const SizedBox(width:10), + + Expanded( + + child:OutlinedButton.icon( + + onPressed:(){ + + pickFromCamera(modalSetState); /// FIXED + + }, + + icon:Icon(Icons.camera_alt), + + label:Text("Camera"), + + ), + + ), + + ], + + ), + + const SizedBox(height:5), + + Text( + + "${drivingLicenseImages.length}/2 images selected", + + style: fontTextStyle( + 12, + Colors.grey, + FontWeight.w400 + ), + + ), + + ], + + ), + ), - ), - ), - const SizedBox(height: 20), - - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF8270DB), - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), + _LabeledField( + label: "Driver Image", + + child: Column( + + children: [ + + /// SINGLE IMAGE + if(driverImage != null) + + Stack( + + children:[ + + ClipRRect( + + borderRadius: BorderRadius.circular(8), + + child: Image.file( + + File(driverImage!.path), + + width:120, + + height:120, + + fit:BoxFit.cover, + + ), + + ), + + Positioned( + + right:4, + + top:4, + + child: GestureDetector( + + onTap:(){ + + driverImage = null; + + modalSetState((){}); + + }, + + child: Container( + + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + + child: const Icon( + Icons.close, + color: Colors.white, + size:18, + ), + + ), + + ), + + ) + + ], + + ), + + const SizedBox(height:10), + + /// BUTTONS + Row( + + children:[ + + Expanded( + + child:OutlinedButton.icon( + + onPressed:(){ + + pickDriverImage(modalSetState); + + }, + + icon:Icon(Icons.photo), + + label:Text("Gallery"), + + ), + + ), + + const SizedBox(width:10), + + Expanded( + + child:OutlinedButton.icon( + + onPressed:(){ + + pickDriverImageCamera(modalSetState); + + }, + + icon:Icon(Icons.camera_alt), + + label:Text("Camera"), + + ), + + ), + + ], + + ), + + const SizedBox(height:5), + + Text( + + driverImage==null + ? "0/1 image selected" + : "1/1 image selected", + + style: fontTextStyle( + 12, + Colors.grey, + FontWeight.w400 + ), + + ), + + ], + ), + ), - onPressed: _addDriver, - child: Text( - "Save", - style: fontTextStyle( - 14, Colors.white, FontWeight.w600), + + const SizedBox(height: 20), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF8270DB), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + ), + onPressed: _addDriver, + child: Text( + "Save", + style: fontTextStyle( + 14, Colors.white, FontWeight.w600), + ), + ), ), - ), + ], ), - ], + ), ), ), ), - ), - ), - ); + ); + }); + }, ); } @@ -936,7 +1559,8 @@ class _ResourcesDriverScreenState extends State { location: d.address, deliveries: int.tryParse(d.deliveries) ?? 0, commission: d.commision, - phone: d.phone_number, // ✅ ADD + phone: d.phone_number, + picture: d.picture ?? '',// ✅ ADD ), ); }, @@ -1001,6 +1625,7 @@ class DriverCard extends StatelessWidget { final int deliveries; final String commission; final String phone; + final String picture; const DriverCard({ super.key, @@ -1010,6 +1635,7 @@ class DriverCard extends StatelessWidget { required this.deliveries, required this.commission, required this.phone, + required this.picture, }); @override @@ -1052,8 +1678,41 @@ class DriverCard extends StatelessWidget { children: [ Row( children: [ - Image.asset("images/avatar.png", - height: 36, width: 36), + ClipRRect( + borderRadius: BorderRadius.circular(8), + + child: picture.isNotEmpty + + ? Image.network( + + picture, + + height: 36, + width: 36, + fit: BoxFit.cover, + + errorBuilder: (context,error,stackTrace){ + + return Image.asset( + "images/avatar.png", + height:36, + width:36, + ); + + }, + + ) + + : Image.asset( + + "images/avatar.png", + + height:36, + width:36, + + ), + + ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/resources/resources_fleet.dart b/lib/resources/resources_fleet.dart index 40e9828..072a24e 100644 --- a/lib/resources/resources_fleet.dart +++ b/lib/resources/resources_fleet.dart @@ -1,9 +1,12 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:supplier_new/common/settings.dart'; import 'package:supplier_new/resources/tanker_details.dart'; import 'package:supplier_new/resources/tankers_model.dart'; +import 'package:http/http.dart' as http; class FirstCharUppercaseFormatter extends TextInputFormatter { const FirstCharUppercaseFormatter(); @@ -93,6 +96,67 @@ class _ResourcesFleetScreenState extends State { "Capacity" ]; + final ImagePicker _picker = ImagePicker(); + + List tankerImages = []; + + final int maxImages = 5; + + + Future pickTankerImages(Function modalSetState) async { + + if(tankerImages.length>=5){ + + AppSettings.longFailedToast("Maximum 5 images allowed"); + + return; + + } + + final images = + await _picker.pickMultiImage(imageQuality:70); + + if(images!=null){ + + int remaining = 5 - tankerImages.length; + + tankerImages.addAll( + images.take(remaining) + ); + + modalSetState((){}); + + } + + } + + Future pickFromCamera(Function modalSetState) async { + + if(tankerImages.length>=5){ + + AppSettings.longFailedToast("Maximum 5 images allowed"); + + return; + + } + + final image = + await _picker.pickImage( + source:ImageSource.camera, + imageQuality:70, + ); + + if(image!=null){ + + tankerImages.add(image); + + modalSetState((){}); + + } + + } + + @override void initState() { super.initState(); @@ -392,7 +456,612 @@ class _ResourcesFleetScreenState extends State { } } + Future uploadTankerImages( + String tankerId + ) async { + + var request = http.MultipartRequest( + + 'POST', + + Uri.parse( + AppSettings.host + + "uploads_tanker_images/$tankerId" + ) + + ); + + request.headers.addAll( + await AppSettings.buildRequestHeaders() + ); + + for(var img in tankerImages){ + + request.files.add( + + await http.MultipartFile.fromPath( + + 'files', + + img.path + + ) + + ); + + } + + var response = + await request.send(); + + if(response.statusCode != 200){ + + throw Exception( + "Upload failed" + ); + + } + + } + + Future _addTankerNew() async { + + /// FORM VALIDATION + final ok = _formKey.currentState?.validate() ?? false; + + if (!ok) { + + setState(() {}); + + return; + + } + + try { + + AppSettings.preLoaderDialog(context); + + /// STEP 1 → CREATE TANKER + final payload = { + + "tankerName": _nameCtrl.text.trim(), + + "capacity": _capacityCtrl.text.trim(), + + "typeofwater": selectedTypeOfWater ?? "", + + "supplier_address": AppSettings.userAddress, + + "supplier_name": AppSettings.userName, + + "phoneNumber": AppSettings.phoneNumber, + + "tanker_type": selectedType ?? "", + + "license_plate": _plateCtrl.text.trim(), + + "manufacturing_year": _mfgYearCtrl.text.trim(), + + "insurance_exp_date": _insExpiryCtrl.text.trim(), + + }; + + final response = + await AppSettings.addTankersWithResponse(payload); + + if(response == null){ + + Navigator.pop(context); + + AppSettings.longFailedToast( + "Tanker creation failed" + ); + + return; + + } + + /// IMPORTANT → GET TANKER ID + String tankerId = + response["tankerId"] ?? + response["_id"] ?? + ""; + + /// STEP 2 → UPLOAD IMAGES + if(tankerImages.isNotEmpty){ + + try{ + + await uploadTankerImages(tankerId); + + } + catch(e){ + + debugPrint( + "Image upload failed $e" + ); + + AppSettings.longFailedToast( + "Tanker created but images failed" + ); + + } + + } + + Navigator.pop(context); + + if (!mounted) return; + + AppSettings.longSuccessToast( + "Tanker Created Successfully" + ); + + Navigator.pop(context,true); + + _resetForm(); + + _fetchTankers(); + + } + + catch (e) { + + Navigator.pop(context); + + debugPrint( + "⚠️ addTanker error $e" + ); + + if (!mounted) return; + + AppSettings.longFailedToast( + "Something went wrong" + ); + + } + + } + Future openTankerSimpleSheet(BuildContext context) async { + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + + builder: (context) { + + return StatefulBuilder( + + builder: (context, modalSetState) { + + final viewInsets = MediaQuery.of(context).viewInsets.bottom; + + return FractionallySizedBox( + heightFactor: 0.75, + + child: Container( + + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + + child: Padding( + + padding: EdgeInsets.fromLTRB(20,16,20,20+viewInsets), + + child: Form( + + key: _formKey, + + child: SingleChildScrollView( + + child: Column( + + crossAxisAlignment: CrossAxisAlignment.stretch, + + children: [ + + Row( + children: [ + Expanded( + child: Center( + child: Container( + width: 86, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: const Color(0xFFE0E0E0), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + ], + ), + + _LabeledField( + label: "Tanker Name *", + child: TextFormField( + controller: _nameCtrl, + validator: (v) => _required(v, field: "Tanker Name"), + textCapitalization: TextCapitalization.characters, + inputFormatters: const [ + FirstCharUppercaseFormatter(), // << live first-letter caps + ], + decoration: InputDecoration( + hintText: "Enter Tanker Name", + hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: "Tanker Capacity (in L) *", + child: TextFormField( + controller: _capacityCtrl, + validator: (v) => _required(v, field: "Tanker Capacity"), + decoration: InputDecoration( + hintText: "10,000", + hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9,]')), + ], + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: "Tanker Type *", + child: DropdownButtonFormField( + value: selectedType, + dropdownColor: Colors.white, + items: tankerTypes + .map((t) => DropdownMenuItem(value: t, child: Text(t))) + .toList(), + onChanged: (v) => setState(() => selectedType = v), + validator: (v) => v == null || v.isEmpty ? "Tanker Type is required" : null, + isExpanded: true, + alignment: Alignment.centerLeft, + hint: Text( + "Select Type", + style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + ), + icon: Image.asset('images/downarrow.png', width: 16, height: 16), + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: false, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), + ), + ), + + _LabeledField( + label: "Type of water *", + child: DropdownButtonFormField( + value: selectedTypeOfWater, + dropdownColor: Colors.white, + items: typeOfWater + .map((t) => DropdownMenuItem(value: t, child: Text(t))) + .toList(), + onChanged: (v) => setState(() => selectedTypeOfWater = v), + validator: (v) => v == null || v.isEmpty ? "Type of water is required" : null, + isExpanded: true, + alignment: Alignment.centerLeft, + hint: Text( + "Select type of water", + style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + ), + icon: Image.asset('images/downarrow.png', width: 16, height: 16), + decoration: const InputDecoration( + border: OutlineInputBorder(), + isDense: false, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + ), + ), + ), + + _LabeledField( + label: "License Plate *", + child: TextFormField( + controller: _plateCtrl, + validator: (v) => _required(v, field: "License Plate"), + decoration: InputDecoration( + hintText: "AB 05 H 4948", + hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + textCapitalization: TextCapitalization.characters, + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: " Age of vehicle (opt)", + child: TextFormField( + controller: _mfgYearCtrl, + decoration: InputDecoration( + hintText: "12", + hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + ], + textInputAction: TextInputAction.next, + ), + ), + + _LabeledField( + label: "Insurance Expiry Date (opt)", + child: TextFormField( + controller: _insExpiryCtrl, + readOnly: true, + decoration: InputDecoration( + hintText: "DD-MM-YYYY", + hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + border: const OutlineInputBorder(), + isDense: true, + suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18), + ), + onTap: _pickInsuranceDate, + ), + ), + + _LabeledField( + label: "Tanker Images (Max 5)", + + child: Column( + + children: [ + + /// IMAGE GRID + if(tankerImages.isNotEmpty) + + GridView.builder( + + shrinkWrap:true, + + physics:NeverScrollableScrollPhysics(), + + itemCount:tankerImages.length, + + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + + crossAxisCount:3, + + crossAxisSpacing:8, + + mainAxisSpacing:8, + + ), + + itemBuilder:(context,index){ + + return Stack( + + children:[ + + ClipRRect( + + borderRadius:BorderRadius.circular(8), + + child:Image.file( + + File(tankerImages[index].path), + + width:double.infinity, + + height:double.infinity, + + fit:BoxFit.cover, + + ), + + ), + + Positioned( + + right:4, + + top:4, + + child:GestureDetector( + + onTap:(){ + + tankerImages.removeAt(index); + + modalSetState((){}); /// FIXED + + }, + + child:Container( + + decoration:const BoxDecoration( + + color:Colors.red, + + shape:BoxShape.circle, + + ), + + child:const Icon( + + Icons.close, + + color:Colors.white, + + size:18, + + ), + + ), + + ), + + ) + + ], + + ); + + }, + + ), + + const SizedBox(height:10), + + /// BUTTONS + Row( + + children:[ + + Expanded( + + child:OutlinedButton.icon( + + onPressed:(){ + + pickTankerImages(modalSetState); /// FIXED + + }, + + icon:Icon(Icons.photo), + + label:Text("Gallery"), + + ), + + ), + + const SizedBox(width:10), + + Expanded( + + child:OutlinedButton.icon( + + onPressed:(){ + + pickFromCamera(modalSetState); /// FIXED + + }, + + icon:Icon(Icons.camera_alt), + + label:Text("Camera"), + + ), + + ), + + ], + + ), + + const SizedBox(height:5), + + Text( + + "${tankerImages.length}/5 images selected", + + style: fontTextStyle( + 12, + Colors.grey, + FontWeight.w400 + ), + + ), + + ], + + ), + + ), + + /// SAVE BUTTON (UNCHANGED) + + const SizedBox(height:20), + + SizedBox( + + width:double.infinity, + + child:ElevatedButton( + + style:ElevatedButton.styleFrom( + + backgroundColor:const Color(0xFF8270DB), + + foregroundColor:Colors.white, + + padding:const EdgeInsets.symmetric(vertical:14), + + shape:RoundedRectangleBorder( + + borderRadius:BorderRadius.circular(24), + + ), + + ), + + onPressed:_addTankerNew, + + child:Text( + + "Save", + + style:fontTextStyle( + 14, + Colors.white, + FontWeight.w600 + ), + + ), + + ), + + ), + + ], + + ), + + ), + + ), + + ), + + ), + + ); + + }, + + ); + + }, + + ); + + } + + /*Future openTankerSimpleSheet1(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, @@ -571,6 +1240,120 @@ class _ResourcesFleetScreenState extends State { onTap: _pickInsuranceDate, ), ), + _LabeledField( + label: "Tanker Images (Max 5)", + child: Column( + children: [ + + /// Grid Images + if(tankerImages.isNotEmpty) + GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: tankerImages.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemBuilder: (context,index){ + + return Stack( + children: [ + + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + File(tankerImages[index].path), + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + ), + + Positioned( + right: 4, + top: 4, + child: GestureDetector( + + onTap: (){ + + tankerImages.removeAt(index); + + setState(() {}); + + }, + + child: Container( + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: Icon( + Icons.close, + color: Colors.white, + size: 18, + ), + ), + + ), + ) + + ], + ); + + }, + ), + + const SizedBox(height:10), + + /// Add Buttons + Row( + children: [ + + Expanded( + child: OutlinedButton.icon( + + onPressed: pickTankerImages, + + icon: Icon(Icons.photo), + + label: Text("Gallery"), + + ), + ), + + const SizedBox(width:10), + + Expanded( + child: OutlinedButton.icon( + + onPressed: pickFromCamera, + + icon: Icon(Icons.camera_alt), + + label: Text("Camera"), + + ), + ), + + ], + ), + + const SizedBox(height:5), + + Text( + "${tankerImages.length}/5 images selected", + style: fontTextStyle( + 12, + Colors.grey, + FontWeight.w400 + ), + ), + + ], + ), + ), const SizedBox(height: 20), @@ -601,7 +1384,7 @@ class _ResourcesFleetScreenState extends State { ); }, ); - } + }*/ diff --git a/lib/resources/tanker_details.dart b/lib/resources/tanker_details.dart index 185ac4f..4827dcc 100644 --- a/lib/resources/tanker_details.dart +++ b/lib/resources/tanker_details.dart @@ -2,9 +2,11 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:supplier_new/resources/tanker_trips_model.dart'; import '../common/settings.dart'; +import 'package:http/http.dart' as http; class FirstCharUppercaseFormatter extends TextInputFormatter { const FirstCharUppercaseFormatter(); @@ -76,6 +78,11 @@ class _TankerDetailsPageState extends State { String? currentAvailability; List tankerTripsList = []; + List tankerImageUrls = []; + List newImages = []; + + final int maxImages = 5; + @override void initState() { @@ -84,6 +91,15 @@ class _TankerDetailsPageState extends State { _nameCtrl.text = widget.tankerDetails.tanker_name ?? ''; + if(widget.tankerDetails.images != null){ + + tankerImageUrls = + List.from( + widget.tankerDetails.images + ); + + } + /// FIX — allow null availability if (widget.tankerDetails.availability != null && widget.tankerDetails.availability is List && @@ -481,26 +497,57 @@ class _TankerDetailsPageState extends State { } Future _refreshTankerDetails() async { + try { + setState(() => isLoading = true); - final updatedDetails = await AppSettings.getTankerDetailsByName( + final updatedDetails = + await AppSettings.getTankerDetailsByName( _nameCtrl.text.trim(), ); if (updatedDetails != null) { + setState(() { + widget.tankerDetails = updatedDetails; + + /// ⭐ VERY IMPORTANT FIX + tankerImageUrls = + List.from( + updatedDetails.images ?? [] + ); + }); - } else { - AppSettings.longFailedToast("Failed to fetch updated tanker details"); + } - } catch (e) { - debugPrint("⚠️ Error refreshing tanker details: $e"); - AppSettings.longFailedToast("Error refreshing tanker details"); - } finally { + else{ + + AppSettings.longFailedToast( + "Failed to fetch updated tanker details" + ); + + } + + } + catch(e){ + + debugPrint( + "⚠️ Error refreshing tanker details: $e" + ); + + AppSettings.longFailedToast( + "Error refreshing tanker details" + ); + + } + finally{ + setState(() => isLoading = false); + } + } String? fitToOption(String? incoming, List options) { @@ -830,6 +877,280 @@ class _TankerDetailsPageState extends State { ); } + Future pickNewImages() async { + + int totalImages = + tankerImageUrls.length + newImages.length; + + if(totalImages >= maxImages){ + + AppSettings.longFailedToast( + "Maximum 5 images allowed" + ); + + return; + + } + + showModalBottomSheet( + + context: context, + + builder:(context){ + + return SafeArea( + + child:Column( + + mainAxisSize:MainAxisSize.min, + + children:[ + + ListTile( + + leading:Icon(Icons.photo), + + title:Text("Gallery"), + + onTap:() async { + + Navigator.pop(context); + + await _pickFromGallery(); + + }, + + ), + + ListTile( + + leading:Icon(Icons.camera_alt), + + title:Text("Camera"), + + onTap:() async { + + Navigator.pop(context); + + await _pickFromCamera(); + + }, + + ), + + ], + + ), + + ); + + } + + ); + + } + + Future _pickFromGallery() async { + + final picker = ImagePicker(); + + final images = + await picker.pickMultiImage(); + + if(images.isEmpty) return; + + int remaining = + maxImages - tankerImageUrls.length; + + if(images.length > remaining){ + + AppSettings.longFailedToast( + "You can upload only $remaining more images" + ); + + newImages = + images.take(remaining).toList(); + + } + else{ + + newImages = images; + + } + + await uploadUpdatedImages(); + + } + + Future _pickFromCamera() async { + + if(tankerImageUrls.length >= maxImages){ + + AppSettings.longFailedToast( + "Maximum 5 images allowed" + ); + + return; + + } + + final picker = ImagePicker(); + + final image = + await picker.pickImage( + + source: ImageSource.camera, + + imageQuality:70, + maxWidth:1280, + maxHeight:1280, + + ); + + if(image == null) return; + + newImages = [image]; + + await uploadUpdatedImages(); + + } + + Future deleteImage(int index) async { + + try{ + + AppSettings.preLoaderDialog(context); + + String imageUrl = + tankerImageUrls[index]; + + bool status = + await AppSettings.deleteTankerImage( + + widget.tankerDetails.dbId, + imageUrl + + ); + + Navigator.pop(context); + + if(status){ + + tankerImageUrls.removeAt(index); + + setState((){}); + + AppSettings.longSuccessToast( + "Image deleted" + ); + + /// optional refresh + await _refreshTankerDetails(); + + } + else{ + + AppSettings.longFailedToast( + "Delete failed" + ); + + } + + } + catch(e){ + + Navigator.pop(context); + + AppSettings.longFailedToast( + "Network error" + ); + + } + + } + + Future uploadUpdatedImages() async { + + try{ + + AppSettings.preLoaderDialog(context); + + var request = http.MultipartRequest( + + 'POST', + + Uri.parse( + + AppSettings.host+ + "uploads_tanker_images/" + "${widget.tankerDetails.dbId}" + + ) + + ); + + request.headers.addAll( + await AppSettings.buildRequestHeaders() + ); + + for(var img in newImages){ + + request.files.add( + + await http.MultipartFile.fromPath( + + 'files', + + img.path + + ) + + ); + + } + + var response = + await request.send(); + + Navigator.pop(context); + + var resp = + await http.Response.fromStream(response); + + if(response.statusCode==200){ + + AppSettings.longSuccessToast( + "Images Updated" + ); + + newImages.clear(); + + await _refreshTankerDetails(); + + } + else{ + + print(resp.body); + + AppSettings.longFailedToast( + "Upload failed" + ); + + } + + } + catch(e){ + + Navigator.pop(context); + + AppSettings.longFailedToast( + "Upload failed" + ); + + } + + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -868,13 +1189,223 @@ class _TankerDetailsPageState extends State { padding: const EdgeInsets.all(16), child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + + Text( + "Images (${tankerImageUrls.length}/5)", + style: fontTextStyle( + 12, + Color(0XFF515253), + FontWeight.w600 + ), + ), + + Text( + "Max 5 allowed", + style: fontTextStyle( + 10, + Color(0XFF939495), + FontWeight.w400 + ), + ), + + ], + ), + + SizedBox(height:8), ClipRRect( borderRadius: BorderRadius.circular(12), - child: Image.asset( - 'images/tanker_image.jpeg', - width: double.infinity, - height: 180, - fit: BoxFit.cover, + child: SizedBox( + + height:180, + + child:tankerImageUrls.isEmpty + + ? Image.asset( + 'images/tanker_image.jpeg', + fit:BoxFit.cover + ) + + : ListView.builder( + + scrollDirection:Axis.horizontal, + + itemCount:tankerImageUrls.length, + + itemBuilder:(context,index){ + + return Padding( + + padding:EdgeInsets.only(right:8), + + child:ClipRRect( + + borderRadius: + BorderRadius.circular(12), + + child:Stack( + + children:[ + + Image.network( + + tankerImageUrls[index], + + width:250, + height:180, + + fit:BoxFit.cover, + + errorBuilder:(context,error,stack){ + + return Container( + + width:250, + height:180, + + color:Colors.grey[300], + + child:Icon( + Icons.image_not_supported, + size:40, + color:Colors.grey, + ), + + ); + + }, + + loadingBuilder:(context,child,progress){ + + if(progress == null) + return child; + + return Container( + + width:250, + height:180, + + alignment:Alignment.center, + + child:CircularProgressIndicator(), + + ); + + }, + + ), + + /// DELETE BUTTON + Positioned( + + top:6, + + right:6, + + child:GestureDetector( + + onTap:(){ + + showDialog( + + context:context, + + builder:(context){ + + return AlertDialog( + + title:Text("Delete Image?"), + + actions:[ + + TextButton( + + onPressed:(){ + + Navigator.pop(context); + + }, + + child:Text("Cancel") + + ), + + TextButton( + + onPressed:(){ + + Navigator.pop(context); + + deleteImage(index); + + }, + + child:Text("Delete") + + ) + + ], + + ); + + } + + ); + + }, + + child:Container( + + decoration:BoxDecoration( + + color:Colors.white, + + shape:BoxShape.circle + + ), + + padding:EdgeInsets.all(6), + + child:Container( + + decoration: BoxDecoration( + shape: BoxShape.circle, + ), + + padding: EdgeInsets.all(6), + + child: Image.asset( + + 'images/delete.png', + + width: 18, + height: 18, + colorBlendMode: BlendMode.srcIn, + + ), + + ), + + ), + + ), + + ), + + ], + + ), + + ), + + ); + + }, + + ), + ), ), const SizedBox(height: 16), @@ -1135,9 +1666,11 @@ class _TankerDetailsPageState extends State { width: 0.5), ), ), - onPressed: () async {}, + onPressed: () async { + pickNewImages(); + }, child: Text( - "Assign", + "Update Images", style: fontTextStyle( 14, const Color(0xFFFFFFFF), diff --git a/lib/resources/tankers_model.dart b/lib/resources/tankers_model.dart index 3a0b4f5..55f04ae 100644 --- a/lib/resources/tankers_model.dart +++ b/lib/resources/tankers_model.dart @@ -14,6 +14,7 @@ class TankersModel { String tanker_type=''; String pumping_fee=''; List availability= []; + List images= []; TankersModel(); factory TankersModel.fromJson(Map json){ @@ -29,6 +30,8 @@ class TankersModel { rtvm.price = json['price'] ?? ''; rtvm.delivery_fee = json['delivery_fee'] ?? ''; rtvm.availability = json['availability'] ?? []; + rtvm.images = json['images'] ?? []; + rtvm.manufacturing_year = json['manufacturing_year'] ?? ''; rtvm.insurance_expiry = json['insurance_exp_date'] ?? ''; rtvm.tanker_type = json['tanker_type'] ?? '';