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(); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { final text = newValue.text; if (text.isEmpty) return newValue; // Find first non-space char final i = text.indexOf(RegExp(r'\S')); if (i == -1) return newValue; final first = text[i]; final upper = first.toUpperCase(); if (first == upper) return newValue; final newText = text.replaceRange(i, i + 1, upper); return newValue.copyWith( text: newText, selection: newValue.selection, composing: TextRange.empty, ); } } class ResourcesDriverScreen extends StatefulWidget { const ResourcesDriverScreen({super.key}); @override State createState() => _ResourcesDriverScreenState(); } class _ResourcesDriverScreenState extends State { // ---------- screen state ---------- int selectedTab = 1; // default "Drivers" String search = ''; bool isLoading = false; List driversList = []; // ---------- form (bottom sheet) ---------- final GlobalKey _formKey = GlobalKey(); // Text controllers final _nameCtrl = TextEditingController(); final _mobileCtrl = TextEditingController(); final _ageCtrl = TextEditingController(); final _altMobileCtrl = TextEditingController(); final _locationCtrl = TextEditingController(); final _licenseController = TextEditingController(); final _experienceController = TextEditingController(); // Unused in UI but kept if you later need them final _commissionCtrl = TextEditingController(); final _joinDateCtrl = TextEditingController(); int onDeliveryCount = 0; int availableCount = 0; int offlineCount = 0; // 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? selectedExperience; // years as string final List yearOptions = List.generate(41, (i) => '$i'); // 0..40 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; } String? selectedFilter; String selectedSort = "Name"; List filterOptions = [ "All", "Available", "On delivery", "Offline" ]; List sortOptions = [ "Name", "Deliveries" ]; void _showFilterMenu(BuildContext context, Offset position) async { await showMenu( context: context, color: Colors.white, position: RelativeRect.fromLTRB( position.dx, position.dy, position.dx + 200, position.dy + 200), items: [ PopupMenuItem( enabled:false, child: Text( "Filter", style: fontTextStyle( 12, Color(0XFF2D2E30), FontWeight.w600), ), ), ...filterOptions.map((option){ return PopupMenuItem( child: RadioListTile( value: option, groupValue: selectedFilter ?? "All", activeColor: Color(0XFF1D7AFC), onChanged:(value){ setState(() { selectedFilter = value=="All" ? null : value; }); Navigator.pop(context); }, title: Text( option, style: fontTextStyle( 12, Color(0XFF2D2E30), FontWeight.w400), ), dense:true, visualDensity: VisualDensity( horizontal:-4, vertical:-4), ), ); }).toList() ], ); } void _showSortMenu(BuildContext context, Offset position) async { await showMenu( context: context, color: Colors.white, position: RelativeRect.fromLTRB( position.dx, position.dy, position.dx + 200, position.dy + 200), items: [ PopupMenuItem( enabled:false, child: Text( "Sort", style: fontTextStyle( 12, Color(0XFF2D2E30), FontWeight.w600), ), ), ...sortOptions.map((option){ return PopupMenuItem( child: RadioListTile( value: option, groupValue: selectedSort, activeColor: Color(0XFF1D7AFC), onChanged:(value){ setState(() { selectedSort = value.toString(); }); Navigator.pop(context); }, title: Text( option, style: fontTextStyle( 12, Color(0XFF2D2E30), FontWeight.w400), ), dense:true, visualDensity: VisualDensity( horizontal:-4, vertical:-4), ), ); }).toList() ], ); } final ImagePicker _picker = ImagePicker(); List drivingLicenseImages = []; final int maxImages = 2; XFile? driverImage; @override void initState() { super.initState(); _fetchDrivers(); } @override void dispose() { _nameCtrl.dispose(); _mobileCtrl.dispose(); _ageCtrl.dispose(); _altMobileCtrl.dispose(); _locationCtrl.dispose(); _commissionCtrl.dispose(); _joinDateCtrl.dispose(); super.dispose(); } void _resetForm() { _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; } Future _fetchDrivers() async { setState(() => isLoading = true); try { final response = await AppSettings.getDrivers(); final data = (jsonDecode(response)['data'] as List) .map((e) => DriversModel.fromJson(e)) .toList(); int onDelivery = 0; int available = 0; int offline = 0; for (final d in data) { switch (d.status.toLowerCase().trim()) { case 'on delivery': onDelivery++; break; case 'available': available++; break; case 'offline': offline++; break; } } if (!mounted) return; setState(() { driversList = data; onDeliveryCount = onDelivery; availableCount = available; offlineCount = offline; isLoading = false; }); } catch (e) { debugPrint("⚠️ Error fetching drivers: $e"); setState(() => isLoading = false); } } // ---------- submit ---------- Future _addDriver() async { final ok = _formKey.currentState?.validate() ?? false; if (!ok) { setState(() {}); return; } 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(), "age": _ageCtrl.text.trim(), "alternativeContactNumber": _altMobileCtrl.text.trim(), "years_of_experience": _experienceController.text, "status": _status ?? "available", }; 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(); } 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 ---------- Future _openDriverFormSheet(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, // style inner container builder: (context) { return StatefulBuilder( 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: [ // 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, ), 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), ), 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), ), 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; }, ), ), _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, 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; }, ), ], validator: (value) { if (value == null || value.isEmpty) { return "Phone Number required"; } if (!RegExp(r'^[6-9]').hasMatch(value)) { return "Enter digits starting 6,7,8,9"; } if (value.length != 10) { return "Enter valid 10 digit number"; } return null; }, 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, 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, ), ), _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, ), ), _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), ), ), ), _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 ), ), ], ), ), _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 ), ), ], ), ), 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), ), ), ), ], ), ), ), ), ), ); }); }, ); } // ---------- UI ---------- @override Widget build(BuildContext context) { List filtered = driversList.where((it) { final q = search.trim().toLowerCase(); bool matchesSearch = q.isEmpty || it.driver_name.toLowerCase().contains(q); bool matchesFilter = true; if(selectedFilter!=null){ matchesFilter = it.status.toLowerCase().trim() == selectedFilter!.toLowerCase(); } return matchesSearch && matchesFilter; }).toList(); if(selectedSort=="Name"){ filtered.sort( (a,b)=>a.driver_name .compareTo(b.driver_name)); } if(selectedSort=="Deliveries"){ filtered.sort((a,b)=> (int.tryParse(b.deliveries) ?? 0) .compareTo( int.tryParse(a.deliveries) ?? 0)); } return Scaffold( backgroundColor: Colors.white, body: Column( children: [ // Top card Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF939495)), ), child: Row( children: [ Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 40, decoration: const BoxDecoration( color: Color(0xFFF6F0FF), borderRadius: BorderRadius.all(Radius.circular(8)), ), child: Padding( padding: const EdgeInsets.all(8.0), child: Image.asset('images/drivers.png', fit: BoxFit.contain), ), ), const SizedBox(height: 8), Text('Total Drivers', style: fontTextStyle( 12, const Color(0xFF2D2E30), FontWeight.w500)), ], ), const Spacer(), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(driversList.length.toString(), style: fontTextStyle( 24, const Color(0xFF0D3771), FontWeight.w500)), /* const SizedBox(height: 6), Text('+1 since last month', style: fontTextStyle( 10, const Color(0xFF646566), FontWeight.w400)),*/ ], ), ], ), ), // Metrics row Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: IntrinsicHeight( child: Row( children: [ Expanded( child: SmallMetricBox( title: 'On delivery', value: onDeliveryCount.toString(), ), ), const SizedBox(width: 8), Expanded( child: SmallMetricBox( title: 'Available', value: availableCount.toString(), ), ), const SizedBox(width: 8), Expanded( child: SmallMetricBox( title: 'Offline', value: offlineCount.toString(), ), ), ], ), ), ), const SizedBox(height: 12), // Search + list Expanded( child: Container( color: const Color(0xFFF5F5F5), child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Column( children: [ Row( children: [ Expanded( child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( border: Border.all( color: const Color(0xFF939495), width: 0.5), borderRadius: BorderRadius.circular(22), ), child: Row( children: [ Image.asset('images/search.png', width: 18, height: 18), const SizedBox(width: 8), Expanded( child: TextField( decoration: InputDecoration( hintText: 'Search', hintStyle: fontTextStyle( 12, const Color(0xFF939495), FontWeight.w400), border: InputBorder.none, isDense: true, ), onChanged: (v) => setState(() => search = v), ), ), ], ), ), ), const SizedBox(width: 16), GestureDetector( onTapDown:(details){ _showFilterMenu( context, details.globalPosition); }, child: Image.asset( "images/icon_tune.png", width: 24, height: 24), ), const SizedBox(width: 16), GestureDetector( onTapDown:(details){ _showSortMenu( context, details.globalPosition); }, child: Image.asset( "images/up_down_arrow.png", width: 24, height: 24), ), ], ), const SizedBox(height: 12), Expanded( child: isLoading ? const Center(child: CircularProgressIndicator()) : (filtered.isEmpty ? Center( child: Padding( padding: const EdgeInsets.symmetric( vertical: 12), child: Text( 'No Data Available', style: fontTextStyle( 12, const Color(0xFF939495), FontWeight.w500), ), ), ) : ListView.separated( itemCount: filtered.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, idx) { final d = filtered[idx]; return GestureDetector( onTap: () async{ final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => DriverDetailsPage( driverDetails: d), ), ); if (result == true) { _fetchDrivers(); } }, child: DriverCard( name: d.driver_name, status: d.status, location: d.address, deliveries: int.tryParse(d.deliveries) ?? 0, commission: d.commision, phone: d.phone_number, picture: d.picture ?? '',// ✅ ADD ), ); }, )), ), ], ), ), ), ), ], ), // FAB -> open fixed-height form sheet floatingActionButton: FloatingActionButton( onPressed: () => _openDriverFormSheet(context), backgroundColor: Colors.black, shape: const CircleBorder(), child: const Icon(Icons.add, color: Colors.white), ), ); } } // ---------- widgets ---------- class SmallMetricBox extends StatelessWidget { final String title; final String value; const SmallMetricBox({super.key, required this.title, required this.value}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFF939495)), color: Colors.white, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500)), const SizedBox(height: 4), Text(value, style: fontTextStyle(24, const Color(0xFF0D3771), FontWeight.w500)), ], ), ); } } class DriverCard extends StatelessWidget { final String name; final String status; final String location; final int deliveries; final String commission; final String phone; final String picture; const DriverCard({ super.key, required this.name, required this.status, required this.location, required this.deliveries, required this.commission, required this.phone, required this.picture, }); @override Widget build(BuildContext context) { final Map statusColors = { 'available' : Color(0xFF0A9E04), 'on delivery' : Color(0xFFD0AE3C), 'offline' : Color(0xFF939495), }; final statusColor = statusColors[status.toLowerCase().trim()] ?? Colors.grey; Future _makePhoneCall(String phone) async { final Uri url = Uri( scheme: 'tel', path: phone, ); if(await canLaunchUrl(url)){ await launchUrl(url); } } return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Row with name + phone Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ 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, children: [ Text(name, style: fontTextStyle( 14, const Color(0xFF2D2E30), FontWeight.w500)), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: statusColor), ), child: Text(status, style: fontTextStyle( 10, statusColor, FontWeight.w400)), ), ], ), ], ), GestureDetector( onTap: (){ _makePhoneCall(phone); }, child: Container( padding: const EdgeInsets.all(8), decoration: const BoxDecoration( color: Color(0xFFF5F6F6), shape: BoxShape.circle, ), child: Image.asset( "images/phone_icon.png", width: 20, height: 20, ), ), ) ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text.rich( TextSpan( children: [ TextSpan( text: "Location\n", style: fontTextStyle( 8, const Color(0xFF939495), FontWeight.w500)), TextSpan( text: location, style: fontTextStyle( 12, const Color(0xFF515253), FontWeight.w500)), ], ), textAlign: TextAlign.center, ), Text.rich( TextSpan( children: [ TextSpan( text: "Deliveries\n", style: fontTextStyle( 8, const Color(0xFF939495), FontWeight.w400)), TextSpan( text: deliveries.toString(), style: fontTextStyle( 12, const Color(0xFF515253), FontWeight.w500)), ], ), textAlign: TextAlign.center, ), Visibility( visible:commission!='' , child: Text.rich( TextSpan( children: [ TextSpan( text: "Commission\n", style: fontTextStyle( 8, const Color(0xFF939495), FontWeight.w400)), TextSpan( text: commission, style: fontTextStyle( 12, const Color(0xFF515253), FontWeight.w500)), ], ), textAlign: TextAlign.center, ),) ], ), /* const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ OutlinedButton( style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF939495)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16)), ), onPressed: () {}, child: Text("View Schedule", style: fontTextStyle( 12, const Color(0xFF515253), FontWeight.w400)), ), const SizedBox(width: 12), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF8270DB), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16)), ), onPressed: () {}, child: Text("Assign", style: fontTextStyle( 12, const Color(0xFFFFFFFF), FontWeight.w400)), ), ], )*/ ], ), ); } } 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, ], ), ); } }