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.

847 lines
35 KiB

2 months ago
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:supplier_new/resources/source_loctaions_model.dart';
import '../common/first_char_upper.dart';
2 months ago
import '../common/settings.dart';
import '../google_maps_place_picker_mb/src/models/pick_result.dart';
import 'dart:io' show Platform;
import 'package:flutter/services.dart';
import '../common/keys.dart';
import '../google_maps_place_picker_mb/src/place_picker.dart';
import 'package:supplier_new/google_maps_place_picker_mb/google_maps_place_picker.dart';
import 'dart:io' show File, Platform;
import 'package:google_maps_flutter_android/google_maps_flutter_android.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:location/location.dart' as locationmap;
2 months ago
class SourceDetailsScreen extends StatefulWidget {
var sourceDetails;
var status;
SourceDetailsScreen({this.sourceDetails, this.status});
@override
State<SourceDetailsScreen> createState() => _SourceDetailsScreenState();
}
PickResult? selectedPlace;
bool _mapsInitialized = false;
final String _mapsRenderer = "latest";
var kInitialPosition = const LatLng(15.462477, 78.717401);
locationmap.Location location = locationmap.Location();
final GoogleMapsFlutterPlatform mapsImplementation =
GoogleMapsFlutterPlatform.instance;
void initRenderer() {
if (_mapsInitialized) return;
if (mapsImplementation is GoogleMapsFlutterAndroid) {
switch (_mapsRenderer) {
case "legacy":
(mapsImplementation as GoogleMapsFlutterAndroid)
.initializeWithRenderer(AndroidMapRenderer.legacy);
break;
case "latest":
(mapsImplementation as GoogleMapsFlutterAndroid)
.initializeWithRenderer(AndroidMapRenderer.latest);
break;
}
}
// setState(() {
// _mapsInitialized = true;
// });
}
2 months ago
class _SourceDetailsScreenState extends State<SourceDetailsScreen> {
final List<Map<String, dynamic>> recentTrips = [
{"type": "Drinking Water", "liters": "10,000 L", "time": "7:02 PM", "date": "28 Jun 2025"},
{"type": "Drinking Water", "liters": "10,000 L", "time": "7:02 PM", "date": "28 Jun 2025"},
{"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
String search = '';
bool isLoading = false;
List<SourceLocationsModel> sourceLocationsList = [];
// ---------- Form state (bottom sheet) ----------
final _formKey = GlobalKey<FormState>();
final _locationNameController = TextEditingController();
final _mobileCtrl = TextEditingController();
bool addBusinessAsSource = false; // toggle if you want to hide map input
String? selectedWaterType;
final List<String> waterTypes = const ['Drinking water', 'Bore water', 'Both'];
double? lat;
double? lng;
String? address;
@override
void initState() {
// TODO: implement initState
super.initState();
lat=widget.sourceDetails.latitude;
lng=widget.sourceDetails.longitude;
}
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> _refreshSourceDetails() async {
try {
setState(() => isLoading = true);
final updatedDetails = await AppSettings.getSourceDetailsById(
widget.sourceDetails.dbId,
);
if (updatedDetails != null) {
setState(() {
widget.sourceDetails = 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> _updateSourceLocation() async {
// run all validators, including the dropdowns
final ok = _formKey.currentState?.validate() ?? false;
if (!ok) {
setState(() {}); // ensure error texts render
return;
}
final payload = <String, dynamic>{
"location_name": _locationNameController.text.trim(),
"phone": _mobileCtrl.text.trim(),
"water_type": selectedWaterType,
"status": 'active', // or whatever string your backend expects
"address": address,
"city": '',
"state": '',
"zip": '',
"latitude": lat,
"longitude": lng,
};
try {
final bool created = await AppSettings.updateSourceLocations(payload, widget.sourceDetails.dbId);
if (!mounted) return;
if (created) {
AppSettings.longSuccessToast("Source Updated successfully");
Navigator.pop(context, true); // close sheet
_refreshSourceDetails();
} else {
Navigator.pop(context, true);
AppSettings.longFailedToast("failed to update Source details");
}
} catch (e) {
debugPrint("⚠️ Source error: $e");
if (!mounted) return;
AppSettings.longFailedToast("Something went wrong");
}
}
Future<void> _openSourceFormSheet(BuildContext context)async {
if (widget.sourceDetails != null) {
_locationNameController.text = widget.sourceDetails.source_name ?? '';
_mobileCtrl.text = widget.sourceDetails.phone ?? '';
selectedWaterType = fitToOption(widget.sourceDetails.water_type, waterTypes);
address = widget.sourceDetails.address ?? '';
lat = widget.sourceDetails.latitude;
lng = widget.sourceDetails.longitude;
} else {
_locationNameController.clear();
_mobileCtrl.clear();
selectedWaterType = null;
address = null;
lat = null;
lng = null;
}
await showModalBottomSheet(
2 months ago
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
2 months ago
builder: (context) {
final kbInset = MediaQuery.of(context).viewInsets.bottom;
return FractionallySizedBox(
heightFactor: 0.75, // fixed height
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + kbInset),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: Center(
child: Container(
width: 86,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: const Color(0xFFE0E0E0),
borderRadius: BorderRadius.circular(2),
),
),
),
),
],
),
_LabeledField(
label: "Location Name *",
child: TextFormField(
controller: _locationNameController,
validator: (v) => _required(v, field: "Location Name"),
textCapitalization: TextCapitalization.none,
inputFormatters: const [
FirstCharUppercaseFormatter(), // << live first-letter caps
],
decoration: InputDecoration(
hintText: "Location Name",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Mobile Number *",
child: TextFormField(
controller: _mobileCtrl,
validator: (v) => _validatePhone(v, label: "Mobile Number"),
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: "Mobile Number",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
Visibility(
visible: !addBusinessAsSource,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
location.serviceEnabled().then((value) {
if (value) {
initRenderer();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return PlacePicker(
resizeToAvoidBottomInset: false,
hintText: "Find a place ...",
searchingText: "Please wait ...",
selectText: "Select place",
outsideOfPickAreaText: "Place not in area",
initialPosition: (lat != null && lng != null)
? LatLng(lat!, lng!)
: kInitialPosition,
useCurrentLocation: (lat == null || lng == null),
selectInitialPosition: true,
usePinPointingSearch: true,
usePlaceDetailSearch: true,
zoomGesturesEnabled: true,
zoomControlsEnabled: true,
onMapCreated: (GoogleMapController controller) {},
onPlacePicked: (PickResult result) {
setState(() {
selectedPlace = result;
lat = selectedPlace!.geometry!.location.lat;
lng = selectedPlace!.geometry!.location.lng;
if (selectedPlace!.types!.length == 1) {
address = selectedPlace!.formattedAddress!;
} else {
address =
'${selectedPlace!.name!}, ${selectedPlace!.formattedAddress!}';
}
Navigator.of(context).pop();
});
},
onMapTypeChanged: (MapType mapType) {},
apiKey: Platform.isAndroid
? APIKeys.androidApiKey
: APIKeys.iosApiKey,
forceAndroidLocationManager: true,
);
},
),
);
} else {
// same dialog code ...
}
});
},
label: Text(
"Change Location on map",
style: fontTextStyle(14, const Color(0xFF646566), FontWeight.w600),
),
),
),
const SizedBox(height: 12),
],
),
),
_LabeledField(
label: "Water Type *",
child: DropdownButtonFormField<String>(
value: selectedWaterType,
items: waterTypes
.map((w) => DropdownMenuItem(value: w, child: Text(w)))
.toList(),
onChanged: (v) => setState(() => selectedWaterType = v),
validator: (v) => v == null || v.isEmpty ? "Water Type is required" : null,
isExpanded: true,
alignment: Alignment.centerLeft,
hint: Text(
"Select Water Type",
style: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
),
icon: Image.asset('images/downarrow.png', width: 16, height: 16),
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: false,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
),
),
const SizedBox(height: 20),
// Actions
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
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: (){
_updateSourceLocation();
},
child: Text(
"Update",
style: fontTextStyle(14, Colors.white, FontWeight.w600),
),
),
),
],
),
],
),
),
2 months ago
),
),
2 months ago
),
);
},
);
}
showDeleteSourceDialog(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.sourceDetails.source_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.deleteSource(widget.sourceDetails.dbId,);
if(status){
AppSettings.longSuccessToast('Source deleted successfully');
Navigator.of(context).pop(true);
Navigator.of(context).pop(true);
}
else{
Navigator.of(context).pop(true);
AppSettings.longFailedToast('Failed to delete Source');
}
},
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)),
),
),
)
),)
],
),
),
],
);
});
},
);
}
2 months ago
@override
Widget build(BuildContext context) {
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(
2 months ago
backgroundColor: Colors.white,
appBar:AppSettings.supplierAppBarWithActionsText( widget.sourceDetails.source_name.isNotEmpty
? widget.sourceDetails.source_name[0].toUpperCase() +
widget.sourceDetails.source_name.substring(1)
: '', context),
2 months ago
body: SingleChildScrollView(
child:
Column(
2 months ago
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height:MediaQuery.of(context).size.height * .1,),
// 👤 Driver Profile Card
2 months ago
Stack(
clipBehavior: Clip.none,
2 months ago
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: [
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(
widget.sourceDetails.address,
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') {
_openSourceFormSheet(context);
} else if (value == 'disable') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Disable selected')),
);
} else if (value == 'delete') {
showDeleteSourceDialog(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),
],
2 months ago
),
),
// Floating avatar (top-left, overlapping the card)
2 months ago
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,
),
2 months ago
),
),
// 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)),
),
),*/
2 months ago
],
),
const SizedBox(height: 12),
// Filling & Wait time cards
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF7F7F7),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text("12",
style: fontTextStyle(20, Colors.black, FontWeight.w600)),
Text("sec/L",
style: fontTextStyle(14, Colors.black, FontWeight.w400)),
const SizedBox(height: 4),
Text("Filling Time",
style: fontTextStyle(12, Colors.grey, FontWeight.w400)),
],
),
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF7F7F7),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text("24",
style: fontTextStyle(20, Colors.black, FontWeight.w600)),
Text("min",
style: fontTextStyle(14, Colors.black, FontWeight.w400)),
const SizedBox(height: 4),
Text("Wait Time",
style: fontTextStyle(12, Colors.grey, FontWeight.w400)),
],
),
),
),
],
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text("Recent Trips",
style: fontTextStyle(16, Colors.black, FontWeight.w600)),
),
const SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: recentTrips.length,
itemBuilder: (context, index) {
final trip = recentTrips[index];
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Color(0XFFC3C4C4)),
2 months ago
),
child: Row(
children: [
Image.asset('images/recent_trips.png', width: 28, height: 28),
2 months ago
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${trip['type']} - ${trip['liters']}",
style: fontTextStyle(
14, const Color(0xFF2D2E30), FontWeight.w500),
),
Text(
"${trip['time']}, ${trip['date']}",
style: fontTextStyle(
12, const Color(0xFF656565), FontWeight.w400),
),
],
)
2 months ago
),
2 months ago
],
),
),
);
},
),
const SizedBox(height: 20),
],))));
2 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,
],
),
);
}
}