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.

905 lines
38 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
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
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<FormState> _formKey = GlobalKey<FormState>();
// 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<String> _statusOptions = const [
'available',
'on delivery',
'offline'
];
String? selectedLicense;
final List<String> licenseNumbers = const [
'DL-042019-9876543',
'DL-052020-1234567',
'DL-072021-7654321',
];
String? selectedExperience; // years as string
final List<String> yearOptions =
List<String>.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<String> 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<void> _refreshDriverDetails() async {
try {
setState(() => isLoading = true);
final updatedDetails = await AppSettings.getDriverDetailsByPhone(
_mobileCtrl.text.trim(),
);
if (updatedDetails != null) {
setState(() {
widget.driverDetails = updatedDetails;
});
} else {
AppSettings.longFailedToast("Failed to fetch updated driver details");
}
} catch (e) {
debugPrint("⚠️ Error refreshing driver details: $e");
AppSettings.longFailedToast("Error refreshing driver details");
} finally {
setState(() => isLoading = false);
}
}
Future<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(),
"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
_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 ?? '';
_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);
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<String>(
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<String>(
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<String>(
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: <Widget>[
Container(
child: Text('Do u want to delete "${widget.driverDetails.driver_name}"',style: fontTextStyle(14,Color(0XFF101214),FontWeight.w600),),
),
],
),
),
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,
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 {
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.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),
),
],
),
),
],
)
],
),
const SizedBox(height: 8),
// buttons row
OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: Color(0XFF515253),
backgroundColor: Color(0xFFF3F1FB),
side: const BorderSide(
color: Color(0xFF939495),
width: 0.5,
),
padding: EdgeInsets.symmetric(vertical: 10),
// uniform height
),
onPressed: () {
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Change Status",
style: fontTextStyle(
14, const Color(0XFF515253), FontWeight.w500),
),
],
),
),
const SizedBox(height: 24),
// 🪪 License Card
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.black,
),
child: Stack(
children: [
// background pattern
/*Image.asset(
'images/license_bg.png',
fit: BoxFit.cover,
width: double.infinity,
height: 140,
),*/
Container(
height: 140,
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.black.withOpacity(0.3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"DRIVING LICENSE",
style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w400),
),
Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.driverDetails.driver_name,
style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500),
),
Text(
widget.driverDetails.license_number,
style: fontTextStyle(12, const Color(0xFFFFFFFF), FontWeight.w500),
),
],
),
Align(
alignment: Alignment.bottomRight,
child: Text(
"Expires on 29/02/2028",
style: fontTextStyle(10, const Color(0xFFFFFFFF), FontWeight.w300),
),
),
],
)
],
),
),
],
),
),
),
],
),
),
// Floating avatar (top-left, overlapping the card)
Positioned(
top: -_avatarOverlap,
left: _cardHPad,
child: ClipRRect(
borderRadius: BorderRadius.circular(_avSize / 2),
child: Image.asset(
'images/avatar.png',
width: _avSize,
height: _avSize,
fit: BoxFit.cover,
),
),
),
// Call icon aligned with the 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: 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"),
],
),)
],
),
),
),
));
}
Widget _buildTripCard(String title, String time) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Color(0XFFC3C4C4)),
),
child: Row(
children: [
Image.asset('images/recent_trips.png', width: 28, height: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style:fontTextStyle(14, const Color(0xFF2D2E30), FontWeight.w500),
),
const SizedBox(height: 2),
Text(
time,
style:fontTextStyle(10, const Color(0xFF939495), FontWeight.w400),
),
],
),
),
],
),
);
}
}
class _LabeledField extends StatelessWidget {
final String label;
final Widget child;
const _LabeledField({required this.label, required this.child});
String _capFirstWord(String input) {
if (input.isEmpty) return input;
final i = input.indexOf(RegExp(r'\S'));
if (i == -1) return input;
return input.replaceRange(i, i + 1, input[i].toUpperCase());
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_capFirstWord(label),
style: fontTextStyle(12, const Color(0xFF515253), FontWeight.w600),
),
const SizedBox(height: 6),
child,
],
),
);
}
}