You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1856 lines
72 KiB

1 month ago
import 'dart:convert';
1 month ago
import 'dart:io';
1 month ago
6 months ago
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
1 month ago
import 'package:image_picker/image_picker.dart';
1 month ago
import 'package:supplier_new/resources/driver_trips_model.dart';
import 'package:supplier_new/resources/resources_drivers.dart';
import '../common/settings.dart';
1 month ago
import 'package:http/http.dart' as http;
6 months ago
class DriverDetailsPage extends StatefulWidget {
var driverDetails;
var status;
DriverDetailsPage({this.driverDetails, this.status});
@override
State<DriverDetailsPage> createState() => _DriverDetailsPageState();
}
class _DriverDetailsPageState extends State<DriverDetailsPage> {
// tweak if you want a different size/offset
1 month ago
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();
1 month ago
final _ageCtrl = TextEditingController();
final _altMobileCtrl = TextEditingController();
final _locationCtrl = TextEditingController();
1 month ago
final _licenseController = TextEditingController();
final _experienceController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
// Unused in UI but kept if you later need them
final _commissionCtrl = TextEditingController();
final _joinDateCtrl = TextEditingController();
1 month ago
bool isLoading = false;
1 month ago
List<DriverTripsModel> driverTripsList = [];
bool isTripsLoading = false;
// Dropdown state
String? _status; // 'available' | 'on delivery' | 'offline'
final List<String> _statusOptions = const [
1 month ago
'Available',
'On delivery',
'Offline'
];
1 month ago
/*String? selectedLicense;
final List<String> licenseNumbers = const [
'DL-042019-9876543',
'DL-052020-1234567',
'DL-072021-7654321',
];
1 month ago
*/
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;
}
1 month ago
final ImagePicker _picker = ImagePicker();
1 month ago
1 month ago
XFile? newProfileImage;
1 month ago
1 month ago
List<XFile> newLicenseImages = [];
List<String> tankerImageUrls = [];
final int maxImages = 2;
@override
void initState() {
1 month ago
super.initState();
1 month ago
if(widget.driverDetails.licenseImages != null){
1 month ago
1 month ago
tankerImageUrls =
List<String>.from(
widget.driverDetails.licenseImages ?? []
);
1 month ago
1 month ago
}
_fetchDriverTrips();
1 month ago
}
Future<void> _fetchDriverTrips() async {
setState(() => isTripsLoading = true);
1 month ago
try {
1 month ago
String response =
1 month ago
await AppSettings.getDriverTrips(widget.driverDetails.phone_number);
1 month ago
1 month ago
final data = (jsonDecode(response)['data'] as List)
.map((e) => DriverTripsModel.fromJson(e))
1 month ago
.toList();
setState(() {
driverTripsList = data;
isTripsLoading = false;
});
1 month ago
} catch (e) {
1 month ago
print(e);
1 month ago
setState(() => isTripsLoading = false);
1 month ago
}
}
String? fitToOption(String? incoming, List<String> options) {
if (incoming == null) return null;
final inc = incoming.trim();
if (inc.isEmpty) return null;
final match = options.firstWhere(
1 month ago
(o) => o.toLowerCase() == inc.toLowerCase(),
orElse: () => '',
);
return match.isEmpty ? null : match; // return the exact option string
}
1 month ago
Future<void> _refreshDriverDetails({bool showToast = true}) async {
try{
if(!mounted) return;
setState(() => isLoading = true);
1 month ago
final updatedDetails =
await AppSettings.getDriverDetailsByPhone(
widget.driverDetails.phone_number /// ⭐ FIXED
);
1 month ago
if(!mounted) return;
if(updatedDetails != null){
setState((){
widget.driverDetails = updatedDetails;
1 month ago
/// ⭐ FIX IMAGE REFRESH BUG
tankerImageUrls =
List<String>.from(
updatedDetails.licenseImages ?? []
);
});
1 month ago
}
1 month ago
else{
if(showToast){
AppSettings.longFailedToast(
"Failed to fetch updated driver details");
}
}
}
catch(e){
if(showToast){
AppSettings.longFailedToast(
"Error refreshing driver details");
}
}
finally{
if(mounted){
setState(() => isLoading = false);
}
}
1 month ago
}
Future<void> _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 = <String, dynamic>{
"name": _nameCtrl.text.trim(),
1 month ago
"license_number": _licenseController.text ?? "",
"address": _locationCtrl.text.trim().isEmpty
? AppSettings.userAddress
: _locationCtrl.text.trim(),
"supplier_name": AppSettings.userName,
"phone": _mobileCtrl.text.trim(),
1 month ago
"age": _ageCtrl.text.trim(),
"alternativeContactNumber": _altMobileCtrl.text.trim(),
1 month ago
"years_of_experience": _experienceController.text ?? "",
"status": _status ?? "available",
};
try {
1 month ago
final bool created =
await AppSettings.updateDrivers(payload, _mobileCtrl.text);
if (!mounted) return;
if (created) {
AppSettings.longSuccessToast("Driver Updated successfully");
1 month ago
await uploadProfileImage();
await uploadUpdatedImages();
Navigator.pop(context, true); // close sheet
_refreshDriverDetails();
} else {
Navigator.pop(context, true);
AppSettings.longFailedToast("failed to update driver details");
}
} catch (e) {
debugPrint("⚠️ addDrivers error: $e");
if (!mounted) return;
AppSettings.longFailedToast("Something went wrong");
}
}
Future<void> _openDriverFormSheet(BuildContext context) async {
_nameCtrl.text = widget.driverDetails.driver_name ?? '';
_mobileCtrl.text = widget.driverDetails.phone_number ?? '';
1 month ago
_ageCtrl.text = widget.driverDetails.age ?? '';
_altMobileCtrl.text = widget.driverDetails.alt_phone_number ?? '';
_locationCtrl.text = widget.driverDetails.address ?? '';
1 month ago
_experienceController.text = widget.driverDetails.years_of_experience ?? '';
_licenseController.text = widget.driverDetails.license_number ?? '';
_status = fitToOption(widget.driverDetails.status, _statusOptions);
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent, // style inner container
builder: (context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return FractionallySizedBox(
heightFactor: 0.75, // fixed height (75% of screen)
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + bottomInset),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Handle + close (optional, simple close icon)
Row(
children: [
Expanded(
child: Center(
child: Container(
width: 86,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
),
_LabeledField(
label: "Driver Name *",
child: TextFormField(
controller: _nameCtrl,
validator: (v) => _required(v, field: "Driver Name"),
textCapitalization: TextCapitalization.none,
inputFormatters: const [
FirstCharUppercaseFormatter(), // << live first-letter caps
],
decoration: InputDecoration(
hintText: "Full Name",
hintStyle: fontTextStyle(
14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Driver License Number *",
1 month ago
child: TextFormField(
controller: _licenseController,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "Enter Driving License Number",
1 month ago
contentPadding: EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
),
1 month ago
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;
},
),
),
1 month ago
_LabeledField(
label: "Age *",
child: TextFormField(
controller: _ageCtrl,
3 weeks ago
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Driver Age required";
}
return null;
},
1 month ago
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: InputDecoration(
hintText: "Age Of driver",
hintStyle: fontTextStyle(
14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Years of Experience *",
1 month ago
child: TextFormField(
controller: _experienceController,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
inputFormatters: [
1 month ago
FilteringTextInputFormatter
.digitsOnly, // Only numbers
LengthLimitingTextInputFormatter(2), // Max 2 digits
1 month ago
],
decoration: const InputDecoration(
border: OutlineInputBorder(),
1 month ago
hintText: "Enter Years",
1 month ago
contentPadding: EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
),
1 month ago
style: fontTextStyle(
14, const Color(0xFF2A2A2A), FontWeight.w400),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Experience is required";
}
if (value.length > 2) {
return "Only 2 digits allowed";
}
return null;
},
),
),
_LabeledField(
label: "Phone Number *",
child: TextFormField(
controller: _mobileCtrl,
validator: (v) =>
_validatePhone(v, label: "Phone Number"),
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: InputDecoration(
hintText: "Mobile Number",
hintStyle: fontTextStyle(
14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Alternate Phone Number",
child: TextFormField(
controller: _altMobileCtrl,
validator: (v) {
if (v == null || v.trim().isEmpty)
return null; // optional
return _validatePhone(v,
label: "Alternate Phone Number");
},
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: InputDecoration(
hintText: "Mobile Number",
hintStyle: fontTextStyle(
14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Location *",
child: TextFormField(
controller: _locationCtrl,
validator: (v) => _required(v, field: "Location"),
textCapitalization: TextCapitalization.none,
inputFormatters: const [
FirstCharUppercaseFormatter(), // << live first-letter caps
],
decoration: InputDecoration(
hintText: "Area / locality",
hintStyle: fontTextStyle(
14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.done,
),
),
_LabeledField(
label: "Status *",
child: DropdownButtonFormField<String>(
value: _status,
items: _statusOptions
.map((s) =>
1 month ago
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),
),
),
1 month ago
onPressed: () {
_updateDriver();
},
child: Text(
"Update",
style: fontTextStyle(
14, Colors.white, FontWeight.w600),
),
),
),
],
),
),
6 months ago
),
),
),
);
},
);
}
showDeleteDriverDialog(BuildContext context) async {
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
1 month ago
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: <Widget>[
Container(
child: Text(
'Do u want to delete "${widget.driverDetails.driver_name}"',
style:
fontTextStyle(14, Color(0XFF101214), FontWeight.w600),
),
),
1 month ago
],
),
),
actions: <Widget>[
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: Color(0XFFFFFFFF),
border: Border.all(
width: 1, color: Color(0XFF1D7AFC)),
borderRadius: BorderRadius.circular(
12,
)),
alignment: Alignment.center,
child: Visibility(
visible: true,
child: Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Text('Cancel',
style: fontTextStyle(
12, Color(0XFF1D7AFC), FontWeight.w600)),
),
),
),
),
),
SizedBox(
width: MediaQuery.of(context).size.width * .016,
),
Expanded(
child: GestureDetector(
onTap: () async {
bool status = await AppSettings.deleteDriver(
widget.driverDetails.phone_number,
);
if (status) {
AppSettings.longSuccessToast(
'Driver deleted successfully');
Navigator.of(context).pop(true);
Navigator.of(context).pop(true);
} else {
Navigator.of(context).pop(true);
AppSettings.longFailedToast(
'Failed to delete driver');
}
},
child: Container(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
1 month ago
color: Color(0XFFE2483D),
border: Border.all(
1 month ago
width: 1, color: Color(0XFFE2483D)),
borderRadius: BorderRadius.circular(
12,
)),
alignment: Alignment.center,
child: Visibility(
visible: true,
child: Padding(
1 month ago
padding: EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Text('Delete',
style: fontTextStyle(12, Color(0XFFFFFFFF),
FontWeight.w600)),
),
),
1 month ago
)),
)
],
),
),
],
);
});
},
);
}
1 month ago
Future<void> pickProfileImage() async {
final XFile? image =
await _picker.pickImage(source: ImageSource.gallery, imageQuality: 70);
1 month ago
if (image != null) {
setState(() {
newProfileImage = image;
});
1 month ago
/// ⭐ UPLOAD IMMEDIATELY
await uploadProfileImage();
1 month ago
/// ⭐ REFRESH DATA
if (mounted) {
await _refreshDriverDetails(showToast: false);
}
1 month ago
1 month ago
AppSettings.longSuccessToast("Profile updated");
}
}
1 month ago
1 month ago
Future<void> pickNewImages() async {
int totalImages = tankerImageUrls.length + newLicenseImages.length;
1 month ago
1 month ago
if (totalImages >= maxImages) {
AppSettings.longFailedToast("Maximum 2 images allowed");
1 month ago
1 month ago
return;
}
1 month ago
1 month ago
showModalBottomSheet(
context: context,
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(Icons.photo),
title: Text("Gallery"),
onTap: () async {
Navigator.pop(context);
await _pickFromGallery();
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text("Camera"),
onTap: () async {
Navigator.pop(context);
await _pickFromCamera();
},
),
],
),
);
});
}
1 month ago
Future<void> _pickFromGallery() async {
final picker = ImagePicker();
1 month ago
final images = await picker.pickMultiImage();
1 month ago
if (images.isEmpty) return;
6 months ago
1 month ago
int remaining =
maxImages -
(tankerImageUrls.length + newLicenseImages.length);
1 month ago
if (images.length > remaining) {
AppSettings.longFailedToast("You can upload only $remaining more images");
1 month ago
newLicenseImages = images.take(remaining).toList();
} else {
newLicenseImages = images;
}
6 months ago
1 month ago
await uploadUpdatedImages();
1 month ago
if(mounted){
1 month ago
await _refreshDriverDetails(
showToast:false);
6 months ago
1 month ago
}
}
6 months ago
1 month ago
Future<void> _pickFromCamera() async {
if (tankerImageUrls.length >= maxImages) {
AppSettings.longFailedToast("Maximum 5 images allowed");
6 months ago
1 month ago
return;
}
1 month ago
1 month ago
final picker = ImagePicker();
1 month ago
1 month ago
final image = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 1280,
maxHeight: 1280,
);
1 month ago
1 month ago
if (image == null) return;
1 month ago
1 month ago
newLicenseImages = [image];
1 month ago
1 month ago
await uploadUpdatedImages();
1 month ago
1 month ago
if(mounted){
1 month ago
1 month ago
await _refreshDriverDetails(
showToast:false);
1 month ago
1 month ago
}
}
1 month ago
1 month ago
/*Future<void> pickLicenseImages() async {
final images = await _picker.pickMultiImage(imageQuality: 70);
1 month ago
1 month ago
if (images != null && images.isNotEmpty) {
setState(() {
newLicenseImages = images.take(2).toList();
});
1 month ago
1 month ago
/// ⭐ UPLOAD IMMEDIATELY
await uploadUpdatedImages();
1 month ago
1 month ago
/// ⭐ REFRESH DATA
if (mounted) {
await _refreshDriverDetails(showToast: false);
}
1 month ago
1 month ago
AppSettings.longSuccessToast("License updated");
}
}*/
1 month ago
1 month ago
Future<void> uploadProfileImage() async {
if (newProfileImage == null) return;
1 month ago
1 month ago
var request = http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host +
"uploads_delievry_profile/${widget.driverDetails.phone_number}",
),
);
1 month ago
1 month ago
request.headers.addAll(await AppSettings.buildRequestHeaders());
1 month ago
1 month ago
request.files.add(
await http.MultipartFile.fromPath(
'file',
newProfileImage!.path,
),
);
1 month ago
1 month ago
await request.send();
}
1 month ago
1 month ago
Future<void> uploadUpdatedImages() async {
1 month ago
1 month ago
try{
1 month ago
1 month ago
AppSettings.preLoaderDialog(context);
1 month ago
1 month ago
var request = http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host +
"uploads_delivery_liosence_images/${widget.driverDetails.phone_number}",
),
);
1 month ago
1 month ago
request.headers.addAll(
await AppSettings.buildRequestHeaders()
);
1 month ago
1 month ago
for(var img in newLicenseImages){
1 month ago
1 month ago
request.files.add(
6 months ago
1 month ago
await http.MultipartFile.fromPath(
6 months ago
1 month ago
'files',
1 month ago
1 month ago
img.path
1 month ago
1 month ago
)
1 month ago
1 month ago
);
1 month ago
1 month ago
}
1 month ago
1 month ago
var response =
await request.send();
1 month ago
1 month ago
Navigator.pop(context);
1 month ago
1 month ago
var resp =
await http.Response.fromStream(response);
1 month ago
1 month ago
if(response.statusCode==200){
1 month ago
1 month ago
AppSettings.longSuccessToast(
"Images Updated"
);
1 month ago
1 month ago
newLicenseImages.clear();
1 month ago
1 month ago
/// ⭐ FORCE REFRESH
await _refreshDriverDetails(showToast:false);
1 month ago
}
else{
1 month ago
1 month ago
print(resp.body);
1 month ago
1 month ago
AppSettings.longFailedToast(
"Upload failed"
);
1 month ago
1 month ago
}
1 month ago
1 month ago
}
catch(e){
1 month ago
1 month ago
Navigator.pop(context);
1 month ago
1 month ago
AppSettings.longFailedToast(
"Upload failed"
);
1 month ago
1 month ago
}
1 month ago
1 month ago
}
1 month ago
1 month ago
/* Future<void> uploadLicenseImages() async {
if (newLicenseImages.isEmpty) return;
1 month ago
1 month ago
var request = http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host +
"uploads_delivery_liosence_images/${widget.driverDetails.phone_number}",
),
);
1 month ago
1 month ago
request.headers.addAll(await AppSettings.buildRequestHeaders());
1 month ago
1 month ago
for (var img in newLicenseImages) {
request.files.add(
await http.MultipartFile.fromPath(
'files',
img.path,
),
);
}
1 month ago
1 month ago
await request.send();
1 month ago
1 month ago
if (mounted) {
await _refreshDriverDetails(showToast: false);
}
}*/
1 month ago
1 month ago
@override
Widget build(BuildContext context) {
final Map<String, Color> statusColors = {
'available': Color(0xFF0A9E04),
'on delivery': Color(0xFFD0AE3C),
'offline': Color(0xFF939495),
};
1 month ago
1 month ago
final statusColor =
statusColors[widget.driverDetails.status.toLowerCase().trim()] ??
Colors.grey;
1 month ago
1 month ago
return WillPopScope(
onWillPop: () async {
Navigator.pop(context, true);
return false; // prevent default pop since we manually handled it
},
child: Scaffold(
backgroundColor: Color(0XFFFFFFFF),
appBar: AppSettings.supplierAppBarWithActionsText(
widget.driverDetails.driver_name.isNotEmpty
? widget.driverDetails.driver_name[0].toUpperCase() +
widget.driverDetails.driver_name.substring(1)
: '',
context,
result: true // ⭐ ADD THIS
1 month ago
),
1 month ago
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: MediaQuery.of(context).size.height * .1,
),
// 👤 Driver Profile Card
Stack(
clipBehavior: Clip.none,
children: [
// Main card (give extra top padding so text starts below the avatar)
Container(
padding: EdgeInsets.fromLTRB(
_cardHPad,
_cardTPad +
_avSize -
_avatarOverlap, // space for avatar
_cardHPad,
16,
),
decoration: const BoxDecoration(
color: Color(0xFFF3F1FB),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// status chip (just under avatar, aligned left)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(4),
border:
Border.all(color: statusColor),
),
child: Text(widget.driverDetails.status,
style: fontTextStyle(10,
statusColor, FontWeight.w400)),
),
const SizedBox(height: 6),
Text(
widget.driverDetails.driver_name,
style: fontTextStyle(20,
Color(0XFF2D2E30), FontWeight.w500),
),
const SizedBox(height: 2),
Text(
"+91 " +
widget.driverDetails.phone_number,
style: fontTextStyle(12,
Color(0XFF646566), FontWeight.w400),
),
],
),
),
PopupMenuButton<String>(
// 🔁 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),
),
],
),
),
],
)
],
),
1 month ago
1 month ago
const SizedBox(height: 8),
1 month ago
1 month ago
// buttons row
/* OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: Color(0XFF515253),
backgroundColor: Color(0xFFF3F1FB),
side: const BorderSide(
color: Color(0xFF939495),
width: 0.5,
),
padding: EdgeInsets.symmetric(vertical: 10),
// uniform height
),
onPressed: () {
1 month ago
1 month ago
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Change Status",
style: fontTextStyle(
14, const Color(0XFF515253), FontWeight.w500),
),
],
),
),
const SizedBox(height: 24),*/
1 month ago
1 month ago
// 🪪 License Card
/*ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GestureDetector(
onTap: pickLicenseImages,
1 month ago
1 month ago
/// tap to update images
1 month ago
1 month ago
child: Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.black,
),
child: Stack(
children: [
/// LICENSE IMAGE BACKGROUND
Container(
height: 140,
width: double.infinity,
decoration: BoxDecoration(
image:
/// PRIORITY 1 → NEW IMAGE
newLicenseImages.isNotEmpty
? DecorationImage(
image: FileImage(
File(newLicenseImages[0]
.path),
),
fit: BoxFit.cover,
)
/// PRIORITY 2 → API IMAGE
: widget.driverDetails
.licenseImages !=
null &&
widget
.driverDetails
.licenseImages
.isNotEmpty
? DecorationImage(
image: NetworkImage(
widget.driverDetails
.licenseImages[0],
),
fit: BoxFit.cover,
)
/// PRIORITY 3 → NO IMAGE
: null,
color: newLicenseImages.isEmpty &&
(widget.driverDetails
.licenseImages ==
null ||
widget
.driverDetails
.licenseImages
.isEmpty)
? Colors.black.withOpacity(0.3)
: null,
),
child: Container(
padding: const EdgeInsets.all(16),
/// DARK OVERLAY
decoration: newLicenseImages
.isNotEmpty ||
(widget.driverDetails
.licenseImages !=
null &&
widget
.driverDetails
.licenseImages
.isNotEmpty)
? BoxDecoration(
color: Colors.black
.withOpacity(0.45),
)
: null,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text(
"DRIVING LICENSE",
style: fontTextStyle(
10,
const Color(0xFFFFFFFF),
FontWeight.w400),
),
/// CAMERA ICON
Container(
padding:
EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white
.withOpacity(.9),
shape: BoxShape.circle,
),
child: Center(
child: Image.asset(
'images/edit.png',
width: 14,
height: 14,
fit: BoxFit.contain,
color: primaryColor,
),
))
],
),
Spacer(),
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
widget.driverDetails
.driver_name,
style: fontTextStyle(
12,
const Color(
0xFFFFFFFF),
FontWeight.w500),
),
SizedBox(height: 2),
Text(
widget.driverDetails
.license_number,
style: fontTextStyle(
12,
const Color(
0xFFFFFFFF),
FontWeight.w500),
),
],
),
],
)
],
),
),
),
],
),
),
),
),*/
1 month ago
1 month ago
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
1 month ago
1 month ago
Text(
"Images (${tankerImageUrls.length}/2)",
style: fontTextStyle(
12,
Color(0XFF515253),
FontWeight.w600
),
),
1 month ago
1 month ago
Text(
"Max 2 allowed",
style: fontTextStyle(
10,
Color(0XFF939495),
FontWeight.w400
),
),
1 month ago
1 month ago
],
),
1 month ago
1 month ago
SizedBox(height:8),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
1 month ago
1 month ago
height:180,
1 month ago
1 month ago
child:tankerImageUrls.isEmpty
1 month ago
1 month ago
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.black,
),
child: Stack(
children: [
// background pattern
/*Image.asset(
'images/license_bg.png',
fit: BoxFit.cover,
width: double.infinity,
height: 140,
),*/
Container(
height: 140,
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.black.withOpacity(0.3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"DRIVING LICENSE",
style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w400),
),
Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.driverDetails.driver_name,
style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500),
),
Text(
widget.driverDetails.license_number,
style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500),
),
],
),
/*Align(
alignment: Alignment.bottomRight,
child: Text(
"Expires on 29/02/2028",
style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w300),
),
),*/
],
)
],
),
),
],
),
),
)
1 month ago
1 month ago
: ListView.builder(
1 month ago
1 month ago
scrollDirection:Axis.horizontal,
1 month ago
1 month ago
itemCount:tankerImageUrls.length,
1 month ago
1 month ago
itemBuilder:(context,index){
1 month ago
1 month ago
return Padding(
1 month ago
1 month ago
padding:EdgeInsets.only(right:8),
1 month ago
1 month ago
child:ClipRRect(
1 month ago
1 month ago
borderRadius:
BorderRadius.circular(12),
1 month ago
1 month ago
child:Stack(
1 month ago
1 month ago
children:[
1 month ago
1 month ago
Image.network(
1 month ago
1 month ago
tankerImageUrls[index],
1 month ago
1 month ago
width:250,
height:180,
1 month ago
1 month ago
fit:BoxFit.cover,
1 month ago
1 month ago
errorBuilder:(context,error,stack){
1 month ago
1 month ago
return Container(
1 month ago
1 month ago
width:250,
height:180,
1 month ago
1 month ago
color:Colors.grey[300],
1 month ago
1 month ago
child:Icon(
Icons.image_not_supported,
size:40,
color:Colors.grey,
),
1 month ago
1 month ago
);
},
loadingBuilder:(context,child,progress){
if(progress == null)
return child;
return Container(
width:250,
height:180,
alignment:Alignment.center,
child:CircularProgressIndicator(),
);
},
),
/// DELETE BUTTON
Positioned(
top:6,
right:6,
child:GestureDetector(
onTap:(){
showDialog(
context:context,
builder:(context){
return AlertDialog(
title:Text("Delete Image?"),
actions:[
TextButton(
onPressed:(){
Navigator.pop(context);
},
child:Text("Cancel")
),
TextButton(
onPressed:(){
Navigator.pop(context);
1 month ago
1 month ago
//deleteImage(index);
},
child:Text("Delete")
)
],
);
}
);
},
child:Container(
decoration:BoxDecoration(
color:Colors.white,
shape:BoxShape.circle
),
padding:EdgeInsets.all(6),
child:Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
),
padding: EdgeInsets.all(6),
child: Image.asset(
'images/delete.png',
width: 18,
height: 18,
colorBlendMode: BlendMode.srcIn,
),
),
),
),
),
],
),
),
);
},
),
),
),
SizedBox(height:8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF8270DB),
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 44),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
1 month ago
1 month ago
onPressed: pickNewImages,
child: Text(
"Update License Images",
style: fontTextStyle(14, Colors.white, FontWeight.w600),
),
)
],
),
),
// Floating avatar (top-left, overlapping the card)
Positioned(
top: -_avatarOverlap,
left: _cardHPad,
child: GestureDetector(
onTap: pickProfileImage,
child: Stack(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(_avSize / 2),
child: newProfileImage != null
? Image.file(
File(newProfileImage!.path),
width: _avSize,
height: _avSize,
fit: BoxFit.cover,
)
: widget.driverDetails.picture != null &&
widget.driverDetails.picture
.isNotEmpty
? Image.network(
widget.driverDetails.picture,
width: _avSize,
height: _avSize,
fit: BoxFit.cover,
)
: Image.asset(
'images/avatar.png',
width: _avSize,
height: _avSize,
fit: BoxFit.cover,
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: Image.asset(
'images/edit.png',
width: 14,
height: 14,
fit: BoxFit.contain,
color: primaryColor,
),
)),
)
],
),
),
),
// Call icon aligned with the cards top-right
/*Positioned(
top: _cardTPad - _avatarOverlap + 4, // aligns near chip row
right: _cardHPad - 4,
child: IconButton(
onPressed: () {
// 📞 Call action
},
icon: const Icon(Icons.call, color: Color(0xFF4F46E5)),
),
),*/
],
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"RECENT TRIPS",
style: fontTextStyle(
10, const Color(0xFF343637), FontWeight.w600),
),
const SizedBox(height: 12),
isTripsLoading
? const Center(child: CircularProgressIndicator())
: driverTripsList.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12),
child: Text(
"No trips found",
style: fontTextStyle(
12,
const Color(0xFF939495),
FontWeight.w500),
),
),
)
: ListView.separated(
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemCount: driverTripsList.length,
separatorBuilder: (_, __) =>
const SizedBox(height: 10),
itemBuilder: (context, index) {
final trip = driverTripsList[index];
return _buildTripCard(trip);
},
)
],
),
),
],
1 month ago
),
1 month ago
),
),
));
}
1 month ago
1 month ago
Widget _buildTripCard(DriverTripsModel trip) {
Color statusColor = trip.status == "delivered"
? Colors.green
: trip.status == "cancelled"
? Colors.red
: Colors.orange;
1 month ago
1 month ago
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0XFFC3C4C4)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.asset('images/recent_trips.png', width: 28, height: 28),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
trip.tankerName,
style: fontTextStyle(
14, const Color(0xFF2D2E30), FontWeight.w500),
),
Text(
trip.customerName,
style: fontTextStyle(
11, const Color(0xFF646566), FontWeight.w400),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(color: statusColor)),
child: Text(
trip.status,
style: fontTextStyle(10, statusColor, FontWeight.w400),
),
)
],
),
const SizedBox(height: 6),
Text(
"${trip.date}${trip.time}",
style: fontTextStyle(11, const Color(0xFF939495), FontWeight.w400),
),
const SizedBox(height: 4),
Text(
trip.address,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: fontTextStyle(11, const Color(0xFF646566), FontWeight.w400),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${trip.price}",
style:
fontTextStyle(13, const Color(0xFF2D2E30), FontWeight.w600),
),
Text(
"Paid ₹${trip.amountPaid}",
style: fontTextStyle(11, Colors.green, FontWeight.w500),
),
],
1 month ago
)
6 months ago
],
),
);
}
}
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,
],
),
);
}
}