diff --git a/images/recent_trips.png b/images/recent_trips.png new file mode 100644 index 0000000..69db6ce Binary files /dev/null and b/images/recent_trips.png differ diff --git a/lib/common/settings.dart b/lib/common/settings.dart index ed9dbb2..606ef37 100644 --- a/lib/common/settings.dart +++ b/lib/common/settings.dart @@ -160,9 +160,13 @@ class AppSettings{ static String getTankersUrl = host + 'getTankers'; static String getTankerDetailsByNameUrl = host + 'getsingledetails'; static String addTankerUrl = host + 'addTankers'; + static String updateTankerAvailabilityUrl = host + 'updatetankerstatus'; + static String deleteTankerUrl = host + 'deleteTanker'; static String updateTankerUrl = host + 'updateTankers'; static String getDriversUrl = host + 'getalldeliveryboys'; static String addDriversUrl = host + 'addDeliveryboys'; + static String updateDriversUrl = host + 'updatedeliveryboy'; + static String deleteDriverUrl = host + 'deletedeliveryboy'; static String addSourceLocationsUrl = host + 'addSource'; static String getSourceLoctaionsUrl = host + 'getallsourcesofsupplier'; static String setRatesDailyUrl = host + 'tankers'; @@ -558,7 +562,10 @@ class AppSettings{ } static Future addTankers(payload) async { - var response = await http.post(Uri.parse(addTankerUrl + '/' + supplierId), + + var uri = Uri.parse(addTankerUrl + '/' + supplierId); + + var response = await http.post(uri, body: json.encode(payload), headers: await buildRequestHeaders()); if (response.statusCode == 200) { @@ -573,7 +580,7 @@ class AppSettings{ } else if (response.statusCode == 401) { bool status = await AppSettings.resetToken(); if (status) { - response = await http.post(Uri.parse(addTankerUrl + '/' + supplierId), + response = await http.post(uri, body: json.encode(payload), headers: await buildRequestHeaders()); if (response.statusCode == 200) { return true; @@ -622,6 +629,71 @@ class AppSettings{ } } + static Future updateTankerAvailability(payload) async { + + var uri = Uri.parse(updateTankerAvailabilityUrl + '/' + supplierId); + var response = await http.put(uri, + body: json.encode(payload), headers: await buildRequestHeaders()); + + if (response.statusCode == 200) { + try { + var _response = json.decode(response.body); + print(_response); + return true; + } catch (e) { + // display error toast + return false; + } + } else if (response.statusCode == 401) { + bool status = await AppSettings.resetToken(); + if (status) { + response = await http.put(uri, + body: json.encode(payload), headers: await buildRequestHeaders()); + if (response.statusCode == 200) { + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + static Future deleteTanker(tankerName) async { + + var uri = Uri.parse(deleteTankerUrl + '/' + supplierId); + uri = uri.replace(query: 'tankerName=$tankerName'); + var response = await http.put(uri, headers: await buildPutRequestHeaders()); + + if (response.statusCode == 200) { + try { + var _response = json.decode(response.body); + print(_response); + return true; + } catch (e) { + // display error toast + return false; + } + } else if (response.statusCode == 401) { + bool status = await AppSettings.resetToken(); + if (status) { + response = await http.put(uri, headers: await buildPutRequestHeaders()); + if (response.statusCode == 200) { + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + static Future getDrivers() async { var uri = Uri.parse(getDriversUrl+'/'+supplierId); //uri = uri.replace(query: 'supplierId=$supplierId'); @@ -677,6 +749,71 @@ class AppSettings{ } } + static Future updateDrivers(payload,phone) async { + + var uri =Uri.parse(updateDriversUrl + '/' + supplierId); + uri = uri.replace(query: 'phone=$phone'); + + 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 true; + } catch (e) { + // display error toast + return false; + } + } 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) { + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + static Future deleteDriver(driverPhone) async { + + var uri = Uri.parse(deleteDriverUrl + '/' + supplierId); + uri = uri.replace(query: 'phone=$driverPhone'); + var response = await http.put(uri, headers: await buildPutRequestHeaders()); + + if (response.statusCode == 200) { + try { + var _response = json.decode(response.body); + print(_response); + return true; + } catch (e) { + // display error toast + return false; + } + } else if (response.statusCode == 401) { + bool status = await AppSettings.resetToken(); + if (status) { + response = await http.put(uri, headers: await buildPutRequestHeaders()); + if (response.statusCode == 200) { + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + static Future getSourceLoctaions() async { var uri = Uri.parse(getSourceLoctaionsUrl+'/'+supplierId); //uri = uri.replace(query: 'supplierId=$supplierId'); @@ -1033,6 +1170,7 @@ class AppSettings{ backgroundColor: Colors.white, elevation: 0, scrolledUnderElevation: 0, + titleSpacing: 0, title: Text( title, style: fontTextStyle(16, Color(0XFF2A2A2A), FontWeight.w600), @@ -1081,6 +1219,7 @@ class AppSettings{ backgroundColor: Colors.white, elevation: 0, scrolledUnderElevation: 0, + titleSpacing: 0, title: Text( title, style: fontTextStyle(16, Color(0XFF2A2A2A), FontWeight.w600), diff --git a/lib/orders/assign_driver.dart b/lib/orders/assign_driver.dart index 52859ef..b46e4c9 100644 --- a/lib/orders/assign_driver.dart +++ b/lib/orders/assign_driver.dart @@ -614,6 +614,9 @@ class _AssignDriverScreenState extends State { final selectedDriver = selectedDriverIndex != null ? driversList[selectedDriverIndex!] : null; + final selectedSource = selectedSourceIndex != null + ? sourceLocationsList[selectedSourceIndex!] + : null; AppSettings.preLoaderDialog(context); bool isOnline = @@ -621,12 +624,10 @@ class _AssignDriverScreenState extends State { if (isOnline) { var payload = {}; - payload["tankerName"] = - selectedTanker.tanker_name; - payload["delivery_agent"] = - selectedDriver?.driver_name; - payload["delivery_agent_mobile"] = - selectedDriver?.phone_number; + payload["tankerName"] = selectedTanker.tanker_name; + payload["delivery_agent"] = selectedDriver?.driver_name; + payload["delivery_agent_mobile"] = selectedDriver?.phone_number; + payload["water_source_location"] = selectedSource?.source_name; bool status = await AppSettings.assignTanker( payload, widget.order.dbId); diff --git a/lib/orders/change_driver.dart b/lib/orders/change_driver.dart index 916c183..06778a6 100644 --- a/lib/orders/change_driver.dart +++ b/lib/orders/change_driver.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:supplier_new/common/settings.dart'; import '../resources/drivers_model.dart'; +import '../resources/source_loctaions_model.dart'; import '../resources/tankers_model.dart'; class ChangeDriverScreen extends StatefulWidget { @@ -19,10 +20,13 @@ class _ChangeDriverScreenState extends State { double totalFare = 0.0; bool isLoading = false; bool isTankersDataLoading = false; + bool isSourceDataLoading = false; List driversList = []; List tankersList = []; + List sourcesList = []; TankersModel? orderTanker; DriversModel? orderDriver; + SourceLocationsModel? orderSource; @override @@ -31,6 +35,7 @@ class _ChangeDriverScreenState extends State { super.initState(); _fetchTankers(); _fetchDrivers(); + _fetchSources(); advance = 150; advancePayable = advance; totalFare = advance + double.parse(widget.order.quoted_amount); @@ -99,6 +104,37 @@ class _ChangeDriverScreenState extends State { } } + Future _fetchSources() async { + setState(() => isSourceDataLoading = true); + try { + final response = await AppSettings.getSourceLoctaions(); + final data = (jsonDecode(response)['data'] as List) + .map((e) => SourceLocationsModel.fromJson(e)) + .toList(); + if (!mounted) return; + + final wanted = + (widget.order.water_source_location ?? '').trim().toLowerCase(); + SourceLocationsModel? matched; + for (final s in data) { + if ((s.source_name ?? '').trim().toLowerCase() == wanted) { + matched = s; + break; + } + } + + setState(() { + sourcesList = data; + orderSource = matched; + isSourceDataLoading = false; + }); + } catch (e) { + debugPrint("⚠️ Error fetching source locations: $e"); + setState(() => isSourceDataLoading = false); + } + } + + Widget _assignedTankerDetails() { if (isTankersDataLoading) { return const Center(child: CircularProgressIndicator()); @@ -329,10 +365,74 @@ class _ChangeDriverScreenState extends State { ); } + Widget _assignedSourceDetails() { + if (isSourceDataLoading) { + return const Center(child: CircularProgressIndicator()); + } + + if (orderSource == null) { + // fallback if not found + return Container( + padding: const EdgeInsets.all(0), + decoration: BoxDecoration( + color: const Color(0XFFFFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0XFFFFFFFF)), + ), + child: Row( + children: [ + Image.asset('images/avatar.png', width: 24, height: 24), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.order.water_source_location ?? "Driver", + style: fontTextStyle(14, const Color(0XFF2D2E30), FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + final d = orderSource!; + + return Container( + padding: const EdgeInsets.all(0), + decoration: BoxDecoration( + color: const Color(0XFFFFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0XFFFFFFFF)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Image.asset('images/avatar.png', width: 24, height: 24), + const SizedBox(width: 8), + Expanded( + child: Text( + d.source_name ?? "", + style: fontTextStyle(14, const Color(0xFF2D2E30), FontWeight.w500), + maxLines: 1, overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + /* const SizedBox(height: 8), + _line("Phone", d.phone_number ?? ""),*/ + ], + ), + ); + } + void _showAssignTankerBottomSheet() { // πŸ”Έ Find default selected indexes int? selectedTankerIndex; int? selectedDriverIndex; + int? selectedSourceIndex; int _capToLiters(dynamic cap) { if (cap == null) return -1; @@ -360,6 +460,11 @@ class _ChangeDriverScreenState extends State { selectedDriverIndex = driversList.indexWhere( (d) => d.driver_name == widget.order.delivery_agent_name); + selectedSourceIndex = sourcesList.indexWhere( + (d) => d.source_name == widget.order.water_source_location); + + + showModalBottomSheet( backgroundColor: const Color(0XFFFFFFFF), context: context, @@ -599,6 +704,105 @@ class _ChangeDriverScreenState extends State { ); }, ), + + const SizedBox(height: 16), + + /// 🧍 Source List + Text( + "SELECTED SOURCE LOCATION", + style: fontTextStyle( + 10, const Color(0XFF2D2E30), FontWeight.w600), + ), + const SizedBox(height: 12), + + selectedTankerIndex == null + ? Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(top: 8), + decoration: BoxDecoration( + color: const Color(0XFFFFFFFF), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFC3C4C4)), + ), + child: Center( + child: Text( + 'Select a tanker to choose driver', + style: fontTextStyle( + 14, + const Color(0xFF2D2E30), + FontWeight.w400), + ), + ), + ) + : ListView.separated( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: sourcesList.length, + separatorBuilder: (_, __) => + const SizedBox(height: 12), + itemBuilder: (context, idx) { + final d = sourcesList[idx]; + final isSelected = + selectedSourceIndex == idx; + /* final statusColor = d.status == "available" + ? const Color(0XFF0A9E04) + : (d.status == "on delivery" + ? const Color(0XFFD0AE3C) + : (d.status == "offline" + ? const Color(0XFF939495) + : Colors.grey));*/ + + return GestureDetector( + onTap: () { + setModalState(() { + selectedSourceIndex = idx; + }); + }, + child: Card( + color: const Color(0XFFFFFFFF), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + side: BorderSide( + color: isSelected + ? primaryColor + : const Color(0XFFC3C4C4), + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Image.asset( + 'images/avatar.png', + fit: BoxFit.cover, + width: 20, + height: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + d.source_name, + style: fontTextStyle( + 14, + const Color( + 0XFF2D2E30), + FontWeight.w500), + ), + ), + ], + ), + ), + ), + ); + }, + ), ], ), ), @@ -625,6 +829,9 @@ class _ChangeDriverScreenState extends State { final selectedDriver = selectedDriverIndex != null ? driversList[selectedDriverIndex!] : null; + final selectedSource = selectedSourceIndex != null + ? sourcesList[selectedSourceIndex!] + : null; AppSettings.preLoaderDialog(context); bool isOnline = @@ -636,8 +843,8 @@ class _ChangeDriverScreenState extends State { selectedTanker.tanker_name; payload["delivery_agent"] = selectedDriver?.driver_name; - payload["delivery_agent_mobile"] = - selectedDriver?.phone_number; + payload["delivery_agent_mobile"] = selectedDriver?.phone_number; + payload["water_source_location"] = selectedSource?.source_name; bool status = await AppSettings.assignTanker( payload, widget.order.dbId); @@ -928,6 +1135,7 @@ class _ChangeDriverScreenState extends State { visible:widget.order.delivery_agent_name != null && widget.order.delivery_agent_name.toString().trim().isNotEmpty, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: MediaQuery.of(context).size.height * .012, @@ -942,7 +1150,28 @@ class _ChangeDriverScreenState extends State { ), _assignedDriverDetails() ], - )) + )), + + Visibility( + visible:widget.order.water_source_location != null && + widget.order.water_source_location.toString().trim().isNotEmpty, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + Text( + "ASSIGNED SOURCE LOCATION", + style: fontTextStyle( + 10, const Color(0XFF646566), FontWeight.w600), + ), + SizedBox( + height: MediaQuery.of(context).size.height * .012, + ), + _assignedSourceDetails() + ], + )) ], )), ], diff --git a/lib/orders/orders_model.dart b/lib/orders/orders_model.dart index 3aa2744..fe52b10 100644 --- a/lib/orders/orders_model.dart +++ b/lib/orders/orders_model.dart @@ -23,6 +23,7 @@ class OrdersModel { String imageAsset='images/building.png'; String delivery_agent_name = ''; String tanker_name = ''; + String water_source_location=''; OrdersModel(); @@ -43,6 +44,7 @@ class OrdersModel { rtvm.lat=json['latitude'] ?? 0.0; rtvm.delivery_agent_name = json['delivery_agent'] ?? ''; rtvm.tanker_name = json['tankerName'] ?? ''; + rtvm.water_source_location = json['water_source_location'] ?? ''; // Split and trim List parts = rtvm.address.split(',').map((e) => e.trim()).toList(); diff --git a/lib/profile/source_location.dart b/lib/profile/source_location.dart index e17f343..143739f 100644 --- a/lib/profile/source_location.dart +++ b/lib/profile/source_location.dart @@ -57,7 +57,6 @@ class _SourceLocationState extends State { bool isLoading = false; int currentStep = 2; List sourceLocationsList = []; - final _locationNameController = TextEditingController(); bool addBusinessAsSource = false; final List waterTypes = [ @@ -170,7 +169,7 @@ class _SourceLocationState extends State { _LabeledField( label: "Location Name *", child: TextFormField( - controller: _locationNameController, + controller: _nameCtrl, validator: (v) => _required(v, field: "Location Name"), textCapitalization: TextCapitalization.none, inputFormatters: const [ diff --git a/lib/resources/driver_details.dart b/lib/resources/driver_details.dart index d243dd3..ce50dba 100644 --- a/lib/resources/driver_details.dart +++ b/lib/resources/driver_details.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:supplier_new/resources/resources_drivers.dart'; +import '../common/settings.dart'; class DriverDetailsPage extends StatefulWidget { var driverDetails; @@ -10,275 +13,850 @@ class DriverDetailsPage extends StatefulWidget { } class _DriverDetailsPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: Text( - widget.driverDetails.driver_name.isNotEmpty - ? widget.driverDetails.driver_name[0].toUpperCase() + - widget.driverDetails.driver_name.substring(1) - : '', - style: TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - centerTitle: false, - actions: [ - TextButton( - onPressed: () {}, - child: const Text( - "HELP", - style: TextStyle( - color: Color(0xFF4F46E5), - fontWeight: FontWeight.w600, - fontSize: 14, + + // 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 GlobalKey _formKey = GlobalKey(); + // Unused in UI but kept if you later need them + final _commissionCtrl = TextEditingController(); + final _joinDateCtrl = TextEditingController(); + bool isLoading=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? 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? 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 _refreshTankerDetails() async { + try { + setState(() => isLoading = true); + + final updatedDetails = await AppSettings.getTankerDetailsByName( + _mobileCtrl.text.trim(), + ); + + if (updatedDetails != null) { + setState(() { + widget.driverDetails = updatedDetails; + }); + } 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); + } + } + + 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": selectedLicense ?? "", + "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": selectedExperience ?? "", + "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 + _refreshTankerDetails(); + } 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 ?? ''; + selectedExperience = fitToOption(widget.driverDetails.years_of_experience, yearOptions); + selectedLicense = fitToOption(widget.driverDetails.license_number, licenseNumbers); + _status = fitToOption(widget.driverDetails.status, _statusOptions); + /* _capacityCtrl.text = widget.tankerDetails.capacity ?? ''; + _plateCtrl.text = widget.tankerDetails.license_plate ?? ''; + _mfgYearCtrl.text = widget.tankerDetails.manufacturing_year ?? ''; + _insExpiryCtrl.text = widget.tankerDetails.insurance_expiry ?? ''; + + selectedExperience = fitToOption(widget.tankerDetails.tanker_type, tankerTypes); + selectedTypeOfWater = fitToOption(widget.tankerDetails.type_of_water, typeOfWater); + */ + 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: DropdownButtonFormField( + value: selectedLicense, + items: licenseNumbers + .map((t) => + DropdownMenuItem(value: t, child: Text(t))) + .toList(), + onChanged: (v) => setState(() => selectedLicense = v), + validator: (v) => v == null || v.isEmpty + ? "Driver License required" + : null, + isExpanded: true, + alignment: Alignment.centerLeft, + hint: Text( + "Select License Number", + 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: "Years of Experience *", + child: DropdownButtonFormField( + value: selectedExperience, + items: yearOptions + .map((t) => + DropdownMenuItem(value: t, child: Text(t))) + .toList(), + onChanged: (v) => + setState(() => selectedExperience = v), + validator: (v) => v == null || v.isEmpty + ? "Experience is required" + : null, + isExpanded: true, + alignment: Alignment.centerLeft, + hint: Text( + "Years", + 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: "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 tanker'); + } + + + }, + 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 statusColor = switch (widget.driverDetails.status) { + 'available' => const Color(0xFF0A9E04), + 'on delivery' => const Color(0xFFD0AE3C), + 'offline' => const Color(0xFF939495), + _ => Colors.grey, + }; + + return WillPopScope( + onWillPop: () async { + // βœ… Return true to indicate successful tanker update refresh + 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), body: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + + SizedBox(height:MediaQuery.of(context).size.height * .1,), // πŸ‘€ Driver Profile Card - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF4F0FF), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + 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: [ - const CircleAvatar( - radius: 30, - backgroundColor: Colors.grey, - backgroundImage: AssetImage('images/avatar.png'), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Status Chip - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: const Color(0xFFE8FFF0), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFF0A9E04), width: 0.8), - ), - child: const Text( - "available", - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Color(0xFF0A9E04), + // 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: 4), - Text( - widget.driverDetails.driver_name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Colors.black, + const SizedBox(height: 6), + + Text( + widget.driverDetails.driver_name, + style: fontTextStyle( + 20, Color(0XFF2D2E30), FontWeight.w500), ), - ), - const SizedBox(height: 2), - const Text( - "+91 789456212 β€’ +91 789456212", - style: TextStyle( - fontSize: 13, - color: Colors.black54, + 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), + ), + ], + ), + ), + ], + ) + ], ), - IconButton( - onPressed: () { - // πŸ“ž Call action - }, - icon: const Icon(Icons.call, color: Color(0xFF4F46E5)), - ) - ], - ), - const SizedBox(height: 16), - // Buttons - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Color(0xFF4F46E5)), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - padding: const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text( - "View Schedule", - style: TextStyle( - color: Color(0xFF4F46E5), - fontWeight: FontWeight.w600, - ), + 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 ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF4F46E5), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - padding: const EdgeInsets.symmetric(vertical: 12), - ), - child: const Text( - "Assign", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, + onPressed: () { + + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Change Status", + style: fontTextStyle( + 14, const Color(0XFF515253), FontWeight.w500), ), - ), + ], ), ), - ], - ), - ], - ), - ), - - const SizedBox(height: 24), + 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( + // πŸͺͺ 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: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - Spacer(), - Text( - widget.driverDetails.driver_name, - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - Text( - "TS84996859930326", - style: TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - Align( - alignment: Alignment.bottomRight, - child: Text( - "Expires on 29/02/2028", - style: TextStyle( - color: Colors.white70, - fontSize: 12, + 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), - const Text( - "RECENT TRIPS", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - ), - const SizedBox(height: 12), + Padding(padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "RECENT TRIPS", + style:fontTextStyle(10, const Color(0xFF343637), FontWeight.w600), + ), + const SizedBox(height: 12), - _buildTripCard("Drinking Water - 10,000 L", "7:02 PM, 28 Jun 2025"), - const SizedBox(height: 8), - _buildTripCard("Drinking Water - 10,000 L", "7:02 PM, 28 Jun 2025"), - const SizedBox(height: 8), - _buildTripCard("Drinking Water - 10,000 L", "7:02 PM, 28 Jun 2025"), + _buildTripCard("Drinking Water - 10,000 L", "7:02 PM, 28 Jun 2025"), + const SizedBox(height: 8), + _buildTripCard("Drinking Water - 10,000 L", "7:02 PM, 28 Jun 2025"), + const SizedBox(height: 8), + _buildTripCard("Drinking Water - 10,000 L", "7:02 PM, 28 Jun 2025"), + ], + ),) - const SizedBox(height: 30), ], ), ), ), - ); + )); } Widget _buildTripCard(String title, String time) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: const Color(0xFFF8F8F8), - borderRadius: BorderRadius.circular(12), + color: const Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Color(0XFFC3C4C4)), ), child: Row( children: [ - const Icon(Icons.local_shipping, color: Color(0xFF4F46E5), size: 24), + + Image.asset('images/recent_trips.png', width: 28, height: 28), const SizedBox(width: 12), Expanded( child: Column( @@ -286,19 +864,12 @@ class _DriverDetailsPageState extends State { children: [ Text( title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Colors.black, - ), + style:fontTextStyle(14, const Color(0xFF2D2E30), FontWeight.w500), ), const SizedBox(height: 2), Text( time, - style: const TextStyle( - fontSize: 12, - color: Colors.black54, - ), + style:fontTextStyle(10, const Color(0xFF939495), FontWeight.w400), ), ], ), @@ -309,3 +880,33 @@ class _DriverDetailsPageState extends State { } } +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, + ], + ), + ); + } +} + diff --git a/lib/resources/drivers_model.dart b/lib/resources/drivers_model.dart index 7d16278..08f4c8e 100644 --- a/lib/resources/drivers_model.dart +++ b/lib/resources/drivers_model.dart @@ -5,8 +5,11 @@ class DriversModel { String address=''; String deliveries='13'; String commision=''; + String years_of_experience=''; List availability= ['filled', 'available']; String phone_number=''; + String alt_phone_number=''; + String license_number=''; DriversModel(); factory DriversModel.fromJson(Map json){ @@ -17,7 +20,10 @@ class DriversModel { rtvm.driver_name = json['name'] ?? ''; rtvm.status = json['status'] ?? ''; rtvm.address = json['address'] ?? ''; - rtvm.phone_number = json['phone'] ?? ''; + rtvm.phone_number =json['phone'] ?? ''; + rtvm.alt_phone_number =json['alternativeContactNumber'] ?? ''; + rtvm.years_of_experience = json['years_of_experience'] ?? ''; + rtvm.license_number = json['license_number'] ?? ''; return rtvm; } } \ No newline at end of file diff --git a/lib/resources/resources_drivers.dart b/lib/resources/resources_drivers.dart index 8913533..8b517d3 100644 --- a/lib/resources/resources_drivers.dart +++ b/lib/resources/resources_drivers.dart @@ -10,9 +10,9 @@ class FirstCharUppercaseFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { + TextEditingValue oldValue, + TextEditingValue newValue, + ) { final text = newValue.text; if (text.isEmpty) return newValue; @@ -63,7 +63,11 @@ class _ResourcesDriverScreenState extends State { // Dropdown state String? _status; // 'available' | 'on delivery' | 'offline' - final List _statusOptions = const ['available', 'on delivery', 'offline']; + final List _statusOptions = const [ + 'available', + 'on delivery', + 'offline' + ]; String? selectedLicense; final List licenseNumbers = const [ @@ -73,7 +77,8 @@ class _ResourcesDriverScreenState extends State { ]; String? selectedExperience; // years as string - final List yearOptions = List.generate(41, (i) => '$i'); // 0..40 + 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"; @@ -147,7 +152,9 @@ class _ResourcesDriverScreenState extends State { final payload = { "Name": _nameCtrl.text.trim(), "license_number": selectedLicense ?? "", - "address": _locationCtrl.text.trim().isEmpty ? AppSettings.userAddress : _locationCtrl.text.trim(), + "address": _locationCtrl.text.trim().isEmpty + ? AppSettings.userAddress + : _locationCtrl.text.trim(), "supplier_name": AppSettings.userName, "phone": _mobileCtrl.text.trim(), "alternativeContactNumber": _altMobileCtrl.text.trim(), @@ -175,17 +182,16 @@ class _ResourcesDriverScreenState extends State { } // ---------- bottom sheet ---------- - Future _openDriverFormSheet(BuildContext context)async { - + Future _openDriverFormSheet(BuildContext context) async { await showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, // style inner container + 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) + heightFactor: 0.75, // fixed height (75% of screen) child: Container( decoration: const BoxDecoration( color: Colors.white, @@ -229,7 +235,8 @@ class _ResourcesDriverScreenState extends State { ], decoration: InputDecoration( hintText: "Full Name", - hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), border: const OutlineInputBorder(), isDense: true, ), @@ -242,21 +249,27 @@ class _ResourcesDriverScreenState extends State { child: DropdownButtonFormField( value: selectedLicense, items: licenseNumbers - .map((t) => DropdownMenuItem(value: t, child: Text(t))) + .map((t) => + DropdownMenuItem(value: t, child: Text(t))) .toList(), onChanged: (v) => setState(() => selectedLicense = v), - validator: (v) => v == null || v.isEmpty ? "Driver License required" : null, + validator: (v) => v == null || v.isEmpty + ? "Driver License required" + : null, isExpanded: true, alignment: Alignment.centerLeft, hint: Text( "Select License Number", - style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + style: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), ), - icon: Image.asset('images/downarrow.png', width: 16, height: 16), + icon: Image.asset('images/downarrow.png', + width: 16, height: 16), decoration: const InputDecoration( border: OutlineInputBorder(), isDense: false, - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), ), ), ), @@ -266,21 +279,28 @@ class _ResourcesDriverScreenState extends State { child: DropdownButtonFormField( value: selectedExperience, items: yearOptions - .map((t) => DropdownMenuItem(value: t, child: Text(t))) + .map((t) => + DropdownMenuItem(value: t, child: Text(t))) .toList(), - onChanged: (v) => setState(() => selectedExperience = v), - validator: (v) => v == null || v.isEmpty ? "Experience is required" : null, + onChanged: (v) => + setState(() => selectedExperience = v), + validator: (v) => v == null || v.isEmpty + ? "Experience is required" + : null, isExpanded: true, alignment: Alignment.centerLeft, hint: Text( "Years", - style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + style: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), ), - icon: Image.asset('images/downarrow.png', width: 16, height: 16), + icon: Image.asset('images/downarrow.png', + width: 16, height: 16), decoration: const InputDecoration( border: OutlineInputBorder(), isDense: false, - contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), ), ), ), @@ -289,7 +309,8 @@ class _ResourcesDriverScreenState extends State { label: "Phone Number *", child: TextFormField( controller: _mobileCtrl, - validator: (v) => _validatePhone(v, label: "Phone Number"), + validator: (v) => + _validatePhone(v, label: "Phone Number"), keyboardType: TextInputType.phone, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -297,7 +318,8 @@ class _ResourcesDriverScreenState extends State { ], decoration: InputDecoration( hintText: "Mobile Number", - hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), border: const OutlineInputBorder(), isDense: true, ), @@ -310,8 +332,10 @@ class _ResourcesDriverScreenState extends State { child: TextFormField( controller: _altMobileCtrl, validator: (v) { - if (v == null || v.trim().isEmpty) return null; // optional - return _validatePhone(v, label: "Alternate Phone Number"); + if (v == null || v.trim().isEmpty) + return null; // optional + return _validatePhone(v, + label: "Alternate Phone Number"); }, keyboardType: TextInputType.phone, inputFormatters: [ @@ -320,7 +344,8 @@ class _ResourcesDriverScreenState extends State { ], decoration: InputDecoration( hintText: "Mobile Number", - hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), border: const OutlineInputBorder(), isDense: true, ), @@ -339,7 +364,8 @@ class _ResourcesDriverScreenState extends State { ], decoration: InputDecoration( hintText: "Area / locality", - hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400), + hintStyle: fontTextStyle( + 14, const Color(0xFF939495), FontWeight.w400), border: const OutlineInputBorder(), isDense: true, ), @@ -352,21 +378,26 @@ class _ResourcesDriverScreenState extends State { child: DropdownButtonFormField( value: _status, items: _statusOptions - .map((s) => DropdownMenuItem(value: s, child: Text(s))) + .map((s) => + DropdownMenuItem(value: s, child: Text(s))) .toList(), onChanged: (v) => setState(() => _status = v), - validator: (v) => v == null || v.isEmpty ? "Status is required" : null, + 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), + 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), + contentPadding: EdgeInsets.symmetric( + horizontal: 12, vertical: 14), ), ), ), @@ -387,7 +418,8 @@ class _ResourcesDriverScreenState extends State { onPressed: _addDriver, child: Text( "Save", - style: fontTextStyle(14, Colors.white, FontWeight.w600), + style: fontTextStyle( + 14, Colors.white, FontWeight.w600), ), ), ), @@ -405,6 +437,11 @@ class _ResourcesDriverScreenState extends State { // ---------- UI ---------- @override Widget build(BuildContext context) { + final filtered = driversList.where((it) { + final q = search.trim().toLowerCase(); + if (q.isEmpty) return true; + return it.driver_name.toLowerCase().contains(q); + }).toList(); return Scaffold( backgroundColor: Colors.white, body: Column( @@ -432,20 +469,27 @@ class _ResourcesDriverScreenState extends State { ), child: Padding( padding: const EdgeInsets.all(8.0), - child: Image.asset('images/drivers.png', fit: BoxFit.contain), + child: Image.asset('images/drivers.png', + fit: BoxFit.contain), ), ), const SizedBox(height: 8), - Text('Total Drivers', style: fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500)), + 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)), + 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)), + Text('+1 since last month', + style: fontTextStyle( + 10, const Color(0xFF646566), FontWeight.w400)), ], ), ], @@ -458,9 +502,11 @@ class _ResourcesDriverScreenState extends State { child: IntrinsicHeight( child: Row( children: const [ - Expanded(child: SmallMetricBox(title: 'On delivery', value: '2')), + Expanded( + child: SmallMetricBox(title: 'On delivery', value: '2')), SizedBox(width: 8), - Expanded(child: SmallMetricBox(title: 'Available', value: '3')), + Expanded( + child: SmallMetricBox(title: 'Available', value: '3')), SizedBox(width: 8), Expanded(child: SmallMetricBox(title: 'Offline', value: '1')), ], @@ -482,24 +528,31 @@ class _ResourcesDriverScreenState extends State { children: [ Expanded( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), decoration: BoxDecoration( - border: Border.all(color: const Color(0xFF939495), width: 0.5), + 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), + 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), + hintStyle: fontTextStyle( + 12, + const Color(0xFF939495), + FontWeight.w400), border: InputBorder.none, isDense: true, ), - onChanged: (v) => setState(() => search = v), + onChanged: (v) => + setState(() => search = v), ), ), ], @@ -507,39 +560,62 @@ class _ResourcesDriverScreenState extends State { ), ), const SizedBox(width: 16), - Image.asset("images/icon_tune.png", width: 24, height: 24), + Image.asset("images/icon_tune.png", + width: 24, height: 24), const SizedBox(width: 16), - Image.asset("images/up_down_arrow.png", width: 24, height: 24), + Image.asset("images/up_down_arrow.png", + width: 24, height: 24), ], ), const SizedBox(height: 12), Expanded( child: isLoading ? const Center(child: CircularProgressIndicator()) - : ListView.separated( - itemCount: driversList.length, - separatorBuilder: (_, __) => const SizedBox(height: 12), - itemBuilder: (context, idx) { - final d = driversList[idx]; - return GestureDetector( - onTap: (){ - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DriverDetailsPage(driverDetails: d), - ), - ); - }, - child: DriverCard( - name: d.driver_name, - status: d.status, - location: d.address, - deliveries: int.tryParse(d.deliveries) ?? 0, - commission: d.commision, - ), - ); - }, - ), + : (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, + ), + ); + }, + )), ), ], ), @@ -580,9 +656,13 @@ class SmallMetricBox extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500)), + Text(title, + style: + fontTextStyle(12, const Color(0xFF2D2E30), FontWeight.w500)), const SizedBox(height: 4), - Text(value, style: fontTextStyle(24, const Color(0xFF0D3771), FontWeight.w500)), + Text(value, + style: + fontTextStyle(24, const Color(0xFF0D3771), FontWeight.w500)), ], ), ); @@ -607,8 +687,12 @@ class DriverCard extends StatelessWidget { @override Widget build(BuildContext context) { - final statusColor = - status == "available" ? Colors.green : (status == "on delivery" ? Colors.orange : Colors.grey); + final statusColor = switch (status) { + 'available' => const Color(0xFF0A9E04), + 'on delivery' => const Color(0xFFD0AE3C), + 'offline' => const Color(0xFF939495), + _ => Colors.grey, + }; return Container( padding: const EdgeInsets.all(14), @@ -626,22 +710,26 @@ class DriverCard extends StatelessWidget { children: [ Row( children: [ - ClipOval( - child: Image.asset("images/profile_pic.png", height: 36, width: 36, fit: BoxFit.cover), - ), + 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)), + Text(name, + style: fontTextStyle( + 14, const Color(0xFF2D2E30), FontWeight.w500)), const SizedBox(height: 4), Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + 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)), + child: Text(status, + style: fontTextStyle( + 10, statusColor, FontWeight.w400)), ), ], ), @@ -653,7 +741,8 @@ class DriverCard extends StatelessWidget { color: Color(0xFFF5F6F6), shape: BoxShape.circle, ), - child: Image.asset("images/phone_icon.png", width: 20, height: 20), + child: + Image.asset("images/phone_icon.png", width: 20, height: 20), ), ], ), @@ -664,8 +753,14 @@ class DriverCard extends StatelessWidget { 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)), + 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, @@ -673,21 +768,35 @@ class DriverCard extends StatelessWidget { 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)), + 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, ), - Text.rich( + 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)), + 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), @@ -697,19 +806,25 @@ class DriverCard extends StatelessWidget { OutlinedButton( style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF939495)), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), ), onPressed: () {}, - child: Text("View Schedule", style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w400)), + 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)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), ), onPressed: () {}, - child: Text("Assign", style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w400)), + child: Text("Assign", + style: fontTextStyle( + 12, const Color(0xFFFFFFFF), FontWeight.w400)), ), ], ) @@ -729,6 +844,7 @@ class _LabeledField extends StatelessWidget { if (i == -1) return input; return input.replaceRange(i, i + 1, input[i].toUpperCase()); } + @override Widget build(BuildContext context) { return Padding( diff --git a/lib/resources/resources_fleet.dart b/lib/resources/resources_fleet.dart index 24a5c6c..62fe7f4 100644 --- a/lib/resources/resources_fleet.dart +++ b/lib/resources/resources_fleet.dart @@ -560,6 +560,20 @@ class _ResourcesFleetScreenState extends State { 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: 10), @@ -587,7 +601,7 @@ class _ResourcesFleetScreenState extends State { ), ); }, - ), + )), ), ], ), diff --git a/lib/resources/sourcelocation_details.dart b/lib/resources/sourcelocation_details.dart index 6420b3b..5374ddc 100644 --- a/lib/resources/sourcelocation_details.dart +++ b/lib/resources/sourcelocation_details.dart @@ -19,6 +19,11 @@ class _SourceDetailsScreenState extends State { {"type": "Drinking Water", "liters": "10,000 L", "time": "7:02 PM", "date": "28 Jun 2025"}, ]; + 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 + void _showMenu() { showModalBottomSheet( context: context, @@ -55,57 +60,201 @@ class _SourceDetailsScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( + return WillPopScope( + onWillPop: () async { + // βœ… Return true to indicate successful tanker update refresh + Navigator.pop(context, true); + return false; // prevent default pop since we manually handled it + }, + child:Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black), - onPressed: () => Navigator.pop(context), - ), - title: Text( widget.sourceDetails.source_name.isNotEmpty - ? widget.sourceDetails.source_name[0].toUpperCase() + - widget.sourceDetails.source_name.substring(1) - : '', - style: fontTextStyle(16, Colors.black, FontWeight.w600)), - actions: [ - TextButton( - onPressed: () {}, - child: Text("HELP", - style: fontTextStyle(12, const Color(0xFF8270DB), FontWeight.w600)), - ), - ], - ), + appBar:AppSettings.supplierAppBarWithActionsText( widget.sourceDetails.source_name.isNotEmpty + ? widget.sourceDetails.source_name[0].toUpperCase() + + widget.sourceDetails.source_name.substring(1) + : '', context), body: SingleChildScrollView( - child: Column( + child: + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Image + + SizedBox(height:MediaQuery.of(context).size.height * .1,), + // πŸ‘€ Driver Profile Card Stack( + clipBehavior: Clip.none, children: [ - ClipRRect( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(8), - bottomRight: Radius.circular(8)), - child: Image.asset( - 'images/tanker_image.jpeg', // Replace with your image - height: 200, - width: double.infinity, - fit: BoxFit.cover, + // 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: [ + const SizedBox(height: 6), + + Text( + widget.sourceDetails.source_name, + style: fontTextStyle( + 20, Color(0XFF2D2E30), FontWeight.w500), + ), + const SizedBox(height: 2), + + Text( + "+91 "+widget.sourceDetails.phone, + style: fontTextStyle( + 12, Color(0XFF646566), FontWeight.w400), + ), + const SizedBox(height: 2), + + Text( + "+91 "+widget.sourceDetails.phone, + 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), + ], ), ), + + // Floating avatar (top-left, overlapping the card) Positioned( - top: 8, - right: 8, - child: IconButton( - icon: const Icon(Icons.more_vert, color: Colors.white), - onPressed: _showMenu, + 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: 12), Padding( @@ -229,9 +378,6 @@ class _SourceDetailsScreenState extends State { }, ), const SizedBox(height: 20), - ], - ), - ), - ); + ],)))); } } diff --git a/lib/resources/tanker_details.dart b/lib/resources/tanker_details.dart index a5fa0b3..392f5ab 100644 --- a/lib/resources/tanker_details.dart +++ b/lib/resources/tanker_details.dart @@ -73,6 +73,295 @@ class _TankerDetailsPageState extends State { String search = ''; bool isLoading = false; + bool isStatusLoading = false; + String? currentAvailability; + + @override + void initState() { + super.initState(); + _nameCtrl.text=widget.tankerDetails.tanker_name ?? ''; + // βœ… If the tanker already has availability from backend, prepare it + if (widget.tankerDetails.availability != null && + widget.tankerDetails.availability is List && + widget.tankerDetails.availability.length == 2) { + final a = widget.tankerDetails.availability; + currentAvailability = "${a[0]}|${a[1]}"; + + // βœ… Automatically show dropdown bottom sheet after short delay + /*WidgetsBinding.instance.addPostFrameCallback((_) { + _showDropdownBottomSheet(defaultValue: currentAvailability); + });*/ + } + } + + // Dropdown options: fill + availability combined + final List options = [ + "empty|available", + "empty|blocked", + "empty|undermaintanence", + "filled|available", + "filled|blocked", + "filled|undermaintanence", + ]; + + String _getLabel(String value) { + final parts = value.split('|'); + return "${parts[0].toUpperCase()} β€’ ${parts[1].toUpperCase()}"; + } + + Future _updateAvailability(String selectedValue) async { + // πŸ”Ή Split the combined dropdown value like "filled|available" + final parts = selectedValue.split('|'); + final availability = [parts[0], parts[1]]; + + // πŸ”Ή Build payload for updateTanker API + final payload = { + "tankerName": widget.tankerDetails.tanker_name, + "availability": availability, // βœ… send array as backend expects + }; + + try { + setState(() => isStatusLoading = true); + + // πŸ”Ή Reuse your existing update API + final bool tankStatus = await AppSettings.updateTankerAvailability(payload); + + if (!mounted) return; + + if (tankStatus) { + AppSettings.longSuccessToast("Tanker status updated successfully"); + + // Close sheet and refresh details + Navigator.pop(context, true); + await _refreshTankerDetails(); + } else { + Navigator.pop(context, true); + AppSettings.longFailedToast("Tanker status update failed"); + } + } catch (e) { + debugPrint("⚠️ update tanker error: $e"); + if (!mounted) return; + AppSettings.longFailedToast("Something went wrong: $e"); + } finally { + if (mounted) setState(() => isStatusLoading = false); + } + } + + void _showDropdownBottomSheet({String? defaultValue}) { + String? selectedValue = defaultValue; // preselect from backend + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, // nice rounded corners + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) { + final viewInsets = MediaQuery.of(context).viewInsets.bottom; + + return FractionallySizedBox( + heightFactor: 0.6, // ⬅️ increase/decrease height here (e.g., 0.7 / 0.8) + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + padding: EdgeInsets.fromLTRB(20, 16, 20, 16 + viewInsets), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // drag handle + Center( + child: Container( + width: 60, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: const Color(0xFFE0E0E0), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + const Text( + "Change Tanker Status", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 20), + + // Make central content scrollable within fixed height + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + DropdownButtonFormField( + value: selectedValue, + decoration: InputDecoration( + labelText: "Select Availability", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 12, + ), + ), + items: options + .map((opt) => DropdownMenuItem( + value: opt, + child: Text(_getLabel(opt)), + )) + .toList(), + onChanged: (val) => selectedValue = val, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + + // Sticky submit at bottom + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF515253), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + onPressed: () { + if (selectedValue != null) { + _updateAvailability(selectedValue!); + } else { + AppSettings.longFailedToast("Please select an option"); + } + }, + child: isLoading + ? const SizedBox( + width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Text("Submit"), + ), + ), + ], + ), + ), + ); + }, + ); + } + + showDeleteTankerDialog(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 Tanker?' ,style: fontTextStyle(16,Color(0XFF3B3B3B),FontWeight.w600),), + ), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + child: Text('Do u want to delete "${widget.tankerDetails.tanker_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.deleteTanker(widget.tankerDetails.tanker_name,); + if(status){ + AppSettings.longSuccessToast('Tanker deleted successfully'); + Navigator.of(context).pop(true); + Navigator.of(context).pop(true); + } + else{ + Navigator.of(context).pop(true); + AppSettings.longFailedToast('Failed to delete tanker'); + } + + + }, + 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)), + ), + ), + ) + ),) + + + ], + ), + ), + ], + ); + }); + }, + ); + } + Color _chipColor(String s) { switch (s) { case 'filled': @@ -596,9 +885,7 @@ class _TankerDetailsPageState extends State { const SnackBar(content: Text('Disable selected')), ); } else if (value == 'delete') { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Delete selected')), - ); + showDeleteTankerDialog(context); } }, itemBuilder: (context) => [ @@ -682,7 +969,7 @@ class _TankerDetailsPageState extends State { // Or: side: const BorderSide(color: Color(0xFFC3C4C4), width: 1.2), ), onPressed: () async { - // _showAssignTankerBottomSheet(); + _showDropdownBottomSheet(defaultValue: currentAvailability); }, child: Text( "Change Status",