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

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:google_maps_flutter/google_maps_flutter.dart';
import 'package:supplier_new/resources/source_loctaions_model.dart';
import '../common/first_char_upper.dart';
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;
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;
// });
}
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(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
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),
),
),
),
],
),
],
),
),
),
),
),
);
},
);
}
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)),
),
),
)
),)
],
),
),
],
);
});
},
);
}
@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(
backgroundColor: Colors.white,
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(
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: [
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),
],
),
),
// 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: 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)),
),
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(
"${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),
),
],
)
),
],
),
),
);
},
),
const SizedBox(height: 20),
],))));
}
}
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,
],
),
);
}
}