master
Sneha 1 month ago
parent 849afdd17a
commit c47bddf151

File diff suppressed because it is too large Load Diff

@ -661,6 +661,101 @@ class AppSettings{
} }
} }
static Future<Map?> addTankersWithResponse(payload) async {
var uri = Uri.parse(addTankerUrl + '/' + supplierId);
var response = await http.post(
uri,
body: json.encode(payload),
headers: await buildRequestHeaders()
);
if (response.statusCode == 200) {
try {
var _response = json.decode(response.body);
print(_response);
/// RETURN FULL DATA
return _response;
}
catch (e) {
return null;
}
}
else if (response.statusCode == 2083) {
AppSettings.longFailedToast(
'Tanker name already exists'
);
return null;
}
else if (response.statusCode == 401) {
bool status =
await AppSettings.resetToken();
if (status) {
response = await http.post(
uri,
body: json.encode(payload),
headers: await buildRequestHeaders()
);
if (response.statusCode == 200) {
var _response =
json.decode(response.body);
return _response["data"];
}
else {
return null;
}
}
else {
return null;
}
}
else {
return null;
}
}
static Future<bool> updateTanker(payload,tankerName) async { static Future<bool> updateTanker(payload,tankerName) async {
var uri = Uri.parse(updateTankerUrl+'/'+supplierId); var uri = Uri.parse(updateTankerUrl+'/'+supplierId);
uri = uri.replace(query: 'tankerName=$tankerName'); uri = uri.replace(query: 'tankerName=$tankerName');
@ -1670,6 +1765,54 @@ class AppSettings{
return response.body; return response.body;
} }
static Future<bool> deleteTankerImage(
String tankerId,
String imageUrl
) async {
try{
var response = await http.delete(
Uri.parse(
AppSettings.host+
"delete_tanker_image/$tankerId"
),
headers:{
'Content-Type':'application/json'
},
body: jsonEncode({
"imageUrl": imageUrl
}),
);
print(response.body);
return response.statusCode==200;
}
catch(e){
print(e);
return false;
}
}
/*Apis ends here*/ /*Apis ends here*/
//save data local //save data local

@ -0,0 +1,37 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:supplier_new/common/settings.dart';
class AuthManager {
static final FlutterSecureStorage storage = FlutterSecureStorage(
aOptions: AndroidOptions(
resetOnError: true,
encryptedSharedPreferences: true,
),
);
static Future<String> decideStartScreen() async {
String? token = await storage.read(key: 'authToken');
if(token == null){
return "login";
}
await AppSettings.loadDataFromMemory();
String? onboarding = await storage.read(key: 'onboardingCompleted');
if(onboarding == null){
await storage.write(
key: 'onboardingCompleted',
value: 'true',
);
return "resources";
}
return "dashboard";
}
}

File diff suppressed because it is too large Load Diff

@ -10,7 +10,10 @@ class DriversModel {
String phone_number=''; String phone_number='';
String alt_phone_number=''; String alt_phone_number='';
String license_number=''; String license_number='';
String age='';
String license_expiry_date=''; String license_expiry_date='';
String picture='';
List<dynamic> licenseImages= [];
DriversModel(); DriversModel();
factory DriversModel.fromJson(Map<String, dynamic> json) { factory DriversModel.fromJson(Map<String, dynamic> json) {
@ -25,7 +28,10 @@ class DriversModel {
rtvm.years_of_experience = json['years_of_experience'] ?? ''; rtvm.years_of_experience = json['years_of_experience'] ?? '';
rtvm.license_number = json['license_number'] ?? ''; rtvm.license_number = json['license_number'] ?? '';
rtvm.deliveries = json['deliveries'] ?? ''; rtvm.deliveries = json['deliveries'] ?? '';
rtvm.picture = json['picture'] ?? '';
rtvm.license_expiry_date=json['license_number'] ?? ''; rtvm.license_expiry_date=json['license_number'] ?? '';
rtvm.age=json['age'] ?? '';
rtvm.licenseImages = json['images'] ?? [];
return rtvm; return rtvm;
} }
} }

@ -1,10 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:supplier_new/common/settings.dart'; import 'package:supplier_new/common/settings.dart';
import 'package:supplier_new/resources/driver_details.dart'; import 'package:supplier_new/resources/driver_details.dart';
import 'package:supplier_new/resources/drivers_model.dart'; import 'package:supplier_new/resources/drivers_model.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
class FirstCharUppercaseFormatter extends TextInputFormatter { class FirstCharUppercaseFormatter extends TextInputFormatter {
const FirstCharUppercaseFormatter(); const FirstCharUppercaseFormatter();
@ -55,6 +58,8 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
// Text controllers // Text controllers
final _nameCtrl = TextEditingController(); final _nameCtrl = TextEditingController();
final _mobileCtrl = TextEditingController(); final _mobileCtrl = TextEditingController();
final _ageCtrl = TextEditingController();
final _altMobileCtrl = TextEditingController(); final _altMobileCtrl = TextEditingController();
final _locationCtrl = TextEditingController(); final _locationCtrl = TextEditingController();
final _licenseController = TextEditingController(); final _licenseController = TextEditingController();
@ -276,6 +281,13 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
} }
final ImagePicker _picker = ImagePicker();
List<XFile> drivingLicenseImages = [];
final int maxImages = 2;
XFile? driverImage;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -286,6 +298,7 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
void dispose() { void dispose() {
_nameCtrl.dispose(); _nameCtrl.dispose();
_mobileCtrl.dispose(); _mobileCtrl.dispose();
_ageCtrl.dispose();
_altMobileCtrl.dispose(); _altMobileCtrl.dispose();
_locationCtrl.dispose(); _locationCtrl.dispose();
_commissionCtrl.dispose(); _commissionCtrl.dispose();
@ -297,10 +310,15 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
_formKey.currentState?.reset(); _formKey.currentState?.reset();
_nameCtrl.clear(); _nameCtrl.clear();
_mobileCtrl.clear(); _mobileCtrl.clear();
_ageCtrl.clear();
_altMobileCtrl.clear(); _altMobileCtrl.clear();
_locationCtrl.clear(); _locationCtrl.clear();
_commissionCtrl.clear(); _commissionCtrl.clear();
_joinDateCtrl.clear(); _joinDateCtrl.clear();
_licenseController.clear();
_experienceController.clear();
drivingLicenseImages.clear();
driverImage= null;
selectedLicense = null; selectedLicense = null;
selectedExperience = null; selectedExperience = null;
_status = null; _status = null;
@ -348,44 +366,306 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
// ---------- submit ---------- // ---------- submit ----------
Future<void> _addDriver() async { Future<void> _addDriver() async {
// run all validators, including the dropdowns
final ok = _formKey.currentState?.validate() ?? false; final ok = _formKey.currentState?.validate() ?? false;
if (!ok) { if (!ok) {
setState(() {}); // ensure error texts render setState(() {});
return; return;
} }
// Build payload (adjust keys to your API if needed)
final payload = <String, dynamic>{ final payload = <String, dynamic>{
"Name": _nameCtrl.text.trim(), "Name": _nameCtrl.text.trim(),
"license_number": _licenseController.text ?? "",
"license_number": _licenseController.text,
"address": _locationCtrl.text.trim().isEmpty "address": _locationCtrl.text.trim().isEmpty
? AppSettings.userAddress ? AppSettings.userAddress
: _locationCtrl.text.trim(), : _locationCtrl.text.trim(),
"supplier_name": AppSettings.userName, "supplier_name": AppSettings.userName,
"phone": _mobileCtrl.text.trim(), "phone": _mobileCtrl.text.trim(),
"age": _ageCtrl.text.trim(),
"alternativeContactNumber": _altMobileCtrl.text.trim(), "alternativeContactNumber": _altMobileCtrl.text.trim(),
"years_of_experience": _experienceController.text ?? "",
"years_of_experience": _experienceController.text,
"status": _status ?? "available", "status": _status ?? "available",
}; };
try{ try{
final bool created = await AppSettings.addDrivers(payload);
AppSettings.preLoaderDialog(context);
final bool created =
await AppSettings.addDrivers(payload);
if(!mounted) return; if(!mounted) return;
if(created){ if(created){
AppSettings.longSuccessToast("Driver created successfully");
Navigator.pop(context, true); // close sheet /// UPLOAD DRIVER IMAGE AFTER CREATE
if(driverImage != null){
await uploadDriverImage(
_mobileCtrl.text.trim().toString()
);
}
/// Upload license images
if(drivingLicenseImages.isNotEmpty){
await uploadLicenseImages(
_mobileCtrl.text.trim().toString()
);
}
Navigator.pop(context);
AppSettings.longSuccessToast(
"Driver created successfully"
);
Navigator.pop(context,true);
_resetForm(); _resetForm();
_fetchDrivers(); // refresh
_fetchDrivers();
}
else{
Navigator.pop(context);
AppSettings.longFailedToast(
"Driver creation failed"
);
}
}
catch(e){
Navigator.pop(context);
AppSettings.longFailedToast(
"Something went wrong"
);
}
}
Future<void> uploadDriverImage(
String phone
) async{
try{
var request =
http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host+
"uploads_delievry_profile/$phone"
)
);
/// headers if required
request.headers.addAll(
await AppSettings.buildRequestHeaders()
);
/// file field name usually "file"
request.files.add(
await http.MultipartFile.fromPath(
'file', /// check backend field name
driverImage!.path
)
);
var response =
await request.send();
var resp =
await http.Response.fromStream(response);
if(response.statusCode==200){
print("Driver image uploaded");
}
else{
print(resp.body);
}
}
catch(e){
print(e);
}
}
Future<void> uploadLicenseImages(String phone) async {
try {
var request = http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host +
"uploads_delivery_liosence_images/$phone"
),
);
request.headers.addAll(
await AppSettings.buildRequestHeaders()
);
/// MULTIPLE FILES
for (var image in drivingLicenseImages) {
request.files.add(
await http.MultipartFile.fromPath(
'files', /// IMPORTANT backend expects "files"
image.path,
),
);
}
var response = await request.send();
var resp = await http.Response.fromStream(response);
if(response.statusCode == 200){
print("License images uploaded");
}else{ }else{
AppSettings.longFailedToast("Driver creation failed");
print(resp.body);
} }
} catch (e) {
debugPrint("⚠️ addDrivers error: $e"); }
if (!mounted) return; catch(e){
AppSettings.longFailedToast("Something went wrong");
print("License upload error $e");
}
}
Future<void> pickTankerImages(Function modalSetState) async {
if(drivingLicenseImages.length>=2){
AppSettings.longFailedToast("Maximum 5 images allowed");
return;
}
final images =
await _picker.pickMultiImage(imageQuality:70);
if(images!=null){
int remaining = 2 - drivingLicenseImages.length;
drivingLicenseImages.addAll(
images.take(remaining)
);
modalSetState((){});
}
}
Future<void> pickFromCamera(Function modalSetState) async {
if(drivingLicenseImages.length>=2){
AppSettings.longFailedToast("Maximum 5 images allowed");
return;
}
final image =
await _picker.pickImage(
source:ImageSource.camera,
imageQuality:70,
);
if(image!=null){
drivingLicenseImages.add(image);
modalSetState((){});
}
}
Future<void> pickDriverImage(Function modalSetState) async {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality:70,
);
if(image != null){
driverImage = image;
modalSetState((){});
} }
}
Future<void> pickDriverImageCamera(Function modalSetState) async {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality:70,
);
if(image != null){
driverImage = image;
modalSetState((){});
}
} }
// ---------- bottom sheet ---------- // ---------- bottom sheet ----------
@ -395,6 +675,9 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, // style inner container backgroundColor: Colors.transparent, // style inner container
builder: (context) { builder: (context) {
return StatefulBuilder(
builder: (context, modalSetState) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom; final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return FractionallySizedBox( return FractionallySizedBox(
@ -476,6 +759,31 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
), ),
), ),
_LabeledField(
label: "Age *",
child: TextFormField(
controller: _ageCtrl,
keyboardType: TextInputType.number,
textCapitalization: TextCapitalization.characters,// create controller
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: "Enter Age Of Driver",
contentPadding:
EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
style: fontTextStyle(
14, const Color(0xFF2A2A2A), FontWeight.w400),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Driver Age required";
}
return null;
},
),
),
_LabeledField( _LabeledField(
label: "Years of Experience *", label: "Years of Experience *",
child: TextFormField( child: TextFormField(
@ -650,6 +958,319 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
), ),
), ),
), ),
_LabeledField(
label: "Driving License (Max 2)",
child: Column(
children: [
/// IMAGE GRID
if(drivingLicenseImages.isNotEmpty)
GridView.builder(
shrinkWrap:true,
physics:NeverScrollableScrollPhysics(),
itemCount:drivingLicenseImages.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:3,
crossAxisSpacing:8,
mainAxisSpacing:8,
),
itemBuilder:(context,index){
return Stack(
children:[
ClipRRect(
borderRadius:BorderRadius.circular(8),
child:Image.file(
File(drivingLicenseImages[index].path),
width:double.infinity,
height:double.infinity,
fit:BoxFit.cover,
),
),
Positioned(
right:4,
top:4,
child:GestureDetector(
onTap:(){
drivingLicenseImages.removeAt(index);
modalSetState((){}); /// FIXED
},
child:Container(
decoration:const BoxDecoration(
color:Colors.red,
shape:BoxShape.circle,
),
child:const Icon(
Icons.close,
color:Colors.white,
size:18,
),
),
),
)
],
);
},
),
const SizedBox(height:10),
/// BUTTONS
Row(
children:[
Expanded(
child:OutlinedButton.icon(
onPressed:(){
pickTankerImages(modalSetState); /// FIXED
},
icon:Icon(Icons.photo),
label:Text("Gallery"),
),
),
const SizedBox(width:10),
Expanded(
child:OutlinedButton.icon(
onPressed:(){
pickFromCamera(modalSetState); /// FIXED
},
icon:Icon(Icons.camera_alt),
label:Text("Camera"),
),
),
],
),
const SizedBox(height:5),
Text(
"${drivingLicenseImages.length}/2 images selected",
style: fontTextStyle(
12,
Colors.grey,
FontWeight.w400
),
),
],
),
),
_LabeledField(
label: "Driver Image",
child: Column(
children: [
/// SINGLE IMAGE
if(driverImage != null)
Stack(
children:[
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(driverImage!.path),
width:120,
height:120,
fit:BoxFit.cover,
),
),
Positioned(
right:4,
top:4,
child: GestureDetector(
onTap:(){
driverImage = null;
modalSetState((){});
},
child: Container(
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size:18,
),
),
),
)
],
),
const SizedBox(height:10),
/// BUTTONS
Row(
children:[
Expanded(
child:OutlinedButton.icon(
onPressed:(){
pickDriverImage(modalSetState);
},
icon:Icon(Icons.photo),
label:Text("Gallery"),
),
),
const SizedBox(width:10),
Expanded(
child:OutlinedButton.icon(
onPressed:(){
pickDriverImageCamera(modalSetState);
},
icon:Icon(Icons.camera_alt),
label:Text("Camera"),
),
),
],
),
const SizedBox(height:5),
Text(
driverImage==null
? "0/1 image selected"
: "1/1 image selected",
style: fontTextStyle(
12,
Colors.grey,
FontWeight.w400
),
),
],
),
),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -679,6 +1300,8 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
), ),
), ),
); );
});
}, },
); );
} }
@ -936,7 +1559,8 @@ class _ResourcesDriverScreenState extends State<ResourcesDriverScreen> {
location: d.address, location: d.address,
deliveries: int.tryParse(d.deliveries) ?? 0, deliveries: int.tryParse(d.deliveries) ?? 0,
commission: d.commision, commission: d.commision,
phone: d.phone_number, // ADD phone: d.phone_number,
picture: d.picture ?? '',// ADD
), ),
); );
}, },
@ -1001,6 +1625,7 @@ class DriverCard extends StatelessWidget {
final int deliveries; final int deliveries;
final String commission; final String commission;
final String phone; final String phone;
final String picture;
const DriverCard({ const DriverCard({
super.key, super.key,
@ -1010,6 +1635,7 @@ class DriverCard extends StatelessWidget {
required this.deliveries, required this.deliveries,
required this.commission, required this.commission,
required this.phone, required this.phone,
required this.picture,
}); });
@override @override
@ -1052,8 +1678,41 @@ class DriverCard extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
Image.asset("images/avatar.png", ClipRRect(
height: 36, width: 36), borderRadius: BorderRadius.circular(8),
child: picture.isNotEmpty
? Image.network(
picture,
height: 36,
width: 36,
fit: BoxFit.cover,
errorBuilder: (context,error,stackTrace){
return Image.asset(
"images/avatar.png",
height:36,
width:36,
);
},
)
: Image.asset(
"images/avatar.png",
height:36,
width:36,
),
),
const SizedBox(width: 10), const SizedBox(width: 10),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

@ -1,9 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:supplier_new/common/settings.dart'; import 'package:supplier_new/common/settings.dart';
import 'package:supplier_new/resources/tanker_details.dart'; import 'package:supplier_new/resources/tanker_details.dart';
import 'package:supplier_new/resources/tankers_model.dart'; import 'package:supplier_new/resources/tankers_model.dart';
import 'package:http/http.dart' as http;
class FirstCharUppercaseFormatter extends TextInputFormatter { class FirstCharUppercaseFormatter extends TextInputFormatter {
const FirstCharUppercaseFormatter(); const FirstCharUppercaseFormatter();
@ -93,6 +96,67 @@ class _ResourcesFleetScreenState extends State<ResourcesFleetScreen> {
"Capacity" "Capacity"
]; ];
final ImagePicker _picker = ImagePicker();
List<XFile> tankerImages = [];
final int maxImages = 5;
Future<void> pickTankerImages(Function modalSetState) async {
if(tankerImages.length>=5){
AppSettings.longFailedToast("Maximum 5 images allowed");
return;
}
final images =
await _picker.pickMultiImage(imageQuality:70);
if(images!=null){
int remaining = 5 - tankerImages.length;
tankerImages.addAll(
images.take(remaining)
);
modalSetState((){});
}
}
Future<void> pickFromCamera(Function modalSetState) async {
if(tankerImages.length>=5){
AppSettings.longFailedToast("Maximum 5 images allowed");
return;
}
final image =
await _picker.pickImage(
source:ImageSource.camera,
imageQuality:70,
);
if(image!=null){
tankerImages.add(image);
modalSetState((){});
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -392,29 +456,214 @@ class _ResourcesFleetScreenState extends State<ResourcesFleetScreen> {
} }
} }
Future<void> uploadTankerImages(
String tankerId
) async {
var request = http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host +
"uploads_tanker_images/$tankerId"
)
);
request.headers.addAll(
await AppSettings.buildRequestHeaders()
);
for(var img in tankerImages){
request.files.add(
await http.MultipartFile.fromPath(
'files',
img.path
)
);
}
var response =
await request.send();
if(response.statusCode != 200){
throw Exception(
"Upload failed"
);
}
}
Future<void> _addTankerNew() async {
/// FORM VALIDATION
final ok = _formKey.currentState?.validate() ?? false;
if (!ok) {
setState(() {});
return;
}
try {
AppSettings.preLoaderDialog(context);
/// STEP 1 CREATE TANKER
final payload = {
"tankerName": _nameCtrl.text.trim(),
"capacity": _capacityCtrl.text.trim(),
"typeofwater": selectedTypeOfWater ?? "",
"supplier_address": AppSettings.userAddress,
"supplier_name": AppSettings.userName,
"phoneNumber": AppSettings.phoneNumber,
"tanker_type": selectedType ?? "",
"license_plate": _plateCtrl.text.trim(),
"manufacturing_year": _mfgYearCtrl.text.trim(),
"insurance_exp_date": _insExpiryCtrl.text.trim(),
};
final response =
await AppSettings.addTankersWithResponse(payload);
if(response == null){
Navigator.pop(context);
AppSettings.longFailedToast(
"Tanker creation failed"
);
return;
}
/// IMPORTANT GET TANKER ID
String tankerId =
response["tankerId"] ??
response["_id"] ??
"";
/// STEP 2 UPLOAD IMAGES
if(tankerImages.isNotEmpty){
try{
await uploadTankerImages(tankerId);
}
catch(e){
debugPrint(
"Image upload failed $e"
);
AppSettings.longFailedToast(
"Tanker created but images failed"
);
}
}
Navigator.pop(context);
if (!mounted) return;
AppSettings.longSuccessToast(
"Tanker Created Successfully"
);
Navigator.pop(context,true);
_resetForm();
_fetchTankers();
}
catch (e) {
Navigator.pop(context);
debugPrint(
"⚠️ addTanker error $e"
);
if (!mounted) return;
AppSettings.longFailedToast(
"Something went wrong"
);
}
}
Future<void> openTankerSimpleSheet(BuildContext context) async { Future<void> openTankerSimpleSheet(BuildContext context) async {
await showModalBottomSheet( await showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
builder: (context) { builder: (context) {
return StatefulBuilder(
builder: (context, modalSetState) {
final viewInsets = MediaQuery.of(context).viewInsets.bottom; final viewInsets = MediaQuery.of(context).viewInsets.bottom;
return FractionallySizedBox( return FractionallySizedBox(
heightFactor: 0.75, heightFactor: 0.75,
child: Container( child: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(20,16,20,20+viewInsets), padding: EdgeInsets.fromLTRB(20,16,20,20+viewInsets),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -572,36 +821,570 @@ class _ResourcesFleetScreenState extends State<ResourcesFleetScreen> {
), ),
), ),
const SizedBox(height: 20), _LabeledField(
label: "Tanker Images (Max 5)",
child: Column(
children: [
/// IMAGE GRID
if(tankerImages.isNotEmpty)
GridView.builder(
shrinkWrap:true,
physics:NeverScrollableScrollPhysics(),
itemCount:tankerImages.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:3,
crossAxisSpacing:8,
mainAxisSpacing:8,
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: _addTanker,
child: Text(
"Save",
style: fontTextStyle(14, Colors.white, FontWeight.w600),
),
), ),
itemBuilder:(context,index){
return Stack(
children:[
ClipRRect(
borderRadius:BorderRadius.circular(8),
child:Image.file(
File(tankerImages[index].path),
width:double.infinity,
height:double.infinity,
fit:BoxFit.cover,
), ),
],
), ),
Positioned(
right:4,
top:4,
child:GestureDetector(
onTap:(){
tankerImages.removeAt(index);
modalSetState((){}); /// FIXED
},
child:Container(
decoration:const BoxDecoration(
color:Colors.red,
shape:BoxShape.circle,
), ),
child:const Icon(
Icons.close,
color:Colors.white,
size:18,
), ),
), ),
), ),
)
],
); );
}, },
);
} ),
const SizedBox(height:10),
/// BUTTONS
Row(
children:[
Expanded(
child:OutlinedButton.icon(
onPressed:(){
pickTankerImages(modalSetState); /// FIXED
},
icon:Icon(Icons.photo),
label:Text("Gallery"),
),
),
const SizedBox(width:10),
Expanded(
child:OutlinedButton.icon(
onPressed:(){
pickFromCamera(modalSetState); /// FIXED
},
icon:Icon(Icons.camera_alt),
label:Text("Camera"),
),
),
],
),
const SizedBox(height:5),
Text(
"${tankerImages.length}/5 images selected",
style: fontTextStyle(
12,
Colors.grey,
FontWeight.w400
),
),
],
),
),
/// SAVE BUTTON (UNCHANGED)
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:_addTankerNew,
child:Text(
"Save",
style:fontTextStyle(
14,
Colors.white,
FontWeight.w600
),
),
),
),
],
),
),
),
),
),
);
},
);
},
);
}
/*Future<void> openTankerSimpleSheet1(BuildContext context) async {
await showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
final viewInsets = MediaQuery.of(context).viewInsets.bottom;
return FractionallySizedBox(
heightFactor: 0.75,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: EdgeInsets.fromLTRB(20, 16, 20, 20 + viewInsets),
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: "Tanker Name *",
child: TextFormField(
controller: _nameCtrl,
validator: (v) => _required(v, field: "Tanker Name"),
textCapitalization: TextCapitalization.characters,
inputFormatters: const [
FirstCharUppercaseFormatter(), // << live first-letter caps
],
decoration: InputDecoration(
hintText: "Enter Tanker Name",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Tanker Capacity (in L) *",
child: TextFormField(
controller: _capacityCtrl,
validator: (v) => _required(v, field: "Tanker Capacity"),
decoration: InputDecoration(
hintText: "10,000",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[0-9,]')),
],
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Tanker Type *",
child: DropdownButtonFormField<String>(
value: selectedType,
dropdownColor: Colors.white,
items: tankerTypes
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
.toList(),
onChanged: (v) => setState(() => selectedType = v),
validator: (v) => v == null || v.isEmpty ? "Tanker Type is required" : null,
isExpanded: true,
alignment: Alignment.centerLeft,
hint: Text(
"Select 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),
),
),
),
_LabeledField(
label: "Type of water *",
child: DropdownButtonFormField<String>(
value: selectedTypeOfWater,
dropdownColor: Colors.white,
items: typeOfWater
.map((t) => DropdownMenuItem(value: t, child: Text(t)))
.toList(),
onChanged: (v) => setState(() => selectedTypeOfWater = v),
validator: (v) => v == null || v.isEmpty ? "Type of water is required" : null,
isExpanded: true,
alignment: Alignment.centerLeft,
hint: Text(
"Select type of water",
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: "License Plate *",
child: TextFormField(
controller: _plateCtrl,
validator: (v) => _required(v, field: "License Plate"),
decoration: InputDecoration(
hintText: "AB 05 H 4948",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: " Age of vehicle (opt)",
child: TextFormField(
controller: _mfgYearCtrl,
decoration: InputDecoration(
hintText: "12",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
],
textInputAction: TextInputAction.next,
),
),
_LabeledField(
label: "Insurance Expiry Date (opt)",
child: TextFormField(
controller: _insExpiryCtrl,
readOnly: true,
decoration: InputDecoration(
hintText: "DD-MM-YYYY",
hintStyle: fontTextStyle(14, const Color(0xFF939495), FontWeight.w400),
border: const OutlineInputBorder(),
isDense: true,
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
),
onTap: _pickInsuranceDate,
),
),
_LabeledField(
label: "Tanker Images (Max 5)",
child: Column(
children: [
/// Grid Images
if(tankerImages.isNotEmpty)
GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: tankerImages.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context,index){
return Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(tankerImages[index].path),
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Positioned(
right: 4,
top: 4,
child: GestureDetector(
onTap: (){
tankerImages.removeAt(index);
setState(() {});
},
child: Container(
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: Colors.white,
size: 18,
),
),
),
)
],
);
},
),
const SizedBox(height:10),
/// Add Buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: pickTankerImages,
icon: Icon(Icons.photo),
label: Text("Gallery"),
),
),
const SizedBox(width:10),
Expanded(
child: OutlinedButton.icon(
onPressed: pickFromCamera,
icon: Icon(Icons.camera_alt),
label: Text("Camera"),
),
),
],
),
const SizedBox(height:5),
Text(
"${tankerImages.length}/5 images selected",
style: fontTextStyle(
12,
Colors.grey,
FontWeight.w400
),
),
],
),
),
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: _addTanker,
child: Text(
"Save",
style: fontTextStyle(14, Colors.white, FontWeight.w600),
),
),
),
],
),
),
),
),
),
);
},
);
}*/

@ -2,9 +2,11 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:supplier_new/resources/tanker_trips_model.dart'; import 'package:supplier_new/resources/tanker_trips_model.dart';
import '../common/settings.dart'; import '../common/settings.dart';
import 'package:http/http.dart' as http;
class FirstCharUppercaseFormatter extends TextInputFormatter { class FirstCharUppercaseFormatter extends TextInputFormatter {
const FirstCharUppercaseFormatter(); const FirstCharUppercaseFormatter();
@ -76,6 +78,11 @@ class _TankerDetailsPageState extends State<TankerDetailsPage> {
String? currentAvailability; String? currentAvailability;
List<TankerTripsModel> tankerTripsList = []; List<TankerTripsModel> tankerTripsList = [];
List<String> tankerImageUrls = [];
List<XFile> newImages = [];
final int maxImages = 5;
@override @override
void initState() { void initState() {
@ -84,6 +91,15 @@ class _TankerDetailsPageState extends State<TankerDetailsPage> {
_nameCtrl.text = _nameCtrl.text =
widget.tankerDetails.tanker_name ?? ''; widget.tankerDetails.tanker_name ?? '';
if(widget.tankerDetails.images != null){
tankerImageUrls =
List<String>.from(
widget.tankerDetails.images
);
}
/// FIX allow null availability /// FIX allow null availability
if (widget.tankerDetails.availability != null && if (widget.tankerDetails.availability != null &&
widget.tankerDetails.availability is List && widget.tankerDetails.availability is List &&
@ -481,26 +497,57 @@ class _TankerDetailsPageState extends State<TankerDetailsPage> {
} }
Future<void> _refreshTankerDetails() async { Future<void> _refreshTankerDetails() async {
try { try {
setState(() => isLoading = true); setState(() => isLoading = true);
final updatedDetails = await AppSettings.getTankerDetailsByName( final updatedDetails =
await AppSettings.getTankerDetailsByName(
_nameCtrl.text.trim(), _nameCtrl.text.trim(),
); );
if (updatedDetails != null) { if (updatedDetails != null) {
setState(() { setState(() {
widget.tankerDetails = updatedDetails; widget.tankerDetails = updatedDetails;
/// VERY IMPORTANT FIX
tankerImageUrls =
List<String>.from(
updatedDetails.images ?? []
);
}); });
} else {
AppSettings.longFailedToast("Failed to fetch updated tanker details");
} }
} catch (e) { else{
debugPrint("⚠️ Error refreshing tanker details: $e");
AppSettings.longFailedToast("Error refreshing tanker details"); AppSettings.longFailedToast(
} finally { "Failed to fetch updated tanker details"
);
}
}
catch(e){
debugPrint(
"⚠️ Error refreshing tanker details: $e"
);
AppSettings.longFailedToast(
"Error refreshing tanker details"
);
}
finally{
setState(() => isLoading = false); setState(() => isLoading = false);
} }
} }
String? fitToOption(String? incoming, List<String> options) { String? fitToOption(String? incoming, List<String> options) {
@ -830,6 +877,280 @@ class _TankerDetailsPageState extends State<TankerDetailsPage> {
); );
} }
Future<void> pickNewImages() async {
int totalImages =
tankerImageUrls.length + newImages.length;
if(totalImages >= maxImages){
AppSettings.longFailedToast(
"Maximum 5 images allowed"
);
return;
}
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();
},
),
],
),
);
}
);
}
Future<void> _pickFromGallery() async {
final picker = ImagePicker();
final images =
await picker.pickMultiImage();
if(images.isEmpty) return;
int remaining =
maxImages - tankerImageUrls.length;
if(images.length > remaining){
AppSettings.longFailedToast(
"You can upload only $remaining more images"
);
newImages =
images.take(remaining).toList();
}
else{
newImages = images;
}
await uploadUpdatedImages();
}
Future<void> _pickFromCamera() async {
if(tankerImageUrls.length >= maxImages){
AppSettings.longFailedToast(
"Maximum 5 images allowed"
);
return;
}
final picker = ImagePicker();
final image =
await picker.pickImage(
source: ImageSource.camera,
imageQuality:70,
maxWidth:1280,
maxHeight:1280,
);
if(image == null) return;
newImages = [image];
await uploadUpdatedImages();
}
Future<void> deleteImage(int index) async {
try{
AppSettings.preLoaderDialog(context);
String imageUrl =
tankerImageUrls[index];
bool status =
await AppSettings.deleteTankerImage(
widget.tankerDetails.dbId,
imageUrl
);
Navigator.pop(context);
if(status){
tankerImageUrls.removeAt(index);
setState((){});
AppSettings.longSuccessToast(
"Image deleted"
);
/// optional refresh
await _refreshTankerDetails();
}
else{
AppSettings.longFailedToast(
"Delete failed"
);
}
}
catch(e){
Navigator.pop(context);
AppSettings.longFailedToast(
"Network error"
);
}
}
Future<void> uploadUpdatedImages() async {
try{
AppSettings.preLoaderDialog(context);
var request = http.MultipartRequest(
'POST',
Uri.parse(
AppSettings.host+
"uploads_tanker_images/"
"${widget.tankerDetails.dbId}"
)
);
request.headers.addAll(
await AppSettings.buildRequestHeaders()
);
for(var img in newImages){
request.files.add(
await http.MultipartFile.fromPath(
'files',
img.path
)
);
}
var response =
await request.send();
Navigator.pop(context);
var resp =
await http.Response.fromStream(response);
if(response.statusCode==200){
AppSettings.longSuccessToast(
"Images Updated"
);
newImages.clear();
await _refreshTankerDetails();
}
else{
print(resp.body);
AppSettings.longFailedToast(
"Upload failed"
);
}
}
catch(e){
Navigator.pop(context);
AppSettings.longFailedToast(
"Upload failed"
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return WillPopScope(
@ -868,13 +1189,223 @@ class _TankerDetailsPageState extends State<TankerDetailsPage> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Images (${tankerImageUrls.length}/5)",
style: fontTextStyle(
12,
Color(0XFF515253),
FontWeight.w600
),
),
Text(
"Max 5 allowed",
style: fontTextStyle(
10,
Color(0XFF939495),
FontWeight.w400
),
),
],
),
SizedBox(height:8),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Image.asset( child: SizedBox(
height:180,
child:tankerImageUrls.isEmpty
? Image.asset(
'images/tanker_image.jpeg', 'images/tanker_image.jpeg',
width: double.infinity, fit:BoxFit.cover
)
: ListView.builder(
scrollDirection:Axis.horizontal,
itemCount:tankerImageUrls.length,
itemBuilder:(context,index){
return Padding(
padding:EdgeInsets.only(right:8),
child:ClipRRect(
borderRadius:
BorderRadius.circular(12),
child:Stack(
children:[
Image.network(
tankerImageUrls[index],
width:250,
height:180, height:180,
fit:BoxFit.cover, fit:BoxFit.cover,
errorBuilder:(context,error,stack){
return Container(
width:250,
height:180,
color:Colors.grey[300],
child:Icon(
Icons.image_not_supported,
size:40,
color:Colors.grey,
),
);
},
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);
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,
),
),
),
),
),
],
),
),
);
},
),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -1135,9 +1666,11 @@ class _TankerDetailsPageState extends State<TankerDetailsPage> {
width: 0.5), width: 0.5),
), ),
), ),
onPressed: () async {}, onPressed: () async {
pickNewImages();
},
child: Text( child: Text(
"Assign", "Update Images",
style: fontTextStyle( style: fontTextStyle(
14, 14,
const Color(0xFFFFFFFF), const Color(0xFFFFFFFF),

@ -14,6 +14,7 @@ class TankersModel {
String tanker_type=''; String tanker_type='';
String pumping_fee=''; String pumping_fee='';
List<dynamic> availability= []; List<dynamic> availability= [];
List<dynamic> images= [];
TankersModel(); TankersModel();
factory TankersModel.fromJson(Map<String, dynamic> json){ factory TankersModel.fromJson(Map<String, dynamic> json){
@ -29,6 +30,8 @@ class TankersModel {
rtvm.price = json['price'] ?? ''; rtvm.price = json['price'] ?? '';
rtvm.delivery_fee = json['delivery_fee'] ?? ''; rtvm.delivery_fee = json['delivery_fee'] ?? '';
rtvm.availability = json['availability'] ?? []; rtvm.availability = json['availability'] ?? [];
rtvm.images = json['images'] ?? [];
rtvm.manufacturing_year = json['manufacturing_year'] ?? ''; rtvm.manufacturing_year = json['manufacturing_year'] ?? '';
rtvm.insurance_expiry = json['insurance_exp_date'] ?? ''; rtvm.insurance_expiry = json['insurance_exp_date'] ?? '';
rtvm.tanker_type = json['tanker_type'] ?? ''; rtvm.tanker_type = json['tanker_type'] ?? '';

Loading…
Cancel
Save