initial commit: push everything
This commit is contained in:
78
lib/Models/Message/GetSms.dart
Normal file
78
lib/Models/Message/GetSms.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
class Sms {
|
||||
int? code;
|
||||
SmsDetails? details;
|
||||
String? message;
|
||||
bool? status;
|
||||
|
||||
Sms({this.code, this.details, this.message, this.status});
|
||||
|
||||
factory Sms.fromJson(Map<String, dynamic> json) {
|
||||
return Sms(
|
||||
code: json['code'] as int?,
|
||||
details: json['details'] != null
|
||||
? SmsDetails.fromJson(json['details'] as Map<String, dynamic>)
|
||||
: null,
|
||||
message: json['message'] as String?,
|
||||
status: json['status'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['code'] = code;
|
||||
if (details != null) {
|
||||
data['details'] = details!.toJson();
|
||||
}
|
||||
data['message'] = message;
|
||||
data['status'] = status;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class SmsDetails {
|
||||
int? providerId;
|
||||
int? templateTypeId;
|
||||
String? templateName;
|
||||
int? templateId;
|
||||
String? providerApi;
|
||||
String? content;
|
||||
int? defaultProvider;
|
||||
int? passkey;
|
||||
|
||||
SmsDetails({
|
||||
this.providerId,
|
||||
this.templateTypeId,
|
||||
this.templateName,
|
||||
this.templateId,
|
||||
this.providerApi,
|
||||
this.content,
|
||||
this.defaultProvider,
|
||||
this.passkey,
|
||||
});
|
||||
|
||||
factory SmsDetails.fromJson(Map<String, dynamic> json) {
|
||||
return SmsDetails(
|
||||
providerId: json['providerid'] as int?,
|
||||
templateTypeId: json['templatetypeid'] as int?,
|
||||
templateName: json['templatename'] as String?,
|
||||
templateId: json['templateid'] as int?,
|
||||
providerApi: json['providerapi'] as String?,
|
||||
content: json['content'] as String?,
|
||||
defaultProvider: json['defaultprovider'] as int?,
|
||||
passkey: json['passkey'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['providerid'] = providerId;
|
||||
data['templatetypeid'] = templateTypeId;
|
||||
data['templatename'] = templateName;
|
||||
data['templateid'] = templateId;
|
||||
data['providerapi'] = providerApi;
|
||||
data['content'] = content;
|
||||
data['defaultprovider'] = defaultProvider;
|
||||
data['passkey'] = passkey;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
175
lib/Models/Offline/Offline.dart
Normal file
175
lib/Models/Offline/Offline.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
class offline {
|
||||
int? userid;
|
||||
String? authname;
|
||||
int? configid;
|
||||
int? authmode;
|
||||
int? roleid;
|
||||
String? firstname;
|
||||
String? lastname;
|
||||
String? fullname;
|
||||
String? password;
|
||||
String? email;
|
||||
String? contactno;
|
||||
String? address;
|
||||
String? suburb;
|
||||
String? city;
|
||||
String? state;
|
||||
String? postcode;
|
||||
String? userfcmtoken;
|
||||
int? pin;
|
||||
int? partnerid;
|
||||
String? identificationno;
|
||||
String? vehiclename;
|
||||
String? vehicleno;
|
||||
String? licenseno;
|
||||
String? insoranceno;
|
||||
String? insurancedate;
|
||||
int? shiftid;
|
||||
String? starttime;
|
||||
String? endtime;
|
||||
int? shifthours;
|
||||
double? basefare;
|
||||
double? additionalcharges;
|
||||
int? orders;
|
||||
double? fuelcharge;
|
||||
String? logdate;
|
||||
int? applocationid;
|
||||
String? applocation;
|
||||
int? logseconds;
|
||||
int? riderid;
|
||||
String? status;
|
||||
int? tenantid;
|
||||
|
||||
offline({
|
||||
this.userid,
|
||||
this.authname,
|
||||
this.configid,
|
||||
this.authmode,
|
||||
this.roleid,
|
||||
this.firstname,
|
||||
this.lastname,
|
||||
this.fullname,
|
||||
this.password,
|
||||
this.email,
|
||||
this.contactno,
|
||||
this.address,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.state,
|
||||
this.postcode,
|
||||
this.userfcmtoken,
|
||||
this.pin,
|
||||
this.partnerid,
|
||||
this.identificationno,
|
||||
this.vehiclename,
|
||||
this.vehicleno,
|
||||
this.licenseno,
|
||||
this.insoranceno,
|
||||
this.insurancedate,
|
||||
this.shiftid,
|
||||
this.starttime,
|
||||
this.endtime,
|
||||
this.shifthours,
|
||||
this.basefare,
|
||||
this.additionalcharges,
|
||||
this.orders,
|
||||
this.fuelcharge,
|
||||
this.logdate,
|
||||
this.applocationid,
|
||||
this.applocation,
|
||||
this.logseconds,
|
||||
this.riderid,
|
||||
this.status,
|
||||
this.tenantid,
|
||||
});
|
||||
|
||||
factory offline.fromJson(Map<String, dynamic> json) {
|
||||
return offline(
|
||||
userid: json['userid'] as int?,
|
||||
authname: json['authname'] as String?,
|
||||
configid: json['configid'] as int?,
|
||||
authmode: json['authmode'] as int?,
|
||||
roleid: json['roleid'] as int?,
|
||||
firstname: json['firstname'] as String?,
|
||||
lastname: json['lastname'] as String?,
|
||||
fullname: json['fullname'] as String?,
|
||||
password: json['password'] as String?,
|
||||
email: json['email'] as String?,
|
||||
contactno: json['contactno'] as String?,
|
||||
address: json['address'] as String?,
|
||||
suburb: json['suburb'] as String?,
|
||||
city: json['city'] as String?,
|
||||
state: json['state'] as String?,
|
||||
postcode: json['postcode'] as String?,
|
||||
userfcmtoken: json['userfcmtoken'] as String?,
|
||||
pin: json['pin'] as int?,
|
||||
partnerid: json['partnerid'] as int?,
|
||||
identificationno: json['identificationno'] as String?,
|
||||
vehiclename: json['vehiclename'] as String?,
|
||||
vehicleno: json['vehicleno'] as String?,
|
||||
licenseno: json['licenseno'] as String?,
|
||||
insoranceno: json['insoranceno'] as String?,
|
||||
insurancedate: json['insurancedate'] as String?,
|
||||
shiftid: json['shiftid'] as int?,
|
||||
starttime: json['starttime'] as String?,
|
||||
endtime: json['endtime'] as String?,
|
||||
shifthours: json['shifthours'] as int?,
|
||||
basefare: (json['basefare'] as num?)?.toDouble(),
|
||||
additionalcharges: (json['additionalcharges'] as num?)?.toDouble(),
|
||||
orders: json['orders'] as int?,
|
||||
fuelcharge: (json['fuelcharge'] as num?)?.toDouble(),
|
||||
logdate: json['logdate'] as String?,
|
||||
applocationid: json['applocationid'] as int?,
|
||||
applocation: json['applocation'] as String?,
|
||||
logseconds: json['logseconds'] as int?,
|
||||
riderid: json['riderid'] as int?,
|
||||
status: json['status'] as String?,
|
||||
tenantid: json['tenantid'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['userid'] = userid;
|
||||
data['authname'] = authname;
|
||||
data['configid'] = configid;
|
||||
data['authmode'] = authmode;
|
||||
data['roleid'] = roleid;
|
||||
data['firstname'] = firstname;
|
||||
data['lastname'] = lastname;
|
||||
data['fullname'] = fullname;
|
||||
data['password'] = password;
|
||||
data['email'] = email;
|
||||
data['contactno'] = contactno;
|
||||
data['address'] = address;
|
||||
data['suburb'] = suburb;
|
||||
data['city'] = city;
|
||||
data['state'] = state;
|
||||
data['postcode'] = postcode;
|
||||
data['userfcmtoken'] = userfcmtoken;
|
||||
data['pin'] = pin;
|
||||
data['partnerid'] = partnerid;
|
||||
data['identificationno'] = identificationno;
|
||||
data['vehiclename'] = vehiclename;
|
||||
data['vehicleno'] = vehicleno;
|
||||
data['licenseno'] = licenseno;
|
||||
data['insoranceno'] = insoranceno;
|
||||
data['insurancedate'] = insurancedate;
|
||||
data['shiftid'] = shiftid;
|
||||
data['starttime'] = starttime;
|
||||
data['endtime'] = endtime;
|
||||
data['shifthours'] = shifthours;
|
||||
data['basefare'] = basefare;
|
||||
data['additionalcharges'] = additionalcharges;
|
||||
data['orders'] = orders;
|
||||
data['fuelcharge'] = fuelcharge;
|
||||
data['logdate'] = logdate;
|
||||
data['applocationid'] = applocationid;
|
||||
data['applocation'] = applocation;
|
||||
data['logseconds'] = logseconds;
|
||||
data['riderid'] = riderid;
|
||||
data['status'] = status;
|
||||
data['tenantid'] = tenantid;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
285
lib/Models/Orders/Orders.dart
Normal file
285
lib/Models/Orders/Orders.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
|
||||
|
||||
class Orders {
|
||||
int? deliveryid;
|
||||
int? orderheaderid;
|
||||
int? applocationid;
|
||||
int? configid;
|
||||
int? partnerid;
|
||||
int? tenantid;
|
||||
int? moduleid;
|
||||
int? locationid;
|
||||
int? categoryid;
|
||||
int? userid;
|
||||
int? subcategoryid;
|
||||
String? orderid;
|
||||
String? deliverydate;
|
||||
String? orderstatus;
|
||||
String? assigntime;
|
||||
String? starttime;
|
||||
String? arrivaltime;
|
||||
String? pickuptime;
|
||||
String? deliverytime;
|
||||
String? canceltime;
|
||||
int? itemcount;
|
||||
double? orderamount;
|
||||
int? customerid;
|
||||
String? pickupcustomer;
|
||||
String? pickupcontactno;
|
||||
String? pickupaddress;
|
||||
String? pickuplocation;
|
||||
int? pickuplocationid;
|
||||
String? pickuplat;
|
||||
String? pickuplon;
|
||||
int? deliverycustomerid;
|
||||
int? deliverylocationid;
|
||||
String? deliverycustomer;
|
||||
String? deliverycontactno;
|
||||
String? deliveryaddress;
|
||||
String? deliverylocation;
|
||||
String? droplat;
|
||||
String? droplon;
|
||||
String? deliverylat;
|
||||
String? deliverylong;
|
||||
double? deliverycharges;
|
||||
double? deliveryamt;
|
||||
String? deliverytype;
|
||||
String? notes;
|
||||
String? ordernotes;
|
||||
String? riderslat;
|
||||
String? riderslon;
|
||||
int? firstmilekm;
|
||||
double? firstmilecharges;
|
||||
double? lastmilecharges;
|
||||
double? ridercharges;
|
||||
String? kms;
|
||||
String? actualkms;
|
||||
int? paymenttype;
|
||||
String? tenantname;
|
||||
String? tenantcontactno;
|
||||
String? tenanttoken;
|
||||
String? tenantsuburb;
|
||||
String? tenantcity;
|
||||
String? locationname;
|
||||
String? locationcontactno;
|
||||
String? locationsuburb;
|
||||
String? ridername;
|
||||
String? userfcmtoken;
|
||||
int? queueid;
|
||||
int? smsdelivery;
|
||||
bool startStatus;
|
||||
|
||||
Orders({
|
||||
this.deliveryid,
|
||||
this.orderheaderid,
|
||||
this.applocationid,
|
||||
this.configid,
|
||||
this.partnerid,
|
||||
this.tenantid,
|
||||
this.moduleid,
|
||||
this.locationid,
|
||||
this.categoryid,
|
||||
this.userid,
|
||||
this.subcategoryid,
|
||||
this.orderid,
|
||||
this.deliverydate,
|
||||
this.orderstatus,
|
||||
this.assigntime,
|
||||
this.starttime,
|
||||
this.arrivaltime,
|
||||
this.pickuptime,
|
||||
this.deliverytime,
|
||||
this.canceltime,
|
||||
this.itemcount,
|
||||
this.orderamount,
|
||||
this.customerid,
|
||||
this.pickupcustomer,
|
||||
this.pickupcontactno,
|
||||
this.pickupaddress,
|
||||
this.pickuplocation,
|
||||
this.pickuplocationid,
|
||||
this.pickuplat,
|
||||
this.pickuplon,
|
||||
this.deliverycustomerid,
|
||||
this.deliverylocationid,
|
||||
this.deliverycustomer,
|
||||
this.deliverycontactno,
|
||||
this.deliveryaddress,
|
||||
this.deliverylocation,
|
||||
this.droplat,
|
||||
this.droplon,
|
||||
this.deliverylat,
|
||||
this.deliverylong,
|
||||
this.deliverycharges,
|
||||
this.deliveryamt,
|
||||
this.deliverytype,
|
||||
this.notes,
|
||||
this.ordernotes,
|
||||
this.riderslat,
|
||||
this.riderslon,
|
||||
this.firstmilekm,
|
||||
this.firstmilecharges,
|
||||
this.lastmilecharges,
|
||||
this.ridercharges,
|
||||
this.kms,
|
||||
this.actualkms,
|
||||
this.paymenttype,
|
||||
this.tenantname,
|
||||
this.tenantcontactno,
|
||||
this.tenanttoken,
|
||||
this.tenantsuburb,
|
||||
this.tenantcity,
|
||||
this.locationname,
|
||||
this.locationcontactno,
|
||||
this.locationsuburb,
|
||||
this.ridername,
|
||||
this.userfcmtoken,
|
||||
this.queueid,
|
||||
this.smsdelivery,
|
||||
this.startStatus = false,
|
||||
});
|
||||
|
||||
factory Orders.fromJson(Map<String, dynamic> json) {
|
||||
return Orders(
|
||||
deliveryid: json['deliveryid'] as int?,
|
||||
orderheaderid: json['orderheaderid'] as int?,
|
||||
applocationid: json['applocationid'] as int?,
|
||||
configid: json['configid'] as int?,
|
||||
partnerid: json['partnerid'] as int?,
|
||||
tenantid: json['tenantid'] as int?,
|
||||
moduleid: json['moduleid'] as int?,
|
||||
locationid: json['locationid'] as int?,
|
||||
categoryid: json['categoryid'] as int?,
|
||||
userid: json['userid'] as int?,
|
||||
subcategoryid: json['subcategoryid'] as int?,
|
||||
orderid: json['orderid'] as String?,
|
||||
deliverydate: json['deliverydate'] as String?,
|
||||
orderstatus: json['orderstatus'] as String?,
|
||||
assigntime: json['assigntime'] as String?,
|
||||
starttime: json['starttime'] as String?,
|
||||
arrivaltime: json['arrivaltime'] as String?,
|
||||
pickuptime: json['pickuptime'] as String?,
|
||||
deliverytime: json['deliverytime'] as String?,
|
||||
canceltime: json['canceltime'] as String?,
|
||||
itemcount: json['itemcount'] as int?,
|
||||
orderamount: (json['orderamount'] as num?)?.toDouble(),
|
||||
customerid: json['customerid'] as int?,
|
||||
pickupcustomer: json['pickupcustomer'] as String?,
|
||||
pickupcontactno: json['pickupcontactno'] as String?,
|
||||
pickupaddress: json['Pickupaddress'] as String?,
|
||||
pickuplocation: json['pickuplocation'] as String?,
|
||||
pickuplocationid: json['pickuplocationid'] as int?,
|
||||
pickuplat: json['pickuplat'] as String?,
|
||||
pickuplon: json['pickuplon'] as String?,
|
||||
deliverycustomerid: json['deliverycustomerid'] as int?,
|
||||
deliverylocationid: json['deliverylocationid'] as int?,
|
||||
deliverycustomer: json['deliverycustomer'] as String?,
|
||||
deliverycontactno: json['deliverycontactno'] as String?,
|
||||
deliveryaddress: json['deliveryaddress'] as String?,
|
||||
deliverylocation: json['deliverylocation'] as String?,
|
||||
droplat: json['droplat'] as String?,
|
||||
droplon: json['droplon'] as String?,
|
||||
deliverylat: json['deliverylat'] as String?,
|
||||
deliverylong: json['deliverylong'] as String?,
|
||||
deliverycharges: (json['deliverycharges'] as num?)?.toDouble(),
|
||||
deliveryamt: (json['deliveryamt'] as num?)?.toDouble(),
|
||||
deliverytype: json['deliverytype'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
ordernotes: json['ordernotes'] as String?,
|
||||
riderslat: json['riderslat'] as String?,
|
||||
riderslon: json['riderslon'] as String?,
|
||||
firstmilekm: json['firstmilekm'] as int?,
|
||||
firstmilecharges: (json['firstmilecharges'] as num?)?.toDouble(),
|
||||
lastmilecharges: (json['lastmilecharges'] as num?)?.toDouble(),
|
||||
ridercharges: (json['ridercharges'] as num?)?.toDouble(),
|
||||
kms: json['kms'] as String?,
|
||||
actualkms: json['actualkms'] as String?,
|
||||
paymenttype: json['paymenttype'] as int?,
|
||||
tenantname: json['tenantname'] as String?,
|
||||
tenantcontactno: json['tenantcontactno'] as String?,
|
||||
tenanttoken: json['tenanttoken'] as String?,
|
||||
tenantsuburb: json['tenantsuburb'] as String?,
|
||||
tenantcity: json['tenantcity'] as String?,
|
||||
locationname: json['locationname'] as String?,
|
||||
locationcontactno: json['locationcontactno'] as String?,
|
||||
locationsuburb: json['locationsuburb'] as String?,
|
||||
ridername: json['ridername'] as String?,
|
||||
userfcmtoken: json['userfcmtoken'] as String?,
|
||||
queueid: json['queueid'] as int?,
|
||||
smsdelivery: json['smsdelivery'] as int?,
|
||||
startStatus: json['startStatus'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['deliveryid'] = deliveryid;
|
||||
data['orderheaderid'] = orderheaderid;
|
||||
data['applocationid'] = applocationid;
|
||||
data['configid'] = configid;
|
||||
data['partnerid'] = partnerid;
|
||||
data['tenantid'] = tenantid;
|
||||
data['moduleid'] = moduleid;
|
||||
data['locationid'] = locationid;
|
||||
data['categoryid'] = categoryid;
|
||||
data['userid'] = userid;
|
||||
data['subcategoryid'] = subcategoryid;
|
||||
data['orderid'] = orderid;
|
||||
data['deliverydate'] = deliverydate;
|
||||
data['orderstatus'] = orderstatus;
|
||||
data['assigntime'] = assigntime;
|
||||
data['starttime'] = starttime;
|
||||
data['arrivaltime'] = arrivaltime;
|
||||
data['pickuptime'] = pickuptime;
|
||||
data['deliverytime'] = deliverytime;
|
||||
data['canceltime'] = canceltime;
|
||||
data['itemcount'] = itemcount;
|
||||
data['orderamount'] = orderamount;
|
||||
data['customerid'] = customerid;
|
||||
data['pickupcustomer'] = pickupcustomer;
|
||||
data['pickupcontactno'] = pickupcontactno;
|
||||
data['Pickupaddress'] = pickupaddress;
|
||||
data['pickuplocation'] = pickuplocation;
|
||||
data['pickuplocationid'] = pickuplocationid;
|
||||
data['pickuplat'] = pickuplat;
|
||||
data['pickuplon'] = pickuplon;
|
||||
data['deliverycustomerid'] = deliverycustomerid;
|
||||
data['deliverylocationid'] = deliverylocationid;
|
||||
data['deliverycustomer'] = deliverycustomer;
|
||||
data['deliverycontactno'] = deliverycontactno;
|
||||
data['deliveryaddress'] = deliveryaddress;
|
||||
data['deliverylocation'] = deliverylocation;
|
||||
data['droplat'] = droplat;
|
||||
data['droplon'] = droplon;
|
||||
data['deliverylat'] = deliverylat;
|
||||
data['deliverylong'] = deliverylong;
|
||||
data['deliverycharges'] = deliverycharges;
|
||||
data['deliveryamt'] = deliveryamt;
|
||||
data['deliverytype'] = deliverytype;
|
||||
data['notes'] = notes;
|
||||
data['ordernotes'] = ordernotes;
|
||||
data['riderslat'] = riderslat;
|
||||
data['riderslon'] = riderslon;
|
||||
data['firstmilekm'] = firstmilekm;
|
||||
data['firstmilecharges'] = firstmilecharges;
|
||||
data['lastmilecharges'] = lastmilecharges;
|
||||
data['ridercharges'] = ridercharges;
|
||||
data['kms'] = kms;
|
||||
data['actualkms'] = actualkms;
|
||||
data['paymenttype'] = paymenttype;
|
||||
data['tenantname'] = tenantname;
|
||||
data['tenantcontactno'] = tenantcontactno;
|
||||
data['tenanttoken'] = tenanttoken;
|
||||
data['tenantsuburb'] = tenantsuburb;
|
||||
data['tenantcity'] = tenantcity;
|
||||
data['locationname'] = locationname;
|
||||
data['locationcontactno'] = locationcontactno;
|
||||
data['locationsuburb'] = locationsuburb;
|
||||
data['ridername'] = ridername;
|
||||
data['userfcmtoken'] = userfcmtoken;
|
||||
data['queueid'] = queueid;
|
||||
data['smsdelivery'] = smsdelivery;
|
||||
data['startStatus'] = startStatus;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
207
lib/Models/ProductDetails/ProductDetails.dart
Normal file
207
lib/Models/ProductDetails/ProductDetails.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
class ProductDetails {
|
||||
int? code;
|
||||
List<Product>? products;
|
||||
String? message;
|
||||
Pricedetails? pricedetails;
|
||||
bool? status;
|
||||
|
||||
ProductDetails({
|
||||
this.code,
|
||||
this.products,
|
||||
this.message,
|
||||
this.pricedetails,
|
||||
this.status,
|
||||
});
|
||||
|
||||
factory ProductDetails.fromJson(Map<String, dynamic> json) {
|
||||
return ProductDetails(
|
||||
code: json['code'] as int?,
|
||||
products: json['details'] != null
|
||||
? (json['details'] as List<dynamic>)
|
||||
.map((v) => Product.fromJson(v as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
message: json['message'] as String?,
|
||||
pricedetails: json['pricedetails'] != null
|
||||
? Pricedetails.fromJson(json['pricedetails'] as Map<String, dynamic>)
|
||||
: null,
|
||||
status: json['status'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['code'] = code;
|
||||
if (products != null) {
|
||||
data['details'] = products!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['message'] = message;
|
||||
if (pricedetails != null) {
|
||||
data['pricedetails'] = pricedetails!.toJson();
|
||||
}
|
||||
data['status'] = status;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Product {
|
||||
int? orderDetailId;
|
||||
int? orderHeaderId;
|
||||
int? tenantId;
|
||||
int? locationId;
|
||||
int? productId;
|
||||
String? productName;
|
||||
String? productDescription;
|
||||
int? supplyQty;
|
||||
int? balanceQty;
|
||||
int? orderQty;
|
||||
int? price;
|
||||
int? unitId;
|
||||
String? unitName;
|
||||
int? productAddonId;
|
||||
int? addonTypeId;
|
||||
int? productMapId;
|
||||
int? productVariantId;
|
||||
String? productAddonDescription;
|
||||
int? discountId;
|
||||
String? discountName;
|
||||
String? discountCode;
|
||||
String? discountTerms;
|
||||
int? discountPercentage;
|
||||
int? discountAmount;
|
||||
int? landingAmount;
|
||||
int? taxPercentage;
|
||||
int? taxAmount;
|
||||
int? productSumPrice;
|
||||
String? itemStatus;
|
||||
String? delivered;
|
||||
int? orderAmount;
|
||||
String? productImage;
|
||||
|
||||
Product({
|
||||
this.orderDetailId,
|
||||
this.orderHeaderId,
|
||||
this.tenantId,
|
||||
this.locationId,
|
||||
this.productId,
|
||||
this.productName,
|
||||
this.productDescription,
|
||||
this.supplyQty,
|
||||
this.balanceQty,
|
||||
this.orderQty,
|
||||
this.price,
|
||||
this.unitId,
|
||||
this.unitName,
|
||||
this.productAddonId,
|
||||
this.addonTypeId,
|
||||
this.productMapId,
|
||||
this.productVariantId,
|
||||
this.productAddonDescription,
|
||||
this.discountId,
|
||||
this.discountName,
|
||||
this.discountCode,
|
||||
this.discountTerms,
|
||||
this.discountPercentage,
|
||||
this.discountAmount,
|
||||
this.landingAmount,
|
||||
this.taxPercentage,
|
||||
this.taxAmount,
|
||||
this.productSumPrice,
|
||||
this.itemStatus,
|
||||
this.delivered,
|
||||
this.orderAmount,
|
||||
this.productImage,
|
||||
});
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
return Product(
|
||||
orderDetailId: json['orderdetailid'] as int?,
|
||||
orderHeaderId: json['orderheaderid'] as int?,
|
||||
tenantId: json['tenantid'] as int?,
|
||||
locationId: json['locationid'] as int?,
|
||||
productId: json['productid'] as int?,
|
||||
productName: json['productname'] as String?,
|
||||
productDescription: json['productdescription'] as String?,
|
||||
supplyQty: json['supplyqty'] as int?,
|
||||
balanceQty: json['balanceqty'] as int?,
|
||||
orderQty: json['orderqty'] as int?,
|
||||
price: json['price'] as int?,
|
||||
unitId: json['unitid'] as int?,
|
||||
unitName: json['unitname'] as String?,
|
||||
productAddonId: json['productaddonid'] as int?,
|
||||
addonTypeId: json['addontypeid'] as int?,
|
||||
productMapId: json['productmapid'] as int?,
|
||||
productVariantId: json['productvariantid'] as int?,
|
||||
productAddonDescription: json['productaddondescription'] as String?,
|
||||
discountId: json['discountid'] as int?,
|
||||
discountName: json['discountname'] as String?,
|
||||
discountCode: json['discountcode'] as String?,
|
||||
discountTerms: json['discountterms'] as String?,
|
||||
discountPercentage: json['discountpercentage'] as int?,
|
||||
discountAmount: json['discountamount'] as int?,
|
||||
landingAmount: json['landingamount'] as int?,
|
||||
taxPercentage: json['taxpercentage'] as int?,
|
||||
taxAmount: json['taxamount'] as int?,
|
||||
productSumPrice: json['productsumprice'] as int?,
|
||||
itemStatus: json['itemstatus'] as String?,
|
||||
delivered: json['delivered'] as String?,
|
||||
orderAmount: json['orderamount'] as int?,
|
||||
productImage: json['productimage'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['orderdetailid'] = orderDetailId;
|
||||
data['orderheaderid'] = orderHeaderId;
|
||||
data['tenantid'] = tenantId;
|
||||
data['locationid'] = locationId;
|
||||
data['productid'] = productId;
|
||||
data['productname'] = productName;
|
||||
data['productdescription'] = productDescription;
|
||||
data['supplyqty'] = supplyQty;
|
||||
data['balanceqty'] = balanceQty;
|
||||
data['orderqty'] = orderQty;
|
||||
data['price'] = price;
|
||||
data['unitid'] = unitId;
|
||||
data['unitname'] = unitName;
|
||||
data['productaddonid'] = productAddonId;
|
||||
data['addontypeid'] = addonTypeId;
|
||||
data['productmapid'] = productMapId;
|
||||
data['productvariantid'] = productVariantId;
|
||||
data['productaddondescription'] = productAddonDescription;
|
||||
data['discountid'] = discountId;
|
||||
data['discountname'] = discountName;
|
||||
data['discountcode'] = discountCode;
|
||||
data['discountterms'] = discountTerms;
|
||||
data['discountpercentage'] = discountPercentage;
|
||||
data['discountamount'] = discountAmount;
|
||||
data['landingamount'] = landingAmount;
|
||||
data['taxpercentage'] = taxPercentage;
|
||||
data['taxamount'] = taxAmount;
|
||||
data['productsumprice'] = productSumPrice;
|
||||
data['itemstatus'] = itemStatus;
|
||||
data['delivered'] = delivered;
|
||||
data['orderamount'] = orderAmount;
|
||||
data['productimage'] = productImage;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Pricedetails {
|
||||
int? orderAmount;
|
||||
|
||||
Pricedetails({this.orderAmount});
|
||||
|
||||
factory Pricedetails.fromJson(Map<String, dynamic> json) {
|
||||
return Pricedetails(
|
||||
orderAmount: json['orderamount'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['orderamount'] = orderAmount;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
78
lib/Models/TenantPrice/Tenant.dart
Normal file
78
lib/Models/TenantPrice/Tenant.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
class Tenant {
|
||||
int? code;
|
||||
TenantDetails? details;
|
||||
String? message;
|
||||
bool? status;
|
||||
|
||||
Tenant({this.code, this.details, this.message, this.status});
|
||||
|
||||
factory Tenant.fromJson(Map<String, dynamic> json) {
|
||||
return Tenant(
|
||||
code: json['code'] as int?,
|
||||
details: json['details'] != null
|
||||
? TenantDetails.fromJson(json['details'] as Map<String, dynamic>)
|
||||
: null,
|
||||
message: json['message'] as String?,
|
||||
status: json['status'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['code'] = code;
|
||||
if (details != null) {
|
||||
data['details'] = details!.toJson();
|
||||
}
|
||||
data['message'] = message;
|
||||
data['status'] = status;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class TenantDetails {
|
||||
int? pricingId;
|
||||
int? tenantId;
|
||||
int? locationId;
|
||||
String? pricingDate;
|
||||
int? basePrice;
|
||||
int? pricePerKm;
|
||||
int? minKm;
|
||||
int? otherCharges;
|
||||
|
||||
TenantDetails({
|
||||
this.pricingId,
|
||||
this.tenantId,
|
||||
this.locationId,
|
||||
this.pricingDate,
|
||||
this.basePrice,
|
||||
this.pricePerKm,
|
||||
this.minKm,
|
||||
this.otherCharges,
|
||||
});
|
||||
|
||||
factory TenantDetails.fromJson(Map<String, dynamic> json) {
|
||||
return TenantDetails(
|
||||
pricingId: json['pricingid'] as int?,
|
||||
tenantId: json['tenantid'] as int?,
|
||||
locationId: json['locationid'] as int?,
|
||||
pricingDate: json['pricingdate'] as String?,
|
||||
basePrice: json['baseprice'] as int?,
|
||||
pricePerKm: json['priceperkm'] as int?,
|
||||
minKm: json['minkm'] as int?,
|
||||
otherCharges: json['othercharges'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['pricingid'] = pricingId;
|
||||
data['tenantid'] = tenantId;
|
||||
data['locationid'] = locationId;
|
||||
data['pricingdate'] = pricingDate;
|
||||
data['baseprice'] = basePrice;
|
||||
data['priceperkm'] = pricePerKm;
|
||||
data['minkm'] = minKm;
|
||||
data['othercharges'] = otherCharges;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
131
lib/Models/User/User.dart
Normal file
131
lib/Models/User/User.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
class User {
|
||||
int? userid;
|
||||
String? authname;
|
||||
int? configid;
|
||||
int? authmode;
|
||||
int? roleid;
|
||||
String? firstname;
|
||||
String? lastname;
|
||||
String? password;
|
||||
String? email;
|
||||
String? contactno;
|
||||
String? address;
|
||||
String? suburb;
|
||||
String? city;
|
||||
String? state;
|
||||
String? postcode;
|
||||
String? userfcmtoken;
|
||||
int? pin;
|
||||
int? partnerid;
|
||||
int? tenantid;
|
||||
String? fullname;
|
||||
String? tenantname;
|
||||
String? tenantaddress;
|
||||
String? tenantcity;
|
||||
String? tenantpostcode;
|
||||
String? tenantlat;
|
||||
String? tenantlong;
|
||||
int? locationid;
|
||||
String? locationname;
|
||||
int? applocationid;
|
||||
|
||||
User({
|
||||
this.userid,
|
||||
this.authname,
|
||||
this.configid,
|
||||
this.authmode,
|
||||
this.roleid,
|
||||
this.firstname,
|
||||
this.lastname,
|
||||
this.password,
|
||||
this.email,
|
||||
this.contactno,
|
||||
this.address,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.state,
|
||||
this.postcode,
|
||||
this.userfcmtoken,
|
||||
this.pin,
|
||||
this.partnerid,
|
||||
this.tenantid,
|
||||
this.fullname,
|
||||
this.tenantname,
|
||||
this.tenantaddress,
|
||||
this.tenantcity,
|
||||
this.tenantpostcode,
|
||||
this.tenantlat,
|
||||
this.tenantlong,
|
||||
this.locationid,
|
||||
this.locationname,
|
||||
this.applocationid,
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
userid: json['userid'] as int?,
|
||||
authname: json['authname'] as String?,
|
||||
configid: json['configid'] as int?,
|
||||
authmode: json['authmode'] as int?,
|
||||
roleid: json['roleid'] as int?,
|
||||
firstname: json['firstname'] as String?,
|
||||
lastname: json['lastname'] as String?,
|
||||
password: json['password'] as String?,
|
||||
email: json['email'] as String?,
|
||||
contactno: json['contactno'] as String?,
|
||||
address: json['address'] as String?,
|
||||
suburb: json['suburb'] as String?,
|
||||
city: json['city'] as String?,
|
||||
state: json['state'] as String?,
|
||||
postcode: json['postcode'] as String?,
|
||||
userfcmtoken: json['userfcmtoken'] as String?,
|
||||
pin: json['pin'] as int?,
|
||||
partnerid: json['partnerid'] as int?,
|
||||
tenantid: json['tenantid'] as int?,
|
||||
fullname: json['fullname'] as String?,
|
||||
tenantname: json['tenantname'] as String?,
|
||||
tenantaddress: json['tenantaddress'] as String?,
|
||||
tenantcity: json['tenantcity'] as String?,
|
||||
tenantpostcode: json['tenantpostcode'] as String?,
|
||||
tenantlat: json['tenantlat'] as String?,
|
||||
tenantlong: json['tenantlong'] as String?,
|
||||
locationid: json['locationid'] as int?,
|
||||
locationname: json['locationname'] as String?,
|
||||
applocationid: json['applocationid'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'userid': userid,
|
||||
'authname': authname,
|
||||
'configid': configid,
|
||||
'authmode': authmode,
|
||||
'roleid': roleid,
|
||||
'firstname': firstname,
|
||||
'lastname': lastname,
|
||||
'password': password,
|
||||
'email': email,
|
||||
'contactno': contactno,
|
||||
'address': address,
|
||||
'suburb': suburb,
|
||||
'city': city,
|
||||
'state': state,
|
||||
'postcode': postcode,
|
||||
'userfcmtoken': userfcmtoken,
|
||||
'pin': pin,
|
||||
'partnerid': partnerid,
|
||||
'tenantid': tenantid,
|
||||
'fullname': fullname,
|
||||
'tenantname': tenantname,
|
||||
'tenantaddress': tenantaddress,
|
||||
'tenantcity': tenantcity,
|
||||
'tenantpostcode': tenantpostcode,
|
||||
'tenantlat': tenantlat,
|
||||
'tenantlong': tenantlong,
|
||||
'locationid': locationid,
|
||||
'locationname': locationname,
|
||||
'applocationid': applocationid,
|
||||
};
|
||||
}
|
||||
|
||||
315
lib/Models/deliveries/deliveries_models.dart
Normal file
315
lib/Models/deliveries/deliveries_models.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
// Consolidated deliveries models
|
||||
|
||||
class DeliveryUpdate {
|
||||
int? deliveryid;
|
||||
int? orderheaderid;
|
||||
int? deliverylocationid;
|
||||
int? pickuplocationid;
|
||||
int? smsdelivery;
|
||||
String? orderstatus;
|
||||
String? starttime;
|
||||
String? arrivaltime;
|
||||
String? pickuptime;
|
||||
String? deliverytime;
|
||||
String? canceltime;
|
||||
String? riderslat;
|
||||
String? riderslon;
|
||||
String? pickuplat;
|
||||
String? pickuplong;
|
||||
String? deliverylat;
|
||||
String? deliverylong;
|
||||
String? address;
|
||||
String? city;
|
||||
String? state;
|
||||
String? suburb;
|
||||
String? postcode;
|
||||
String? kms;
|
||||
String? riderkms;
|
||||
String? actualkms;
|
||||
double? deliveryamt;
|
||||
String? notes;
|
||||
String? deliverytype;
|
||||
String? kmcal;
|
||||
String? feedback;
|
||||
|
||||
DeliveryUpdate({
|
||||
this.deliveryid,
|
||||
this.orderheaderid,
|
||||
this.deliverylocationid,
|
||||
this.pickuplocationid,
|
||||
this.smsdelivery,
|
||||
this.orderstatus,
|
||||
this.starttime,
|
||||
this.arrivaltime,
|
||||
this.pickuptime,
|
||||
this.deliverytime,
|
||||
this.canceltime,
|
||||
this.riderslat,
|
||||
this.riderslon,
|
||||
this.pickuplat,
|
||||
this.pickuplong,
|
||||
this.deliverylat,
|
||||
this.deliverylong,
|
||||
this.address,
|
||||
this.city,
|
||||
this.state,
|
||||
this.suburb,
|
||||
this.postcode,
|
||||
this.kms,
|
||||
this.riderkms,
|
||||
this.actualkms,
|
||||
this.deliveryamt,
|
||||
this.notes,
|
||||
this.deliverytype,
|
||||
this.kmcal,
|
||||
this.feedback,
|
||||
});
|
||||
|
||||
factory DeliveryUpdate.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryUpdate(
|
||||
deliveryid: json['deliveryid'],
|
||||
orderheaderid: json['orderheaderid'],
|
||||
deliverylocationid: json['deliverylocationid'],
|
||||
pickuplocationid: json['pickuplocationid'],
|
||||
smsdelivery: json['smsdelivery'],
|
||||
orderstatus: json['orderstatus'],
|
||||
starttime: json['starttime'],
|
||||
arrivaltime: json['arrivaltime'],
|
||||
pickuptime: json['pickuptime'],
|
||||
deliverytime: json['deliverytime'],
|
||||
canceltime: json['canceltime'],
|
||||
riderslat: json['riderslat'],
|
||||
riderslon: json['riderslon'],
|
||||
pickuplat: json['pickuplat'],
|
||||
pickuplong: json['pickuplong'],
|
||||
deliverylat: json['deliverylat'],
|
||||
deliverylong: json['deliverylong'],
|
||||
address: json['address'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
suburb: json['suburb'],
|
||||
postcode: json['postcode'],
|
||||
kms: json['kms'],
|
||||
riderkms: json['riderkms'],
|
||||
actualkms: json['actualkms'],
|
||||
deliveryamt: (json['deliveryamt'] as num?)?.toDouble(),
|
||||
notes: json['notes'],
|
||||
deliverytype: json['deliverytype'],
|
||||
kmcal: json['kmcal'],
|
||||
feedback: json['feedback'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['deliveryid'] = deliveryid;
|
||||
data['orderheaderid'] = orderheaderid;
|
||||
data['deliverylocationid'] = deliverylocationid;
|
||||
data['pickuplocationid'] = pickuplocationid;
|
||||
data['smsdelivery'] = smsdelivery;
|
||||
data['orderstatus'] = orderstatus;
|
||||
data['starttime'] = starttime;
|
||||
data['arrivaltime'] = arrivaltime;
|
||||
data['pickuptime'] = pickuptime;
|
||||
data['deliverytime'] = deliverytime;
|
||||
data['canceltime'] = canceltime;
|
||||
data['riderslat'] = riderslat;
|
||||
data['riderslon'] = riderslon;
|
||||
data['pickuplat'] = pickuplat;
|
||||
data['pickuplong'] = pickuplong;
|
||||
data['deliverylat'] = deliverylat;
|
||||
data['deliverylong'] = deliverylong;
|
||||
data['address'] = address;
|
||||
data['city'] = city;
|
||||
data['state'] = state;
|
||||
data['suburb'] = suburb;
|
||||
data['postcode'] = postcode;
|
||||
data['kms'] = kms;
|
||||
data['riderkms'] = riderkms;
|
||||
data['actualkms'] = actualkms;
|
||||
data['deliveryamt'] = deliveryamt;
|
||||
data['notes'] = notes;
|
||||
data['deliverytype'] = deliverytype;
|
||||
data['kmcal'] = kmcal;
|
||||
data['feedback'] = feedback;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class DeliveryLogModel {
|
||||
int? logid;
|
||||
int? tenantid;
|
||||
int? partnerid;
|
||||
int? locationid;
|
||||
int? orderheaderid;
|
||||
int? deliveryid;
|
||||
int? userid;
|
||||
String? orderid;
|
||||
String? logdate;
|
||||
String? orderstatus;
|
||||
String? latitude;
|
||||
String? longitude;
|
||||
|
||||
DeliveryLogModel({
|
||||
this.logid,
|
||||
this.tenantid,
|
||||
this.partnerid,
|
||||
this.locationid,
|
||||
this.orderheaderid,
|
||||
this.deliveryid,
|
||||
this.userid,
|
||||
this.orderid,
|
||||
this.logdate,
|
||||
this.orderstatus,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
});
|
||||
|
||||
factory DeliveryLogModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryLogModel(
|
||||
logid: json['logid'],
|
||||
tenantid: json['tenantid'],
|
||||
partnerid: json['Partnerid'] ?? json['partnerid'],
|
||||
locationid: json['Locationid'] ?? json['locationid'],
|
||||
orderheaderid: json['orderheaderid'],
|
||||
deliveryid: json['deliveryid'],
|
||||
userid: json['userid'],
|
||||
orderid: json['orderid'],
|
||||
logdate: json['logdate'],
|
||||
orderstatus: json['orderstatus'],
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['logid'] = logid;
|
||||
data['tenantid'] = tenantid;
|
||||
data['Partnerid'] = partnerid;
|
||||
data['Locationid'] = locationid;
|
||||
data['orderheaderid'] = orderheaderid;
|
||||
data['deliveryid'] = deliveryid;
|
||||
data['userid'] = userid;
|
||||
data['orderid'] = orderid;
|
||||
data['logdate'] = logdate;
|
||||
data['orderstatus'] = orderstatus;
|
||||
data['latitude'] = latitude;
|
||||
data['longitude'] = longitude;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class DeliverySummaryModel {
|
||||
int? total;
|
||||
int? created;
|
||||
int? pending;
|
||||
int? accepted;
|
||||
int? picked;
|
||||
int? delivered;
|
||||
int? cancelled;
|
||||
|
||||
DeliverySummaryModel({
|
||||
this.total,
|
||||
this.created,
|
||||
this.pending,
|
||||
this.accepted,
|
||||
this.picked,
|
||||
this.delivered,
|
||||
this.cancelled,
|
||||
});
|
||||
|
||||
factory DeliverySummaryModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeliverySummaryModel(
|
||||
total: json['total'],
|
||||
created: json['created'],
|
||||
pending: json['pending'],
|
||||
accepted: json['accepted'],
|
||||
picked: json['picked'],
|
||||
delivered: json['delivered'],
|
||||
cancelled: json['cancelled'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['total'] = total;
|
||||
data['created'] = created;
|
||||
data['pending'] = pending;
|
||||
data['accepted'] = accepted;
|
||||
data['picked'] = picked;
|
||||
data['delivered'] = delivered;
|
||||
data['cancelled'] = cancelled;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class DeliveryItem {
|
||||
int? logid;
|
||||
String? logdate;
|
||||
int? tenantid;
|
||||
int? locationid;
|
||||
int? orderheaderid;
|
||||
int? deliveryid;
|
||||
int? userid;
|
||||
int? partnerid;
|
||||
String? orderid;
|
||||
String? orderstatus;
|
||||
String? latitude;
|
||||
String? longitude;
|
||||
int? logstatus;
|
||||
|
||||
DeliveryItem({
|
||||
this.logid,
|
||||
this.logdate,
|
||||
this.tenantid,
|
||||
this.locationid,
|
||||
this.orderheaderid,
|
||||
this.deliveryid,
|
||||
this.userid,
|
||||
this.partnerid,
|
||||
this.orderid,
|
||||
this.orderstatus,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.logstatus,
|
||||
});
|
||||
|
||||
factory DeliveryItem.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryItem(
|
||||
logid: json['logid'],
|
||||
logdate: json['logdate'],
|
||||
tenantid: json['tenantid'],
|
||||
locationid: json['locationid'],
|
||||
orderheaderid: json['orderheaderid'],
|
||||
deliveryid: json['deliveryid'],
|
||||
userid: json['userid'],
|
||||
partnerid: json['partnerid'],
|
||||
orderid: json['orderid'],
|
||||
orderstatus: json['orderstatus'],
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
logstatus: json['logstatus'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['logid'] = logid;
|
||||
data['logdate'] = logdate;
|
||||
data['tenantid'] = tenantid;
|
||||
data['locationid'] = locationid;
|
||||
data['orderheaderid'] = orderheaderid;
|
||||
data['deliveryid'] = deliveryid;
|
||||
data['userid'] = userid;
|
||||
data['partnerid'] = partnerid;
|
||||
data['orderid'] = orderid;
|
||||
data['orderstatus'] = orderstatus;
|
||||
data['latitude'] = latitude;
|
||||
data['longitude'] = longitude;
|
||||
data['logstatus'] = logstatus;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
136
lib/Models/login/login.dart
Normal file
136
lib/Models/login/login.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
class Login {
|
||||
int? code;
|
||||
String? message;
|
||||
bool? status;
|
||||
|
||||
int? userid;
|
||||
String? contactno;
|
||||
String? devicetype;
|
||||
int? configid;
|
||||
String? deviceid;
|
||||
String? userfcmtoken;
|
||||
String? authname;
|
||||
int? authmode;
|
||||
int? roleid;
|
||||
String? firstname;
|
||||
String? lastname;
|
||||
String? fullname;
|
||||
String? password;
|
||||
String? email;
|
||||
String? address;
|
||||
String? suburb;
|
||||
String? city;
|
||||
String? state;
|
||||
String? postcode;
|
||||
int? pin;
|
||||
int? shiftid;
|
||||
int? logid;
|
||||
int? partnerid;
|
||||
int? tenantid;
|
||||
int? riderid;
|
||||
int? deliveryradius;
|
||||
int? locationid;
|
||||
int? applocationid;
|
||||
|
||||
Login({
|
||||
this.code,
|
||||
this.message,
|
||||
this.status,
|
||||
this.userid,
|
||||
this.contactno,
|
||||
this.devicetype,
|
||||
this.configid,
|
||||
this.deviceid,
|
||||
this.userfcmtoken,
|
||||
this.authname,
|
||||
this.authmode,
|
||||
this.roleid,
|
||||
this.firstname,
|
||||
this.lastname,
|
||||
this.fullname,
|
||||
this.password,
|
||||
this.email,
|
||||
this.address,
|
||||
this.suburb,
|
||||
this.city,
|
||||
this.state,
|
||||
this.postcode,
|
||||
this.pin,
|
||||
this.shiftid,
|
||||
this.logid,
|
||||
this.partnerid,
|
||||
this.tenantid,
|
||||
this.riderid,
|
||||
this.deliveryradius,
|
||||
this.locationid,
|
||||
this.applocationid,
|
||||
});
|
||||
|
||||
/// Factory: builds a Login model from JSON (supports both "data" and "details")
|
||||
factory Login.fromJson(Map<String, dynamic> json) {
|
||||
// Safely extract nested data from either "data" or "details"
|
||||
final Map<String, dynamic> nested =
|
||||
(json['details'] ?? json['data'] ?? <String, dynamic>{});
|
||||
|
||||
final dynamic topAuthMode = json['authmode'];
|
||||
final dynamic nestedAuthMode = nested['authmode'];
|
||||
|
||||
return Login(
|
||||
code: json['code'] is int
|
||||
? json['code']
|
||||
: int.tryParse('${json['code']}'),
|
||||
message: json['message']?.toString(),
|
||||
status: json['status'] is bool ? json['status'] : json['status'] == 1,
|
||||
userid: json['userid'] ?? nested['userid'],
|
||||
contactno:
|
||||
json['contactno']?.toString() ?? nested['contactno']?.toString(),
|
||||
devicetype: json['devicetype']?.toString(),
|
||||
configid: json['configid'] ?? nested['configid'],
|
||||
deviceid: json['deviceid']?.toString(),
|
||||
userfcmtoken:
|
||||
json['userfcmtoken']?.toString() ?? nested['userfcmtoken']?.toString(),
|
||||
authname: (json['authname'] ?? nested['authname'])?.toString(),
|
||||
authmode: topAuthMode is int
|
||||
? topAuthMode
|
||||
: (nestedAuthMode is int
|
||||
? nestedAuthMode
|
||||
: int.tryParse('$topAuthMode')),
|
||||
roleid: nested['roleid'],
|
||||
firstname: nested['firstname']?.toString(),
|
||||
lastname: nested['lastname']?.toString(),
|
||||
fullname: nested['fullname']?.toString(),
|
||||
password: nested['password']?.toString(),
|
||||
email: nested['email']?.toString(),
|
||||
address: nested['address']?.toString(),
|
||||
suburb: nested['suburb']?.toString(),
|
||||
city: nested['city']?.toString(),
|
||||
state: nested['state']?.toString(),
|
||||
postcode: nested['postcode']?.toString(),
|
||||
pin: nested['pin'],
|
||||
shiftid: nested['shiftid'],
|
||||
logid: nested['logid'],
|
||||
partnerid: nested['partnerid'],
|
||||
tenantid: nested['tenantid'],
|
||||
riderid: nested['riderid'],
|
||||
deliveryradius: nested['deliveryradius'],
|
||||
locationid: nested['locationid'] is int
|
||||
? nested['locationid']
|
||||
: int.tryParse('${nested['locationid'] ?? 0}'),
|
||||
applocationid: nested['applocationid'] is int
|
||||
? nested['applocationid']
|
||||
: int.tryParse('${nested['applocationid'] ?? 0}'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert this model to JSON for sending to the backend
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (contactno != null) data['contactno'] = contactno;
|
||||
if (devicetype != null) data['devicetype'] = devicetype;
|
||||
if (configid != null) data['configid'] = configid;
|
||||
if (deviceid != null) data['deviceid'] = deviceid;
|
||||
if (userfcmtoken != null) data['userfcmtoken'] = userfcmtoken;
|
||||
if (pin != null) data['pin'] = pin;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
139
lib/Models/notification/notification_models.dart
Normal file
139
lib/Models/notification/notification_models.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// Consolidated notification models
|
||||
|
||||
class AdminNotification {
|
||||
String? priority;
|
||||
List<String>? registrationIds;
|
||||
String? accessid;
|
||||
String? title;
|
||||
String? body;
|
||||
String? sound;
|
||||
|
||||
AdminNotification({
|
||||
this.priority,
|
||||
this.registrationIds,
|
||||
this.accessid,
|
||||
this.title,
|
||||
this.body,
|
||||
this.sound,
|
||||
});
|
||||
|
||||
factory AdminNotification.fromJson(Map<String, dynamic> json) {
|
||||
return AdminNotification(
|
||||
priority: json['priority'] as String?,
|
||||
registrationIds: json['registration_ids']?.cast<String>(),
|
||||
accessid: json['data']?['accessid'] as String?,
|
||||
title: json['notification']?['title'] as String?,
|
||||
body: json['notification']?['body'] as String?,
|
||||
sound: json['notification']?['sound'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['priority'] = priority;
|
||||
data['registration_ids'] = registrationIds;
|
||||
if (accessid != null) {
|
||||
data['data'] = <String, dynamic>{'accessid': accessid};
|
||||
}
|
||||
if (title != null || body != null || sound != null) {
|
||||
data['notification'] = <String, dynamic>{
|
||||
if (title != null) 'title': title,
|
||||
if (body != null) 'body': body,
|
||||
if (sound != null) 'sound': sound,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationMessage {
|
||||
String? sender;
|
||||
String? accessid;
|
||||
String? priority;
|
||||
String? to;
|
||||
String? title;
|
||||
String? body;
|
||||
String? sound;
|
||||
|
||||
NotificationMessage({
|
||||
this.sender,
|
||||
this.accessid,
|
||||
this.priority,
|
||||
this.to,
|
||||
this.title,
|
||||
this.body,
|
||||
this.sound,
|
||||
});
|
||||
|
||||
factory NotificationMessage.fromJson(Map<String, dynamic> json) {
|
||||
return NotificationMessage(
|
||||
sender: json['sender'] as String?,
|
||||
accessid: json['accessid'] as String?,
|
||||
priority: json['notification']?['priority'] as String?,
|
||||
to: json['notification']?['to'] as String?,
|
||||
title: json['notification']?['notification']?['title'] as String?,
|
||||
body: json['notification']?['notification']?['body'] as String?,
|
||||
sound: json['notification']?['notification']?['sound'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['sender'] = sender;
|
||||
data['accessid'] = accessid;
|
||||
if (priority != null || to != null || title != null || body != null || sound != null) {
|
||||
data['notification'] = <String, dynamic>{
|
||||
if (priority != null) 'priority': priority,
|
||||
if (to != null) 'to': to,
|
||||
if (title != null || body != null || sound != null) 'notification': <String, dynamic>{
|
||||
if (title != null) 'title': title,
|
||||
if (body != null) 'body': body,
|
||||
if (sound != null) 'sound': sound,
|
||||
},
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RiderNotification {
|
||||
String? token;
|
||||
String? title;
|
||||
String? body;
|
||||
String? sound;
|
||||
String? image;
|
||||
|
||||
RiderNotification({
|
||||
this.token,
|
||||
this.title,
|
||||
this.body,
|
||||
this.sound,
|
||||
this.image,
|
||||
});
|
||||
|
||||
factory RiderNotification.fromJson(Map<String, dynamic> json) {
|
||||
return RiderNotification(
|
||||
token: json['token'] as String?,
|
||||
title: json['notification']?['title'] as String?,
|
||||
body: json['notification']?['body'] as String?,
|
||||
sound: json['notification']?['sound'] as String?,
|
||||
image: json['notification']?['image'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['token'] = token;
|
||||
if (title != null || body != null || sound != null || image != null) {
|
||||
data['notification'] = <String, dynamic>{
|
||||
if (title != null) 'title': title,
|
||||
if (body != null) 'body': body,
|
||||
if (sound != null) 'sound': sound,
|
||||
if (image != null) 'image': image,
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
289
lib/Models/riders/riders_models.dart
Normal file
289
lib/Models/riders/riders_models.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
// Consolidated riders models
|
||||
|
||||
class RiderLog {
|
||||
int? logid;
|
||||
String? logdate;
|
||||
int? userid;
|
||||
int? partnerid;
|
||||
int? shiftid;
|
||||
double? shifthours;
|
||||
String? login;
|
||||
String? latitude;
|
||||
double? workhours;
|
||||
double? shorthours;
|
||||
int? logstatus;
|
||||
String? longitude;
|
||||
double? breakhours;
|
||||
|
||||
int? onduty;
|
||||
int? tenantid;
|
||||
int? locationid;
|
||||
int? applocationid;
|
||||
String? userfcmtoken;
|
||||
|
||||
RiderLog({
|
||||
this.logid,
|
||||
this.logdate,
|
||||
this.userid,
|
||||
this.partnerid,
|
||||
this.shiftid,
|
||||
this.shifthours,
|
||||
this.login,
|
||||
this.latitude,
|
||||
this.workhours,
|
||||
this.shorthours,
|
||||
this.logstatus,
|
||||
this.longitude,
|
||||
this.breakhours,
|
||||
this.onduty,
|
||||
this.tenantid,
|
||||
this.locationid,
|
||||
this.applocationid,
|
||||
this.userfcmtoken,
|
||||
});
|
||||
|
||||
factory RiderLog.fromJson(Map<String, dynamic> json) {
|
||||
return RiderLog(
|
||||
logid: json['logid'],
|
||||
logdate: json['logdate'],
|
||||
userid: json['userid'],
|
||||
partnerid: json['partnerid'],
|
||||
shiftid: json['shiftid'],
|
||||
shifthours: (json['shifthours'] as num?)?.toDouble(),
|
||||
login: json['login'],
|
||||
latitude: json['latitude'],
|
||||
workhours: (json['workhours'] as num?)?.toDouble(),
|
||||
shorthours: (json['shorthours'] as num?)?.toDouble(),
|
||||
logstatus: json['logstatus'],
|
||||
longitude: json['longitude'],
|
||||
breakhours: (json['breakhours'] as num?)?.toDouble(),
|
||||
onduty: json['onduty'],
|
||||
tenantid: json['tenantid'],
|
||||
locationid: json['locationid'],
|
||||
applocationid: json['applocationid'],
|
||||
userfcmtoken: json['userfcmtoken'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['logid'] = logid;
|
||||
data['logdate'] = logdate;
|
||||
data['userid'] = userid;
|
||||
data['partnerid'] = partnerid;
|
||||
data['shiftid'] = shiftid;
|
||||
data['shifthours'] = shifthours;
|
||||
data['login'] = login;
|
||||
data['latitude'] = latitude;
|
||||
data['workhours'] = workhours;
|
||||
data['shorthours'] = shorthours;
|
||||
data['logstatus'] = logstatus;
|
||||
data['longitude'] = longitude;
|
||||
data['breakhours'] = breakhours;
|
||||
data['onduty'] = onduty;
|
||||
data['tenantid'] = tenantid;
|
||||
data['locationid'] = locationid;
|
||||
data['applocationid'] = applocationid;
|
||||
data['userfcmtoken'] = userfcmtoken;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RiderBreak {
|
||||
int? breakid;
|
||||
int? logid;
|
||||
String? breakdate;
|
||||
int? userid;
|
||||
int? partnerid;
|
||||
int? shiftid;
|
||||
String? breakstart;
|
||||
String? breakend;
|
||||
double? breakhours;
|
||||
String? latitude;
|
||||
String? longitude;
|
||||
|
||||
RiderBreak({
|
||||
this.breakid,
|
||||
this.logid,
|
||||
this.breakdate,
|
||||
this.userid,
|
||||
this.partnerid,
|
||||
this.shiftid,
|
||||
this.breakstart,
|
||||
this.breakend,
|
||||
this.breakhours,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
});
|
||||
|
||||
factory RiderBreak.fromJson(Map<String, dynamic> json) {
|
||||
return RiderBreak(
|
||||
breakid: json['breakid'],
|
||||
logid: json['logid'],
|
||||
breakdate: json['breakdate'],
|
||||
userid: json['userid'],
|
||||
partnerid: json['partnerid'],
|
||||
shiftid: json['shiftid'],
|
||||
breakstart: json['breakstart'],
|
||||
breakend: json['breakend'],
|
||||
breakhours: (json['breakhours'] as num?)?.toDouble(),
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['breakid'] = breakid;
|
||||
data['logid'] = logid;
|
||||
data['breakdate'] = breakdate;
|
||||
data['userid'] = userid;
|
||||
data['partnerid'] = partnerid;
|
||||
data['shiftid'] = shiftid;
|
||||
data['breakstart'] = breakstart;
|
||||
data['breakend'] = breakend;
|
||||
data['breakhours'] = breakhours;
|
||||
data['latitude'] = latitude;
|
||||
data['longitude'] = longitude;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RiderLogin {
|
||||
int? logid;
|
||||
int? userid;
|
||||
int? partnerid;
|
||||
int? shiftid;
|
||||
String? logdate;
|
||||
String? login;
|
||||
double? shifthours;
|
||||
String? latitude;
|
||||
String? longitude;
|
||||
int? onduty;
|
||||
String? status; // "active" when there are active deliveries, "idle" otherwise
|
||||
String? username; // Rider display name for rider logs
|
||||
String? contactno;
|
||||
int? tenantid;
|
||||
int? locationid;
|
||||
int? applocationid;
|
||||
String? userfcmtoken;
|
||||
|
||||
RiderLogin({
|
||||
this.logid,
|
||||
this.userid,
|
||||
this.partnerid,
|
||||
this.shiftid,
|
||||
this.logdate,
|
||||
this.login,
|
||||
this.shifthours,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.onduty,
|
||||
this.status,
|
||||
this.username,
|
||||
|
||||
this.contactno,
|
||||
this.tenantid,
|
||||
this.locationid,
|
||||
this.applocationid,
|
||||
this.userfcmtoken,
|
||||
});
|
||||
|
||||
factory RiderLogin.fromJson(Map<String, dynamic> json) {
|
||||
return RiderLogin(
|
||||
logid: json['logid'],
|
||||
userid: json['userid'],
|
||||
partnerid: json['partnerid'],
|
||||
shiftid: json['shiftid'],
|
||||
logdate: json['logdate'],
|
||||
login: json['Login'] ?? json['login'],
|
||||
shifthours: (json['shifthours'] as num?)?.toDouble(),
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
onduty: json['onduty'],
|
||||
status: json['status'],
|
||||
username: json['username'],
|
||||
contactno: json['contactno'],
|
||||
tenantid: json['tenantid'],
|
||||
locationid: json['locationid'],
|
||||
applocationid: json['applocationid'],
|
||||
userfcmtoken: json['userfcmtoken'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['logid'] = logid;
|
||||
data['userid'] = userid;
|
||||
data['partnerid'] = partnerid;
|
||||
data['shiftid'] = shiftid;
|
||||
data['logdate'] = logdate;
|
||||
data['Login'] = login;
|
||||
data['shifthours'] = shifthours;
|
||||
data['latitude'] = latitude;
|
||||
data['longitude'] = longitude;
|
||||
if (onduty != null) data['onduty'] = onduty;
|
||||
if (status != null) data['status'] = status;
|
||||
if (username != null && username!.trim().isNotEmpty) {
|
||||
data['username'] = username;
|
||||
}
|
||||
if (contactno != null) {
|
||||
data['contactno'] = contactno;
|
||||
}
|
||||
data['tenantid'] = tenantid;
|
||||
data['locationid'] = locationid;
|
||||
data['applocationid'] = applocationid;
|
||||
data['userfcmtoken'] = userfcmtoken;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RiderUpdate {
|
||||
int? userid;
|
||||
int? logstatus;
|
||||
String? logout;
|
||||
double? workhours;
|
||||
double? shorthours;
|
||||
String? latitude;
|
||||
String? longitude;
|
||||
int? onduty;
|
||||
|
||||
RiderUpdate({
|
||||
this.userid,
|
||||
this.logstatus,
|
||||
this.logout,
|
||||
this.workhours,
|
||||
this.shorthours,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.onduty,
|
||||
});
|
||||
|
||||
factory RiderUpdate.fromJson(Map<String, dynamic> json) {
|
||||
return RiderUpdate(
|
||||
userid: json['userid'],
|
||||
logstatus: json['logstatus'],
|
||||
logout: json['Logout'] ?? json['logout'],
|
||||
workhours: (json['workhours'] as num?)?.toDouble(),
|
||||
shorthours: (json['shorthours'] as num?)?.toDouble(),
|
||||
latitude: json['latitude'],
|
||||
longitude: json['longitude'],
|
||||
onduty: json['onduty'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['userid'] = userid;
|
||||
data['logstatus'] = logstatus;
|
||||
data['Logout'] = logout;
|
||||
data['workhours'] = workhours;
|
||||
data['shorthours'] = shorthours;
|
||||
data['latitude'] = latitude;
|
||||
data['longitude'] = longitude;
|
||||
if (onduty != null) data['onduty'] = onduty;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
lib/Models/summary/deliverystats.dart
Normal file
27
lib/Models/summary/deliverystats.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
class DeliveryStats {
|
||||
final int today;
|
||||
final int week;
|
||||
final int month;
|
||||
final int total;
|
||||
final int cancelled;
|
||||
|
||||
DeliveryStats({
|
||||
required this.today,
|
||||
required this.week,
|
||||
required this.month,
|
||||
required this.total,
|
||||
required this.cancelled,
|
||||
});
|
||||
|
||||
factory DeliveryStats.fromJson(Map<String, dynamic> json) {
|
||||
return DeliveryStats(
|
||||
today: json['today'] ?? 0,
|
||||
week: json['week'] ?? 0,
|
||||
month: json['month'] ?? 0,
|
||||
total: json['total'] ?? 0,
|
||||
cancelled: json['cancelled'] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
lib/Models/summary/riderweeklykms.dart
Normal file
49
lib/Models/summary/riderweeklykms.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
class RiderWeeklyKms {
|
||||
final String day;
|
||||
final double kms;
|
||||
|
||||
RiderWeeklyKms({
|
||||
required this.day,
|
||||
required this.kms,
|
||||
});
|
||||
|
||||
factory RiderWeeklyKms.fromJson(Map<String, dynamic> json) {
|
||||
return RiderWeeklyKms(
|
||||
day: json['day'] ?? '',
|
||||
kms: double.tryParse('${json['kms'] ?? 0}') ?? 0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RiderWeeklyKmsResponse {
|
||||
final bool status;
|
||||
final int code;
|
||||
final String message;
|
||||
final double totalKms;
|
||||
final double overallKms;
|
||||
final List<RiderWeeklyKms> details;
|
||||
|
||||
RiderWeeklyKmsResponse({
|
||||
required this.status,
|
||||
required this.code,
|
||||
required this.message,
|
||||
required this.totalKms,
|
||||
required this.overallKms,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
factory RiderWeeklyKmsResponse.fromJson(Map<String, dynamic> json) {
|
||||
return RiderWeeklyKmsResponse(
|
||||
status: json['status'] ?? false,
|
||||
code: json['code'] ?? 0,
|
||||
message: json['message'] ?? '',
|
||||
totalKms: double.tryParse('${json['total_kms'] ?? 0}') ?? 0.0,
|
||||
overallKms: double.tryParse('${json['overall_kms'] ?? 0}') ?? 0.0,
|
||||
details: (json['details'] as List<dynamic>? ?? [])
|
||||
.map((e) => RiderWeeklyKms.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
lib/Models/supportticket/support_ticket.dart
Normal file
37
lib/Models/supportticket/support_ticket.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
class SupportTicketModel {
|
||||
final int ridersupportid;
|
||||
final int userid;
|
||||
final String category;
|
||||
final String priority;
|
||||
final String subject;
|
||||
final String issue;
|
||||
final String? image;
|
||||
final DateTime created;
|
||||
final DateTime updated;
|
||||
|
||||
SupportTicketModel({
|
||||
required this.ridersupportid,
|
||||
required this.userid,
|
||||
required this.category,
|
||||
required this.priority,
|
||||
required this.subject,
|
||||
required this.issue,
|
||||
this.image,
|
||||
required this.created,
|
||||
required this.updated,
|
||||
});
|
||||
|
||||
factory SupportTicketModel.fromJson(Map<String, dynamic> json) {
|
||||
return SupportTicketModel(
|
||||
ridersupportid: json['ridersupportid'] as int,
|
||||
userid: json['userid'] as int,
|
||||
category: json['category'] as String,
|
||||
priority: json['priority'] as String,
|
||||
subject: json['subject'] as String,
|
||||
issue: json['issue'] as String,
|
||||
image: json['image'] as String?,
|
||||
created: DateTime.parse(json['created'] as String),
|
||||
updated: DateTime.parse(json['updated'] as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
969
lib/background/backgroundservice.dart
Normal file
969
lib/background/backgroundservice.dart
Normal file
@@ -0,0 +1,969 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:nearle/providers/deliverylog/deliverylog_provider.dart';
|
||||
import 'package:nearle/providers/notifications/notificationservce.dart';
|
||||
import 'package:nearle/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:nearle/utils/kalman_filter.dart';
|
||||
|
||||
/// Background service for managing active delivery logs
|
||||
class BackgroundDeliveryLog {
|
||||
static NearleKalmanFilter? _kf;
|
||||
static DateTime? _lastUpdateTime;
|
||||
|
||||
static final CreateDeliveryLogProvider _logProvider =
|
||||
CreateDeliveryLogProvider();
|
||||
static final http.Client _httpClient = http.Client();
|
||||
static final FlutterTts _tts = FlutterTts();
|
||||
static final AudioPlayer _proximityPlayer = AudioPlayer();
|
||||
|
||||
static const double _idleThresholdMeters = 10; // ~5-10m tolerance
|
||||
static const double _proximityThresholdMeters = 50.0; // 50 meters for arrival alert
|
||||
static const String _activeDeliveriesKey = 'active_deliveries';
|
||||
static const String _payloadKeyPrefix = 'delivery_payload_';
|
||||
static const String _offlineLogKey = 'offline_delivery_logs';
|
||||
static const String _proximityAlertKeyPrefix = 'delivery_proximity_alerted_';
|
||||
|
||||
static bool _isPosting = false;
|
||||
static bool _ttsReady = false;
|
||||
static bool _audioReady = false;
|
||||
|
||||
/// Process active deliveries: fetch from API, filter active, and post logs
|
||||
static Future<void> processActiveDeliveries() async {
|
||||
if (_isPosting) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Already posting, skipping...');
|
||||
return;
|
||||
}
|
||||
_isPosting = true;
|
||||
|
||||
try {
|
||||
// Get userid from SharedPreferences
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Checking for user: $userId');
|
||||
|
||||
if (userId == 0) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] ❌ No user ID found in prefs');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current date dynamically (YYYY-MM-DD format)
|
||||
final now = DateTime.now();
|
||||
final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}';
|
||||
|
||||
// Build API URL using v1 endpoint (getdeliveries)
|
||||
final bool isLive = ApiConstants.mainRoute == 'live';
|
||||
final baseUrl = isLive
|
||||
? ApiConstants.currentDeliveryLive
|
||||
: ApiConstants.currentDeliveryDev;
|
||||
|
||||
final uri = Uri.parse(baseUrl).replace(
|
||||
queryParameters: {
|
||||
'userid': userId.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Fetching deliveries: $uri');
|
||||
|
||||
// Fetch deliveries from API using v1 endpoint
|
||||
final deliveries = await _fetchDeliveriesFromApi(uri);
|
||||
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] API returned ${deliveries.length} items');
|
||||
|
||||
// Filter for active orders only
|
||||
final activeOrders = deliveries.whereType<Map<String, dynamic>>().where((
|
||||
order,
|
||||
) {
|
||||
final status = (order['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isActive = status == 'active';
|
||||
return isActive;
|
||||
}).toList();
|
||||
|
||||
if (activeOrders.isEmpty) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] No active orders found');
|
||||
// ✅ Still check shift end even if no active deliveries
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] 🔍 Checking shift end time...');
|
||||
await checkShiftEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG] Found ${activeOrders.length} active deliveries',
|
||||
);
|
||||
|
||||
// Post logs for each active delivery
|
||||
for (final order in activeOrders) {
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
if (orderId.isEmpty) continue;
|
||||
|
||||
// Get current coordinates once per order to reuse for proximity + log
|
||||
final Map<String, String>? coords = await _getCoordinatesWithFallback();
|
||||
if (coords != null) {
|
||||
await _checkProximityAlert(order, coords);
|
||||
}
|
||||
|
||||
// Get payload from SharedPreferences or create new one
|
||||
Map<String, dynamic>? payload = await _loadPayload(orderId);
|
||||
if (payload == null) {
|
||||
// Create new payload from order data with userId from SharedPreferences
|
||||
payload = _createPayload(order, userId);
|
||||
await _persistPayload(orderId, payload);
|
||||
await _markAsActive(orderId);
|
||||
} else {
|
||||
// Ensure userid is set correctly in existing payload
|
||||
payload['userid'] = userId;
|
||||
}
|
||||
|
||||
// Post the log
|
||||
await _postDeliveryLog(orderId, payload, currentCoords: coords);
|
||||
}
|
||||
|
||||
// ✅ CRITICAL: Check if shift end time has passed (backup to alarm)
|
||||
await checkShiftEnd();
|
||||
|
||||
// Attempt to flush offline logs
|
||||
await _flushOfflineLogs();
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Error processing: $e');
|
||||
} finally {
|
||||
_isPosting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Post delivery log for a specific order
|
||||
static Future<void> _postDeliveryLog(
|
||||
String orderId,
|
||||
Map<String, dynamic> payload, {
|
||||
Map<String, String>? currentCoords,
|
||||
}) async {
|
||||
Map<String, dynamic>? payloadWithCoords;
|
||||
try {
|
||||
// Get current coordinates
|
||||
final coords = currentCoords ?? await _getCoordinatesWithFallback();
|
||||
if (coords == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Distance calculation is now fully handled by LiveTrackingService in the background.
|
||||
// 03-14: Removed redundant 60-second background calculations to avoid logic conflicts.
|
||||
final deliveryId = payload['deliveryid']?.toString() ?? '';
|
||||
|
||||
|
||||
// Create log date timestamp
|
||||
final now = DateTime.now();
|
||||
final logDate =
|
||||
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
|
||||
// Retrieve final cumulative KM to send
|
||||
double riderKmsToSend = 0.0;
|
||||
if (deliveryId.isNotEmpty && deliveryId != '0') {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.reload(); // Force reload so we get the latest value written by the main isolate
|
||||
riderKmsToSend = double.tryParse(prefs.getString('delivery_tracking_${deliveryId}_cumulativeKm') ?? '0') ?? 0.0;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Build payload with coordinates and timestamp
|
||||
payloadWithCoords = <String, dynamic>{
|
||||
...payload,
|
||||
'logdate': logDate,
|
||||
'latitude': coords['lat'] ?? '0',
|
||||
'longitude': coords['lng'] ?? '0',
|
||||
'raw_latitude': coords['raw_lat'] ?? '0',
|
||||
'raw_longitude': coords['raw_lng'] ?? '0',
|
||||
'velocity_lat': coords['velocity_lat'] ?? '0',
|
||||
'velocity_lng': coords['velocity_lng'] ?? '0',
|
||||
'speed': coords['speed'] ?? '0',
|
||||
'heading': coords['heading'] ?? '0',
|
||||
'riderkms': riderKmsToSend,
|
||||
'logstatus': await _resolveLogStatus(orderId, coords),
|
||||
};
|
||||
|
||||
// Determine API endpoint
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
// Post the log
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Posting log for $orderId');
|
||||
|
||||
final result = await _logProvider
|
||||
.createDeliveryLog(url, payloadWithCoords)
|
||||
.timeout(
|
||||
const Duration(seconds: 8),
|
||||
onTimeout: () => throw TimeoutException(
|
||||
'Delivery log post timeout for $orderId',
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Success for $orderId');
|
||||
await _persistLastLogLocation(orderId, coords);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG] Warning: No response for $orderId',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG] Failed to post log for $orderId: $e',
|
||||
);
|
||||
// Offline fallback
|
||||
if (payloadWithCoords != null) {
|
||||
await _saveToOfflineQueue(orderId, payloadWithCoords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create payload from delivery order data
|
||||
static Map<String, dynamic> _createPayload(
|
||||
Map<String, dynamic> delivery,
|
||||
int userId,
|
||||
) {
|
||||
return <String, dynamic>{
|
||||
'logid': 0,
|
||||
'tenantid': delivery['tenantid'] ?? 0,
|
||||
'partnerid': delivery['partnerid'] ?? 0,
|
||||
'locationid': delivery['locationid'] ?? 0,
|
||||
'orderheaderid': delivery['orderheaderid'] ?? 0,
|
||||
'deliveryid': delivery['deliveryid'] ?? 0,
|
||||
'userid': userId,
|
||||
'orderid': (delivery['orderid'] ?? '').toString(),
|
||||
'orderstatus': 'active',
|
||||
};
|
||||
}
|
||||
|
||||
/// Get coordinates with fallback (current position -> last known -> cached)
|
||||
static Future<Map<String, String>?> _getCoordinatesWithFallback() async {
|
||||
Map<String, String> result = {
|
||||
'lat': '0',
|
||||
'lng': '0',
|
||||
'raw_lat': '0',
|
||||
'raw_lng': '0',
|
||||
'speed': '0',
|
||||
'heading': '0',
|
||||
'velocity_lat': '0',
|
||||
'velocity_lng': '0',
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. Check if location services are enabled
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Location services disabled');
|
||||
return await _getCachedCoordinatesAsMap();
|
||||
}
|
||||
|
||||
// 2. Check permissions
|
||||
final permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Location permission denied');
|
||||
return await _getCachedCoordinatesAsMap();
|
||||
}
|
||||
|
||||
Position? position;
|
||||
try {
|
||||
position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
),
|
||||
).timeout(const Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Failed to get current pos: $e');
|
||||
position = await Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
if (position != null) {
|
||||
// Reject mocked GPS (anti-cheat)
|
||||
if (position.isMocked) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Mocked position — using cached');
|
||||
return await _getCachedCoordinatesAsMap();
|
||||
}
|
||||
// Reject very poor accuracy to avoid phantom movements in delivery logs
|
||||
if (position.accuracy > 50.0) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG] Low-accuracy position (${position.accuracy.toStringAsFixed(0)}m) — using cached',
|
||||
);
|
||||
return await _getCachedCoordinatesAsMap();
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
double outLat = position.latitude;
|
||||
double outLng = position.longitude;
|
||||
double speed = position.speed;
|
||||
double heading = position.heading;
|
||||
|
||||
// Decompose velocity for Kalman
|
||||
final double headingRadians = heading * (math.pi / 180.0);
|
||||
final double velocityLng = speed * math.sin(headingRadians);
|
||||
final double velocityLat = speed * math.cos(headingRadians);
|
||||
|
||||
if (_kf == null) {
|
||||
_kf = NearleKalmanFilter(lat: outLat, lng: outLng);
|
||||
} else {
|
||||
final double dt = _lastUpdateTime != null
|
||||
? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0
|
||||
: 30.0;
|
||||
|
||||
_kf!.predict(dt);
|
||||
_kf!.update(outLat, outLng);
|
||||
outLat = _kf!.x[0];
|
||||
outLng = _kf!.x[1];
|
||||
}
|
||||
_lastUpdateTime = now;
|
||||
|
||||
result = {
|
||||
'lat': outLat.toString(),
|
||||
'lng': outLng.toString(),
|
||||
'raw_lat': position.latitude.toString(),
|
||||
'raw_lng': position.longitude.toString(),
|
||||
'speed': speed.toStringAsFixed(2),
|
||||
'heading': heading.toStringAsFixed(2),
|
||||
'velocity_lat': velocityLat.toStringAsFixed(4),
|
||||
'velocity_lng': velocityLng.toStringAsFixed(4),
|
||||
};
|
||||
|
||||
await _cacheCoordinates(result['lat']!, result['lng']!);
|
||||
return result;
|
||||
}
|
||||
|
||||
return await _getCachedCoordinatesAsMap();
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Error getting coords: $e');
|
||||
return await _getCachedCoordinatesAsMap();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, String>?> _getCachedCoordinatesAsMap() async {
|
||||
final coords = await _getCachedCoordinates();
|
||||
if (coords == null) return null;
|
||||
return {
|
||||
'lat': coords.$1,
|
||||
'lng': coords.$2,
|
||||
'raw_lat': coords.$1,
|
||||
'raw_lng': coords.$2,
|
||||
'speed': '0',
|
||||
'heading': '0',
|
||||
'velocity_lat': '0',
|
||||
'velocity_lng': '0',
|
||||
};
|
||||
}
|
||||
|
||||
/// Cache coordinates to SharedPreferences
|
||||
static Future<void> _cacheCoordinates(String lat, String lng) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('last_lat', lat);
|
||||
await prefs.setString('last_lng', lng);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Get cached coordinates from SharedPreferences
|
||||
static Future<(String, String)?> _getCachedCoordinates() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lat = (prefs.getString('last_lat') ?? '').trim();
|
||||
final lng = (prefs.getString('last_lng') ?? '').trim();
|
||||
if (lat.isNotEmpty && lng.isNotEmpty) {
|
||||
return (lat, lng);
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Load payload from SharedPreferences
|
||||
static Future<Map<String, dynamic>?> _loadPayload(String orderId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString('$_payloadKeyPrefix$orderId');
|
||||
if (jsonStr == null || jsonStr.isEmpty) return null;
|
||||
final decoded = jsonDecode(jsonStr);
|
||||
if (decoded is Map<String, dynamic>) return decoded;
|
||||
if (decoded is Map) return decoded.cast<String, dynamic>();
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Persist payload to SharedPreferences
|
||||
static Future<void> _persistPayload(
|
||||
String orderId,
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('$_payloadKeyPrefix$orderId', jsonEncode(payload));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Mark order as active in SharedPreferences
|
||||
static Future<void> _markAsActive(String orderId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = prefs.getStringList(_activeDeliveriesKey) ?? <String>[];
|
||||
if (!list.contains(orderId)) {
|
||||
list.add(orderId);
|
||||
await prefs.setStringList(_activeDeliveriesKey, list);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// Fetch deliveries from API using v1 endpoint
|
||||
static Future<List<dynamic>> _fetchDeliveriesFromApi(Uri uri) async {
|
||||
try {
|
||||
final res = await _httpClient
|
||||
.get(uri)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) {
|
||||
return data['items'] as List;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to pad numbers with leading zero
|
||||
static String _pad(int n) => n.toString().padLeft(2, '0');
|
||||
|
||||
static Future<int> _resolveLogStatus(
|
||||
String orderId,
|
||||
Map<String, String> coords,
|
||||
) async {
|
||||
try {
|
||||
final last = await _loadLastLogLocation(orderId);
|
||||
if (last != null) {
|
||||
final lastLat = last.$1;
|
||||
final lastLng = last.$2;
|
||||
final currentLat = double.tryParse(coords['lat'] ?? '0') ?? 0;
|
||||
final currentLng = double.tryParse(coords['lng'] ?? '0') ?? 0;
|
||||
if (currentLat != 0 &&
|
||||
currentLng != 0 &&
|
||||
lastLat != 0 &&
|
||||
lastLng != 0) {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
lastLat,
|
||||
lastLng,
|
||||
currentLat,
|
||||
currentLng,
|
||||
);
|
||||
if (distance <= _idleThresholdMeters) {
|
||||
return 1; // idle
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG] logstatus error: $e');
|
||||
}
|
||||
return 0; // moving
|
||||
}
|
||||
|
||||
static Future<(double, double)?> _loadLastLogLocation(String orderId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString('last_log_loc_$orderId');
|
||||
if (jsonStr == null || jsonStr.isEmpty) return null;
|
||||
final decoded = jsonDecode(jsonStr);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final lat = double.tryParse('${decoded['lat']}') ?? 0;
|
||||
final lng = double.tryParse('${decoded['lng']}') ?? 0;
|
||||
if (lat != 0 && lng != 0) return (lat, lng);
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> _persistLastLogLocation(
|
||||
String orderId,
|
||||
Map<String, String> coords,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'last_log_loc_$orderId',
|
||||
jsonEncode({'lat': coords['lat'], 'lng': coords['lng']}),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---------------- Offline Queue Logic ----------------
|
||||
|
||||
static Future<void> _flushOfflineLogs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final List<String> queue = prefs.getStringList(_offlineLogKey) ?? [];
|
||||
if (queue.isEmpty) return;
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Flushing ${queue.length} offline logs...',
|
||||
);
|
||||
|
||||
final List<String> remaining = [];
|
||||
bool anySuccess = false;
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
for (final itemStr in queue) {
|
||||
try {
|
||||
final Map<String, dynamic> item = jsonDecode(itemStr);
|
||||
final String orderId = item['orderId'] ?? '';
|
||||
final Map<String, dynamic> payload = Map<String, dynamic>.from(
|
||||
item['payload'] ?? {},
|
||||
);
|
||||
|
||||
if (payload.isEmpty) continue;
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Retrying for orderId: $orderId',
|
||||
);
|
||||
|
||||
final result = await _logProvider
|
||||
.createDeliveryLog(url, payload)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
|
||||
if (result != null) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Success for orderId: $orderId',
|
||||
);
|
||||
anySuccess = true;
|
||||
} else {
|
||||
remaining.add(itemStr);
|
||||
}
|
||||
} catch (e) {
|
||||
remaining.add(itemStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (anySuccess || remaining.length != queue.length) {
|
||||
await prefs.setStringList(_offlineLogKey, remaining);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Flush error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _saveToOfflineQueue(
|
||||
String orderId,
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final List<String> queue = prefs.getStringList(_offlineLogKey) ?? [];
|
||||
|
||||
final item = jsonEncode({
|
||||
'orderId': orderId,
|
||||
'payload': payload,
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
|
||||
queue.add(item);
|
||||
await prefs.setStringList(_offlineLogKey, queue);
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Saved to queue. Total: ${queue.length}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Error saving to queue: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Auto Shift End Logic ----------------
|
||||
|
||||
/// Check if shift has ended and trigger auto-break if needed
|
||||
static Future<void> checkShiftEnd() async {
|
||||
try {
|
||||
debugPrint('[AUTO_SHIFT_END] 🔍 Starting shift end check...');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// 1. Check if currently On Duty
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
debugPrint('[AUTO_SHIFT_END] 📊 Current onduty status: $onduty');
|
||||
if (onduty != 1) {
|
||||
debugPrint('[AUTO_SHIFT_END] ⏭️ Rider not on duty, skipping check');
|
||||
return; // Already offline
|
||||
}
|
||||
|
||||
// 2. Get Shift Timings
|
||||
final String startTimeStr = prefs.getString('starttime') ?? '';
|
||||
final String endTimeStr = prefs.getString('endtime') ?? '';
|
||||
debugPrint('[AUTO_SHIFT_END] ⏰ Shift times - Start: "$startTimeStr", End: "$endTimeStr"');
|
||||
|
||||
if (endTimeStr.isEmpty) {
|
||||
debugPrint('[AUTO_SHIFT_END] ⚠️ No endtime found, cannot check shift end');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Parse Times (Assumed format HH:mm:ss)
|
||||
final now = DateTime.now();
|
||||
|
||||
final endParts = endTimeStr.split(':');
|
||||
if (endParts.length < 2) return;
|
||||
|
||||
final int endHour = int.tryParse(endParts[0]) ?? 0;
|
||||
final int endMinute = int.tryParse(endParts[1]) ?? 0;
|
||||
final int endSecond = endParts.length > 2 ? (int.tryParse(endParts[2]) ?? 0) : 0;
|
||||
|
||||
// Create DateTime for end time on TODAY
|
||||
final DateTime endToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
endHour,
|
||||
endMinute,
|
||||
endSecond,
|
||||
);
|
||||
|
||||
bool isShiftOver = false;
|
||||
|
||||
if (startTimeStr.isNotEmpty) {
|
||||
final startParts = startTimeStr.split(':');
|
||||
if (startParts.length >= 2) {
|
||||
final int startHour = int.tryParse(startParts[0]) ?? 0;
|
||||
final int startMinute = int.tryParse(startParts[1]) ?? 0;
|
||||
|
||||
// Create DateTime for start time on TODAY
|
||||
final DateTime startToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
startHour,
|
||||
startMinute,
|
||||
);
|
||||
|
||||
// Check for overnight shift (Start > End in 24-hour format, e.g. 22:00 to 06:00)
|
||||
// This means shift crosses midnight
|
||||
final double startVal = startHour + (startMinute / 60.0);
|
||||
final double endVal = endHour + (endMinute / 60.0);
|
||||
|
||||
if (startVal > endVal) {
|
||||
// ✅ OVERNIGHT SHIFT (crosses midnight, e.g. 22:00 to 06:00)
|
||||
// Shift is over if current time is AFTER end time today AND BEFORE start time today
|
||||
// Example: End 06:00, Start 22:00
|
||||
// - Now 23:00 -> After 22:00 (start) -> Still in shift (ACTIVE)
|
||||
// - Now 05:00 -> Before 06:00 (end) -> Still in shift from yesterday (ACTIVE)
|
||||
// - Now 10:00 -> After 06:00 (end) AND Before 22:00 (start) -> Shift OVER
|
||||
|
||||
debugPrint('[AUTO_SHIFT_END] 🌙 Overnight shift detected (Start: ${_pad(startHour)}:${_pad(startMinute)}, End: ${_pad(endHour)}:${_pad(endMinute)})');
|
||||
debugPrint('[AUTO_SHIFT_END] 📅 Current time: ${_pad(now.hour)}:${_pad(now.minute)}');
|
||||
|
||||
if (now.isAfter(endToday) && now.isBefore(startToday)) {
|
||||
// We're in the gap period between end and start -> shift is OVER
|
||||
isShiftOver = true;
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Overnight shift: In gap period -> SHIFT OVER');
|
||||
} else {
|
||||
// We're either before end (still in shift from yesterday) or after start (still in shift today)
|
||||
isShiftOver = false;
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Overnight shift: Still active');
|
||||
}
|
||||
} else {
|
||||
// ✅ NORMAL DAY SHIFT (e.g. 09:00 to 17:00, doesn't cross midnight)
|
||||
// Shift is over if current time is AFTER end time
|
||||
debugPrint('[AUTO_SHIFT_END] ☀️ Day shift detected (Start: ${_pad(startHour)}:${_pad(startMinute)}, End: ${_pad(endHour)}:${_pad(endMinute)})');
|
||||
debugPrint('[AUTO_SHIFT_END] 📅 Current time: ${_pad(now.hour)}:${_pad(now.minute)}, End time: ${_pad(endHour)}:${_pad(endMinute)}');
|
||||
|
||||
if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) {
|
||||
isShiftOver = true;
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Day shift: Shift ended');
|
||||
} else {
|
||||
isShiftOver = false;
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Day shift: Still active');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback if start time parse fails: assume day shift
|
||||
debugPrint('[AUTO_SHIFT_END] ⚠️ Could not parse start time, assuming day shift');
|
||||
if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback if no start time: assume day shift
|
||||
debugPrint('[AUTO_SHIFT_END] ⚠️ No start time provided, assuming day shift');
|
||||
if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Trigger Auto End if Shift is Over
|
||||
debugPrint('[AUTO_SHIFT_END] 📅 Time check result - isShiftOver: $isShiftOver, Current: ${_pad(now.hour)}:${_pad(now.minute)}, End: ${_pad(endHour)}:${_pad(endMinute)}');
|
||||
if (isShiftOver) {
|
||||
debugPrint('[AUTO_SHIFT_END] ⏰ Shift ended (Start: $startTimeStr, End: $endTimeStr). Current: ${_pad(now.hour)}:${_pad(now.minute)}');
|
||||
await _autoEndShift(prefs);
|
||||
} else {
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Shift not ended yet, continuing...');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[AUTO_SHIFT_END] ❌ Error checking shift end: $e');
|
||||
debugPrint('[AUTO_SHIFT_END] Stack trace: ${StackTrace.current}');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _autoEndShift(SharedPreferences prefs) async {
|
||||
try {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...',
|
||||
);
|
||||
|
||||
// 1. Get Required IDs
|
||||
final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
final int partnerid =
|
||||
prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0;
|
||||
final int shiftid =
|
||||
prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0;
|
||||
final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0;
|
||||
|
||||
if (userid == 0) {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Get Location
|
||||
final Map<String, String>? coords = await _getCoordinatesWithFallback();
|
||||
final String lat = coords?['lat'] ?? '0';
|
||||
final String lng = coords?['lng'] ?? '0';
|
||||
|
||||
// 3. Prepare Break Log Payload
|
||||
final now = DateTime.now();
|
||||
final int localBreakId =
|
||||
(DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID
|
||||
final String breakdate =
|
||||
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
final String breakstart =
|
||||
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
|
||||
|
||||
final payload = <String, dynamic>{
|
||||
"breakid": localBreakId,
|
||||
"logid": logid,
|
||||
"breakdate": breakdate,
|
||||
"userid": userid,
|
||||
"partnerid": partnerid,
|
||||
"shiftid": shiftid,
|
||||
"breakstart": breakstart,
|
||||
"breakend": "",
|
||||
"breakhours": 0.0,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
};
|
||||
|
||||
// 4. Call API to Create Break
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createBreakRiderLogLive
|
||||
: ApiConstants.createBreakRiderLogDev;
|
||||
|
||||
debugPrint('[AUTO_SHIFT_END] Creating break log: $url');
|
||||
|
||||
// We use a separate provider instance or http call if needed,
|
||||
// but _logProvider is for delivery logs. We need a generic post or use http directly.
|
||||
// Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation.
|
||||
|
||||
try {
|
||||
final response = await _httpClient
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(payload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully');
|
||||
|
||||
// Parse response to get server break ID if needed, but mainly we just need to go offline
|
||||
final body = jsonDecode(response.body);
|
||||
final det = (body['details'] is Map) ? body['details'] : body;
|
||||
final serverBreakId = det['breakid'];
|
||||
|
||||
if (serverBreakId != null) {
|
||||
await prefs.setInt(
|
||||
'breakId',
|
||||
int.tryParse('$serverBreakId') ?? localBreakId,
|
||||
);
|
||||
}
|
||||
await prefs.setString('breakStart', breakstart);
|
||||
await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[AUTO_SHIFT_END] ❌ API Error: $e');
|
||||
// Even if API fails, we MUST go offline locally to prevent further issues
|
||||
}
|
||||
|
||||
// 5. Set Offline Locally
|
||||
await prefs.setInt('onduty', 0);
|
||||
await prefs.setBool('online', false);
|
||||
|
||||
// 6. Update Rider Log (Set Duty = 0)
|
||||
// We should also update the main rider log to say onduty=0
|
||||
final updateUrl = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.updateRiderLogLive
|
||||
: ApiConstants.updateRiderLogDev;
|
||||
|
||||
final updatePayload = {
|
||||
"userid": userid,
|
||||
"onduty": 0,
|
||||
"latitude": lat,
|
||||
"longitude": lng,
|
||||
};
|
||||
|
||||
try {
|
||||
await _httpClient
|
||||
.post(
|
||||
Uri.parse(updateUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(updatePayload),
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline');
|
||||
} catch (_) {}
|
||||
|
||||
debugPrint('[AUTO_SHIFT_END] 🏁 Auto-shift end sequence complete.');
|
||||
} catch (e) {
|
||||
debugPrint('[AUTO_SHIFT_END] Critical error in _autoEndShift: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger proximity alert once per delivery when within threshold
|
||||
static Future<void> _checkProximityAlert(
|
||||
Map<String, dynamic> order,
|
||||
Map<String, String> coords,
|
||||
) async {
|
||||
try {
|
||||
final deliveryId =
|
||||
(order['deliveryid'] ?? order['orderid'] ?? '').toString();
|
||||
if (deliveryId.isEmpty) return;
|
||||
|
||||
final dropLatRaw =
|
||||
(order['droplat'] ?? order['DropLat'] ?? order['deliverylat'] ?? '')
|
||||
.toString();
|
||||
final dropLngRaw = (order['droplon'] ??
|
||||
order['droplong'] ??
|
||||
order['DropLon'] ??
|
||||
order['DropLong'] ??
|
||||
order['deliverylong'] ??
|
||||
'')
|
||||
.toString();
|
||||
final dropLat = double.tryParse(dropLatRaw) ?? 0.0;
|
||||
final dropLng = double.tryParse(dropLngRaw) ?? 0.0;
|
||||
if (dropLat == 0 ||
|
||||
dropLng == 0 ||
|
||||
dropLat.abs() > 90 ||
|
||||
dropLng.abs() > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
final riderLat = double.tryParse(coords['lat'] ?? '0') ?? 0.0;
|
||||
final riderLng = double.tryParse(coords['lng'] ?? '0') ?? 0.0;
|
||||
if (riderLat == 0 ||
|
||||
riderLng == 0 ||
|
||||
riderLat.abs() > 90 ||
|
||||
riderLng.abs() > 180) {
|
||||
return;
|
||||
}
|
||||
|
||||
final distanceMeters = Geolocator.distanceBetween(
|
||||
dropLat,
|
||||
dropLng,
|
||||
riderLat,
|
||||
riderLng,
|
||||
);
|
||||
|
||||
if (distanceMeters <= _proximityThresholdMeters) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final alreadyAlerted = prefs.getBool(‘$_proximityAlertKeyPrefix$deliveryId’) ?? false;
|
||||
if (alreadyAlerted) {
|
||||
debugPrint(‘[PROXIMITY] Already alerted for deliveryId=$deliveryId, skipping’);
|
||||
return;
|
||||
}
|
||||
|
||||
await NotificationServce.showLocalNotification(
|
||||
title: ‘Near delivery location’,
|
||||
body: ‘You\’ve reached your destination. Please update the status.’,
|
||||
);
|
||||
|
||||
final played = await _playProximityAudio();
|
||||
await prefs.setBool(‘$_proximityAlertKeyPrefix$deliveryId’, true);
|
||||
|
||||
debugPrint(
|
||||
‘[PROXIMITY] Alerted deliveryId=$deliveryId at ${distanceMeters.toStringAsFixed(1)}m ‘
|
||||
‘target=($dropLat,$dropLng) rider=($riderLat,$riderLng) played=$played’,
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
‘[PROXIMITY] Skipped alert for deliveryId=$deliveryId | distance=${distanceMeters.toStringAsFixed(1)}m’,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[PROXIMITY] Error sending alert: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> _playProximityAudio() async {
|
||||
try {
|
||||
if (!_audioReady) {
|
||||
await _proximityPlayer.setReleaseMode(ReleaseMode.stop);
|
||||
await _proximityPlayer.setVolume(1.0);
|
||||
_audioReady = true;
|
||||
}
|
||||
// Attempt to play bundled destination audio
|
||||
await _proximityPlayer.stop();
|
||||
await _proximityPlayer.play(AssetSource('audio/destination.mp3'));
|
||||
debugPrint('[PROXIMITY][AUDIO] Playing destination.mp3');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[PROXIMITY][AUDIO] Error playing destination clip: $e');
|
||||
}
|
||||
|
||||
// Fallback to TTS if audio fails
|
||||
try {
|
||||
// Initialize once
|
||||
if (!_ttsReady) {
|
||||
await _tts.setLanguage('en-US');
|
||||
await _tts.setSpeechRate(0.9);
|
||||
await _tts.setVolume(1.0);
|
||||
await _tts.setPitch(1.0);
|
||||
_ttsReady = true;
|
||||
}
|
||||
// Speak without awaiting completion to avoid blocking
|
||||
await _tts.speak('You have reached your destination. Please update the delivery status.');
|
||||
debugPrint('[PROXIMITY][TTS] Spoke destination prompt');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('[PROXIMITY][TTS] Error speaking prompt: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
461
lib/background/foreground_service.dart
Normal file
461
lib/background/foreground_service.dart
Normal file
@@ -0,0 +1,461 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:nearle/providers/Riderlog/riderlog_provider.dart';
|
||||
import 'package:nearle/background/backgroundservice.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:nearle/utils/kalman_filter.dart';
|
||||
import 'package:nearle/utils/mqtt_service.dart';
|
||||
import 'package:nearle/views/helpers/constants/mqtt_constants.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'dart:io';
|
||||
import 'package:nearle/helpers/http_overrides.dart';
|
||||
|
||||
class _BackgroundRiderLog {
|
||||
static NearleKalmanFilter? _kf;
|
||||
static DateTime? _lastUpdateTime;
|
||||
|
||||
static Future<Map<String, String>> _ensureLatLng() async {
|
||||
Map<String, String> result = {
|
||||
'lat': '0',
|
||||
'lng': '0',
|
||||
'raw_lat': '0',
|
||||
'raw_lng': '0',
|
||||
'speed': '0',
|
||||
'heading': '0',
|
||||
'velocity_lat': '0',
|
||||
'velocity_lng': '0',
|
||||
'status': 'unknown',
|
||||
'accuracy': '0',
|
||||
};
|
||||
try {
|
||||
// 1. Check if location services are enabled
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[BG_RIDER_LOG] Location services are disabled.');
|
||||
result['status'] = 'disabled';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. Check permissions
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
debugPrint('[BG_RIDER_LOG] Location permission denied.');
|
||||
result['status'] = 'denied';
|
||||
return result;
|
||||
}
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
debugPrint('[BG_RIDER_LOG] Location permission denied forever.');
|
||||
result['status'] = 'denied_forever';
|
||||
return result;
|
||||
}
|
||||
|
||||
result['status'] = 'enabled';
|
||||
|
||||
// 3. Get position (using non-deprecated LocationSettings + explicit timeout)
|
||||
final pos = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
),
|
||||
);
|
||||
|
||||
// Reject mocked positions (anti-cheat)
|
||||
if (pos.isMocked) {
|
||||
debugPrint('[BG_RIDER_LOG] Mocked position detected — using cached');
|
||||
return result;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
double outLat = pos.latitude;
|
||||
double outLng = pos.longitude;
|
||||
double speed = pos.speed;
|
||||
double heading = pos.heading;
|
||||
|
||||
// Decompose velocity for Kalman
|
||||
final double headingRadians = heading * (math.pi / 180.0);
|
||||
final double velocityLng = speed * math.sin(headingRadians);
|
||||
final double velocityLat = speed * math.cos(headingRadians);
|
||||
|
||||
if (_kf == null) {
|
||||
_kf = NearleKalmanFilter(lat: outLat, lng: outLng);
|
||||
} else {
|
||||
final double dt = _lastUpdateTime != null
|
||||
? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0
|
||||
: 30.0; // Default background interval
|
||||
_kf!.predict(dt);
|
||||
_kf!.update(outLat, outLng);
|
||||
outLat = _kf!.x[0];
|
||||
outLng = _kf!.x[1];
|
||||
}
|
||||
_lastUpdateTime = now;
|
||||
|
||||
return {
|
||||
'lat': outLat.toStringAsFixed(6),
|
||||
'lng': outLng.toStringAsFixed(6),
|
||||
'raw_lat': pos.latitude.toStringAsFixed(6),
|
||||
'raw_lng': pos.longitude.toStringAsFixed(6),
|
||||
'speed': speed.toStringAsFixed(2),
|
||||
'heading': heading.toStringAsFixed(2),
|
||||
'velocity_lat': velocityLat.toStringAsFixed(4),
|
||||
'velocity_lng': velocityLng.toStringAsFixed(4),
|
||||
'status': 'enabled',
|
||||
'accuracy': pos.accuracy.toStringAsFixed(1),
|
||||
};
|
||||
} catch (e) {
|
||||
debugPrint('[BG_RIDER_LOG] Error getting location: $e');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static String _two(int n) => n.toString().padLeft(2, '0');
|
||||
static String _formatDateTimeFull(DateTime dt) {
|
||||
final y = dt.year.toString();
|
||||
final m = _two(dt.month);
|
||||
final d = _two(dt.day);
|
||||
final hh = _two(dt.hour);
|
||||
final mm = _two(dt.minute);
|
||||
final ss = _two(dt.second);
|
||||
return "$y-$m-$d $hh:$mm:$ss";
|
||||
}
|
||||
static String _formatTime(DateTime dt) {
|
||||
final hh = _two(dt.hour);
|
||||
final mm = _two(dt.minute);
|
||||
final ss = _two(dt.second);
|
||||
return "$hh:$mm:$ss";
|
||||
}
|
||||
|
||||
/// Accumulates cumulative KMs for active deliveries using the foreground service GPS position.
|
||||
/// Only runs when LiveTrackingService (main isolate) hasn't updated in the last 10 seconds,
|
||||
/// which means the app is backgrounded/screen-off/power-saver and the main isolate is dormant.
|
||||
static Future<void> _accumulateBackgroundKms(
|
||||
SharedPreferences prefs,
|
||||
Map<String, String> loc,
|
||||
) async {
|
||||
try {
|
||||
// Check if the main isolate's LiveTrackingService is still actively updating
|
||||
final lastLiveUpdateMs = prefs.getInt('live_tracking_last_update_ms') ?? 0;
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
final secondsSinceLiveUpdate = (nowMs - lastLiveUpdateMs) / 1000.0;
|
||||
|
||||
if (secondsSinceLiveUpdate < 10.0) {
|
||||
// Main isolate is active — let it handle KMs to avoid race conditions
|
||||
debugPrint(
|
||||
'[BG_KM] LiveTrackingService active (${secondsSinceLiveUpdate.toStringAsFixed(1)}s ago) — skipping background accumulation',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if GPS accuracy is too poor for reliable KM tracking
|
||||
final double accuracy = double.tryParse(loc['accuracy'] ?? '9999') ?? 9999.0;
|
||||
if (accuracy > 50.0) {
|
||||
debugPrint(
|
||||
'[BG_KM] Low-accuracy position (${accuracy.toStringAsFixed(0)}m) — skipping KM accumulation',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final double currentLat = double.tryParse(loc['lat'] ?? '0') ?? 0.0;
|
||||
final double currentLng = double.tryParse(loc['lng'] ?? '0') ?? 0.0;
|
||||
if (currentLat == 0.0 || currentLng == 0.0) return;
|
||||
|
||||
final activeDeliveryIds =
|
||||
prefs.getStringList('active_tracking_delivery_ids') ?? [];
|
||||
|
||||
for (final dId in activeDeliveryIds) {
|
||||
try {
|
||||
final lastLatStr = prefs.getString('delivery_tracking_${dId}_lastLat') ?? '';
|
||||
final lastLngStr = prefs.getString('delivery_tracking_${dId}_lastLng') ?? '';
|
||||
final currentCumKm = double.tryParse(
|
||||
prefs.getString('delivery_tracking_${dId}_cumulativeKm') ?? '0',
|
||||
) ??
|
||||
0.0;
|
||||
|
||||
if (lastLatStr.isNotEmpty && lastLngStr.isNotEmpty) {
|
||||
final lastLat = double.tryParse(lastLatStr) ?? 0.0;
|
||||
final lastLng = double.tryParse(lastLngStr) ?? 0.0;
|
||||
|
||||
if (lastLat != 0.0 && lastLng != 0.0) {
|
||||
final distanceMeters = Geolocator.distanceBetween(
|
||||
lastLat,
|
||||
lastLng,
|
||||
currentLat,
|
||||
currentLng,
|
||||
);
|
||||
|
||||
// Speed-based jump guard: reject if implied speed > 120 km/h (33.3 m/s).
|
||||
// Uses elapsed time since last recorded position so the threshold scales
|
||||
// correctly whether the background interval is 30s, 60s, or longer.
|
||||
final lastUpdateMs =
|
||||
prefs.getInt('delivery_tracking_${dId}_lastUpdateMs') ?? 0;
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
final elapsedSeconds = lastUpdateMs > 0
|
||||
? (nowMs - lastUpdateMs) / 1000.0
|
||||
: 60.0; // conservative default
|
||||
final maxRealisticMeters = elapsedSeconds * 33.3; // 120 km/h ceiling
|
||||
|
||||
if (distanceMeters > maxRealisticMeters && distanceMeters > 50.0) {
|
||||
// GPS jumped — update anchor without counting phantom distance
|
||||
debugPrint(
|
||||
'[BG_KM] GPS jump for $dId: ${distanceMeters.toStringAsFixed(0)}m '
|
||||
'in ${elapsedSeconds.toStringAsFixed(1)}s (max: ${maxRealisticMeters.toStringAsFixed(0)}m) — resetting anchor',
|
||||
);
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLat', currentLat.toString());
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLng', currentLng.toString());
|
||||
await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', nowMs);
|
||||
} else if (distanceMeters >= 5.0) {
|
||||
final newCumKm = currentCumKm + (distanceMeters / 1000.0);
|
||||
await prefs.setString(
|
||||
'delivery_tracking_${dId}_cumulativeKm',
|
||||
newCumKm.toStringAsFixed(4),
|
||||
);
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLat', currentLat.toString());
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLng', currentLng.toString());
|
||||
await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', nowMs);
|
||||
debugPrint(
|
||||
'[BG_KM] +${(distanceMeters / 1000.0).toStringAsFixed(4)} km for $dId '
|
||||
'in ${elapsedSeconds.toStringAsFixed(1)}s (total: ${newCumKm.toStringAsFixed(4)} km)',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No anchor yet — set initial position
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLat', currentLat.toString());
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLng', currentLng.toString());
|
||||
await prefs.setInt(
|
||||
'delivery_tracking_${dId}_lastUpdateMs',
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[BG_KM] Error in background KM accumulation: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> createLoginNow() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
// Reload from disk so we see the latest values written by the main isolate
|
||||
await prefs.reload();
|
||||
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
if (onduty != 1) {
|
||||
return;
|
||||
}
|
||||
final int? userid = prefs.getInt('userId') ?? prefs.getInt('userid');
|
||||
final int? partnerid = prefs.getInt('partnerId') ?? prefs.getInt('partnerid');
|
||||
final int? shiftid = prefs.getInt('shiftId') ?? prefs.getInt('shiftid');
|
||||
if ((userid ?? 0) == 0) return;
|
||||
|
||||
// Prefer explicit username, then fallback to stored full name or first/last
|
||||
String? username = prefs.getString('username');
|
||||
username ??= prefs.getString('user_name');
|
||||
if (username == null || username.trim().isEmpty) {
|
||||
final first = prefs.getString('firstname') ?? '';
|
||||
final last = prefs.getString('lastname') ?? '';
|
||||
final combined = ('$first $last').trim();
|
||||
if (combined.isNotEmpty) {
|
||||
username = combined;
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Check if there are active deliveries to set status
|
||||
final bool hasActiveDeliveries = prefs.getBool('has_live_deliveries') ?? false;
|
||||
final String riderStatus = hasActiveDeliveries ? 'active' : 'idle';
|
||||
|
||||
final now = DateTime.now();
|
||||
final iso = _formatDateTimeFull(now);
|
||||
final loginTime = _formatTime(now);
|
||||
final loc = await _ensureLatLng();
|
||||
|
||||
// Accumulate KMs in background when LiveTrackingService (main isolate) is not active
|
||||
await _accumulateBackgroundKms(prefs, loc);
|
||||
|
||||
|
||||
final int? tenantid = prefs.getInt('tenantid');
|
||||
final int? locationid = prefs.getInt('locationid');
|
||||
final int? applocationid = prefs.getInt('applocationid');
|
||||
final String? userfcmtoken = prefs.getString('userfcmtoken');
|
||||
|
||||
final int? logid = prefs.getInt('logId') ?? prefs.getInt('logid');
|
||||
final String orderId = prefs.getString('current_riding_order_id') ?? '';
|
||||
|
||||
final payload = {
|
||||
"logid": logid ?? 0,
|
||||
"userid": userid,
|
||||
"partnerid": partnerid,
|
||||
"shiftid": shiftid,
|
||||
"logdate": iso,
|
||||
"login": loginTime,
|
||||
"latitude": loc['lat'] ?? '0',
|
||||
"longitude": loc['lng'] ?? '0',
|
||||
"raw_latitude": loc['raw_lat'] ?? '0',
|
||||
"raw_longitude": loc['raw_lng'] ?? '0',
|
||||
"velocity_lat": loc['velocity_lat'] ?? '0',
|
||||
"velocity_lng": loc['velocity_lng'] ?? '0',
|
||||
"speed": loc['speed'] ?? '0',
|
||||
"heading": loc['heading'] ?? '0',
|
||||
"onduty": 1,
|
||||
"status": riderStatus,
|
||||
"contactno": prefs.getString('contactno') ?? '',
|
||||
"tenantid": tenantid ?? 0,
|
||||
"locationid": locationid ?? 0,
|
||||
"applocationid": applocationid ?? 0,
|
||||
"userfcmtoken": userfcmtoken ?? '',
|
||||
"username": (username ?? '').trim(),
|
||||
"orderid": orderId,
|
||||
};
|
||||
|
||||
final firstName = prefs.getString('firstname') ?? '';
|
||||
final lastName = prefs.getString('lastname') ?? '';
|
||||
if (firstName.trim().isNotEmpty) {
|
||||
payload['firstname'] = firstName.trim();
|
||||
}
|
||||
if (lastName.trim().isNotEmpty) {
|
||||
payload['lastname'] = lastName.trim();
|
||||
}
|
||||
|
||||
final base = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createRiderLogLive
|
||||
: ApiConstants.createRiderLogDev;
|
||||
|
||||
final provider = CreateRiderLogProvider();
|
||||
final resp = await provider.createRiderLog(base, payload);
|
||||
|
||||
if (resp == null || resp.isEmpty) return;
|
||||
final det = (resp['details'] is Map<String, dynamic>)
|
||||
? (resp['details'] as Map<String, dynamic>)
|
||||
: resp;
|
||||
final newLogId = int.tryParse('${det['logid'] ?? 0}') ?? (det['logid'] as int? ?? 0);
|
||||
await prefs.setInt('logid', newLogId);
|
||||
await prefs.setInt('logId', newLogId);
|
||||
|
||||
// ✅ MQTT BACKGROUND PUBLISH ( Lane Split )
|
||||
final mqttService = NearleMqttService();
|
||||
if (!mqttService.isConnected) {
|
||||
// Use a slightly different client ID for background to avoid kicking the main one off
|
||||
await mqttService.connect();
|
||||
}
|
||||
|
||||
if (mqttService.isConnected) {
|
||||
// Gather Telemetry
|
||||
final battery = Battery();
|
||||
final int batteryLevel = await battery.batteryLevel;
|
||||
final BatteryState batteryState = await battery.batteryState;
|
||||
final isCharging = batteryState == BatteryState.charging || batteryState == BatteryState.full;
|
||||
|
||||
final connectivity = await Connectivity().checkConnectivity();
|
||||
final String connType = connectivity.isNotEmpty ? connectivity.first.toString().split('.').last : 'none';
|
||||
|
||||
// 1. Direct Telemetry (Feeding the /full API)
|
||||
mqttService.publish('battery', '$batteryLevel%');
|
||||
mqttService.publish('charging', isCharging ? 'yes' : 'no');
|
||||
mqttService.publish('speed', loc['speed'] ?? '0');
|
||||
mqttService.publish('connection', connType);
|
||||
mqttService.publish('accuracy', loc['accuracy'] ?? '0');
|
||||
|
||||
// 2. Alert if Location is Off
|
||||
final String locStatus = loc['status'] ?? 'unknown';
|
||||
if (locStatus != 'enabled') {
|
||||
mqttService.publish('alerts', {
|
||||
'userid': userid,
|
||||
'username': (username ?? '').trim(),
|
||||
'event': 'location_turned_off',
|
||||
'error_type': locStatus,
|
||||
'battery': '$batteryLevel%',
|
||||
'is_charging': isCharging,
|
||||
'connection': connType,
|
||||
'logdate': iso,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Low Battery Alert
|
||||
if (batteryLevel < 15 && !isCharging) {
|
||||
mqttService.publish('alerts', {
|
||||
'userid': userid,
|
||||
'username': (username ?? '').trim(),
|
||||
'event': 'low_battery_warning',
|
||||
'battery': '$batteryLevel%',
|
||||
'logdate': iso,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Poor GPS Accuracy Alert
|
||||
final double accuracy = double.tryParse(loc['accuracy'] ?? '0') ?? 0;
|
||||
if (accuracy > 30) {
|
||||
mqttService.publish('alerts', {
|
||||
'userid': userid,
|
||||
'username': (username ?? '').trim(),
|
||||
'event': 'poor_gps_signal',
|
||||
'accuracy': '${accuracy.toStringAsFixed(1)}m',
|
||||
'logdate': iso,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Lane: Status
|
||||
mqttService.updateStatus(riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline);
|
||||
|
||||
// 6. Lane: Periodic Log (Comprehensive Snapshot)
|
||||
mqttService.publishLog('rider_periodic_log', {
|
||||
'userid': userid,
|
||||
'username': username,
|
||||
'logdate': iso,
|
||||
'latitude': loc['lat'] ?? '0',
|
||||
'longitude': loc['lng'] ?? '0',
|
||||
'speed': loc['speed'] ?? '0',
|
||||
'heading': loc['heading'] ?? '0',
|
||||
'accuracy': loc['accuracy'] ?? '0',
|
||||
'status': riderStatus,
|
||||
'orderid': orderId,
|
||||
'battery': '$batteryLevel%',
|
||||
'is_charging': isCharging,
|
||||
'connection': connType,
|
||||
'location_service': locStatus,
|
||||
'is_background': true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore background errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RiderLogTaskHandler extends TaskHandler {
|
||||
Timer? _timer; // not used; plugin provides repeat callback, but keep safety
|
||||
|
||||
@override
|
||||
Future<void> onStart(DateTime timestamp, SendPort? sendPort) async {
|
||||
// No-op
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
|
||||
// 1. Rider Log (existing)
|
||||
await _BackgroundRiderLog.createLoginNow();
|
||||
|
||||
// 2. Delivery Log (new)
|
||||
await BackgroundDeliveryLog.processActiveDeliveries();
|
||||
|
||||
// 3. Auto Shift End (new)
|
||||
await BackgroundDeliveryLog.checkShiftEnd();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onDestroy(DateTime timestamp, SendPort? sendPort) async {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void riderLogCallback() {
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
FlutterForegroundTask.setTaskHandler(RiderLogTaskHandler());
|
||||
}
|
||||
|
||||
292
lib/background/live_tracking_service.dart
Normal file
292
lib/background/live_tracking_service.dart
Normal file
@@ -0,0 +1,292 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/utils/kalman_filter.dart';
|
||||
import 'package:nearle/utils/mqtt_service.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
|
||||
class LiveTrackingService {
|
||||
static final LiveTrackingService _instance = LiveTrackingService._internal();
|
||||
|
||||
factory LiveTrackingService() => _instance;
|
||||
|
||||
LiveTrackingService._internal();
|
||||
|
||||
StreamSubscription<Position>? _positionStreamSubscription;
|
||||
bool _isTracking = false;
|
||||
bool _isProcessing = false;
|
||||
NearleKalmanFilter? _kf;
|
||||
DateTime? _lastUpdateTime;
|
||||
DateTime? _lastSentTime;
|
||||
DateTime? _lastTelemetrySent;
|
||||
Timer? _watchdogTimer;
|
||||
|
||||
// Thresholds
|
||||
static const double _maxAccuracyMeters = 50.0; // Reject GPS > 50m accuracy
|
||||
static const double _minDistanceMeters = 5.0; // Minimum movement to count
|
||||
static const double _maxJumpMeters = 200.0; // Reject single-step jumps > 200m
|
||||
static const int _rateLimitSeconds = 3; // Minimum seconds between updates
|
||||
static const int _watchdogSeconds = 45; // Restart if no update in 45s
|
||||
|
||||
void startTracking() async {
|
||||
if (_isTracking) return;
|
||||
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[LIVE TRACKING] Location services disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
final permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
debugPrint('[LIVE TRACKING] Location permission denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
_isTracking = true;
|
||||
_lastSentTime = null;
|
||||
debugPrint('[LIVE TRACKING] Starting high-frequency tracking...');
|
||||
|
||||
_startStream();
|
||||
_startWatchdog();
|
||||
}
|
||||
|
||||
void _startStream() {
|
||||
_positionStreamSubscription?.cancel();
|
||||
|
||||
const locationSettings = LocationSettings(
|
||||
accuracy: LocationAccuracy.bestForNavigation,
|
||||
distanceFilter: 0,
|
||||
);
|
||||
|
||||
_positionStreamSubscription = Geolocator.getPositionStream(
|
||||
locationSettings: locationSettings,
|
||||
).listen(
|
||||
(Position position) async {
|
||||
await _sendToKalmanBackend(position);
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('[LIVE TRACKING] Stream error: $error — restarting in 5s');
|
||||
_positionStreamSubscription?.cancel();
|
||||
_positionStreamSubscription = null;
|
||||
if (_isTracking) {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
if (_isTracking) _startStream();
|
||||
});
|
||||
}
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _startWatchdog() {
|
||||
_watchdogTimer?.cancel();
|
||||
_watchdogTimer = Timer.periodic(
|
||||
const Duration(seconds: _watchdogSeconds),
|
||||
(_) {
|
||||
if (!_isTracking) return;
|
||||
final lastSent = _lastSentTime;
|
||||
if (lastSent == null) return;
|
||||
final staleSeconds = DateTime.now().difference(lastSent).inSeconds;
|
||||
if (staleSeconds > _watchdogSeconds) {
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] Watchdog: stream stale for ${staleSeconds}s — restarting',
|
||||
);
|
||||
_positionStreamSubscription?.cancel();
|
||||
_positionStreamSubscription = null;
|
||||
_kf = null;
|
||||
_lastUpdateTime = null;
|
||||
_startStream();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void stopTracking() {
|
||||
if (!_isTracking) return;
|
||||
debugPrint('[LIVE TRACKING] Stopping tracking.');
|
||||
_watchdogTimer?.cancel();
|
||||
_watchdogTimer = null;
|
||||
_positionStreamSubscription?.cancel();
|
||||
_positionStreamSubscription = null;
|
||||
_kf = null;
|
||||
_lastUpdateTime = null;
|
||||
_isTracking = false;
|
||||
}
|
||||
|
||||
Future<void> _sendToKalmanBackend(Position position) async {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Rate limiter
|
||||
if (_lastSentTime != null &&
|
||||
now.difference(_lastSentTime!).inSeconds < _rateLimitSeconds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutex — prevent concurrent processing
|
||||
if (_isProcessing) return;
|
||||
_isProcessing = true;
|
||||
_lastSentTime = now;
|
||||
|
||||
try {
|
||||
// Reject mocked GPS (anti-cheat)
|
||||
if (position.isMocked) {
|
||||
debugPrint('[LIVE TRACKING] Skipped mocked position');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reject poor-accuracy GPS (power saver / other apps degrading GPS)
|
||||
if (position.accuracy > _maxAccuracyMeters) {
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] Skipped low-accuracy position: ${position.accuracy.toStringAsFixed(0)}m',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userid = prefs.getInt('userId') ?? prefs.getInt('userid');
|
||||
final currentOrderId = prefs.getString('current_riding_order_id') ?? '';
|
||||
|
||||
final double headingRadians = position.heading * (math.pi / 180.0);
|
||||
final double velocityLng = position.speed * math.sin(headingRadians);
|
||||
final double velocityLat = position.speed * math.cos(headingRadians);
|
||||
|
||||
double displayLat = position.latitude;
|
||||
double displayLng = position.longitude;
|
||||
|
||||
if (_kf == null) {
|
||||
_kf = NearleKalmanFilter(
|
||||
lat: position.latitude,
|
||||
lng: position.longitude,
|
||||
);
|
||||
} else {
|
||||
final double dt = _lastUpdateTime != null
|
||||
? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0
|
||||
: _rateLimitSeconds.toDouble();
|
||||
_kf!.predict(dt);
|
||||
_kf!.update(position.latitude, position.longitude);
|
||||
displayLat = _kf!.x[0];
|
||||
displayLng = _kf!.x[1];
|
||||
}
|
||||
_lastUpdateTime = now;
|
||||
|
||||
// Stamp that the main isolate is actively tracking.
|
||||
// The foreground service reads this to avoid double-counting KMs.
|
||||
await prefs.setInt('live_tracking_last_update_ms', now.millisecondsSinceEpoch);
|
||||
|
||||
final payload = {
|
||||
'userid': userid,
|
||||
'orderid': currentOrderId,
|
||||
'lat': displayLat,
|
||||
'lng': displayLng,
|
||||
'raw_lat': position.latitude,
|
||||
'raw_lng': position.longitude,
|
||||
'speed': position.speed,
|
||||
'heading': position.heading,
|
||||
'velocity_lat': velocityLat,
|
||||
'velocity_lng': velocityLng,
|
||||
'timestamp': now.toIso8601String(),
|
||||
};
|
||||
|
||||
// Accumulate cumulative KMs for active deliveries
|
||||
final activeDeliveryIds =
|
||||
prefs.getStringList('active_tracking_delivery_ids') ?? [];
|
||||
for (final dId in activeDeliveryIds) {
|
||||
try {
|
||||
final lastLatStr =
|
||||
prefs.getString('delivery_tracking_${dId}_lastLat') ?? '';
|
||||
final lastLngStr =
|
||||
prefs.getString('delivery_tracking_${dId}_lastLng') ?? '';
|
||||
final currentCumKm = double.tryParse(
|
||||
prefs.getString('delivery_tracking_${dId}_cumulativeKm') ?? '0',
|
||||
) ??
|
||||
0.0;
|
||||
|
||||
if (lastLatStr.isNotEmpty && lastLngStr.isNotEmpty) {
|
||||
final lastLat = double.tryParse(lastLatStr) ?? 0.0;
|
||||
final lastLng = double.tryParse(lastLngStr) ?? 0.0;
|
||||
|
||||
if (lastLat != 0 && lastLng != 0 && displayLat != 0 && displayLng != 0) {
|
||||
final distanceMeters = Geolocator.distanceBetween(
|
||||
lastLat,
|
||||
lastLng,
|
||||
displayLat,
|
||||
displayLng,
|
||||
);
|
||||
|
||||
// Speed-based jump guard: reject if implied speed > 120 km/h (33.3 m/s)
|
||||
final lastUpdateMs = prefs.getInt('delivery_tracking_${dId}_lastUpdateMs') ?? 0;
|
||||
final elapsedSeconds = lastUpdateMs > 0
|
||||
? (now.millisecondsSinceEpoch - lastUpdateMs) / 1000.0
|
||||
: _rateLimitSeconds.toDouble();
|
||||
final maxRealisticMeters = elapsedSeconds * 33.3; // 120 km/h ceiling
|
||||
|
||||
if (distanceMeters > maxRealisticMeters && distanceMeters > _maxJumpMeters) {
|
||||
// GPS jumped — update anchor without counting the phantom distance
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] GPS jump for $dId: ${distanceMeters.toStringAsFixed(0)}m '
|
||||
'in ${elapsedSeconds.toStringAsFixed(1)}s (max realistic: ${maxRealisticMeters.toStringAsFixed(0)}m) — resetting anchor',
|
||||
);
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLat', displayLat.toString());
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLng', displayLng.toString());
|
||||
await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', now.millisecondsSinceEpoch);
|
||||
} else if (distanceMeters >= _minDistanceMeters) {
|
||||
final newCumKm = currentCumKm + (distanceMeters / 1000.0);
|
||||
await prefs.setString(
|
||||
'delivery_tracking_${dId}_cumulativeKm',
|
||||
newCumKm.toStringAsFixed(4),
|
||||
);
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLat', displayLat.toString());
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLng', displayLng.toString());
|
||||
await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', now.millisecondsSinceEpoch);
|
||||
}
|
||||
// else: too small — skip without moving anchor (avoids GPS noise accumulation)
|
||||
}
|
||||
} else {
|
||||
// First point for this delivery — set anchor only
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLat', displayLat.toString());
|
||||
await prefs.setString('delivery_tracking_${dId}_lastLng', displayLng.toString());
|
||||
await prefs.setInt('delivery_tracking_${dId}_lastUpdateMs', now.millisecondsSinceEpoch);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
final mqttService = NearleMqttService();
|
||||
if (mqttService.isConnected) {
|
||||
mqttService.publishLocation(payload);
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] ${displayLat.toStringAsFixed(6)}, ${displayLng.toStringAsFixed(6)} '
|
||||
'(acc: ${position.accuracy.toStringAsFixed(0)}m, speed: ${position.speed.toStringAsFixed(1)} m/s)',
|
||||
);
|
||||
} else {
|
||||
debugPrint('[LIVE TRACKING] MQTT not connected — attempting reconnect');
|
||||
mqttService.connect();
|
||||
}
|
||||
|
||||
// Telemetry once every 5 minutes (timestamp guard prevents duplicate fires
|
||||
// across the 3-second GPS update window at the same minute mark)
|
||||
final shouldSendTelemetry = _lastTelemetrySent == null ||
|
||||
now.difference(_lastTelemetrySent!).inMinutes >= 5;
|
||||
if (now.minute % 5 == 0 && shouldSendTelemetry) {
|
||||
_lastTelemetrySent = now;
|
||||
try {
|
||||
final battery = Battery();
|
||||
final batteryLevel = await battery.batteryLevel;
|
||||
mqttService.publishTelemetry({
|
||||
'battery_level': batteryLevel,
|
||||
'gps_accuracy': position.accuracy,
|
||||
'mocked': position.isMocked,
|
||||
'timestamp': now.toIso8601String(),
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[LIVE TRACKING] Error: $e');
|
||||
} finally {
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
712
lib/controllers/auth.dart
Normal file
712
lib/controllers/auth.dart
Normal file
@@ -0,0 +1,712 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/providers/auth/auth_provider.dart';
|
||||
import 'package:nearle/utils/device.dart';
|
||||
import 'package:sms_autofill/sms_autofill.dart';
|
||||
import 'package:nearle/controllers/profile_controller.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:nearle/controllers/riderlog.dart';
|
||||
import 'package:nearle/Models/login/login.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
enum AuthNext { verifyPin, otp, notRegistered, error }
|
||||
|
||||
class AuthController extends GetxController {
|
||||
final RxBool sendingOtp = false.obs;
|
||||
String? currentPhone;
|
||||
final AuthProvider _api = AuthProvider();
|
||||
AuthNext? lastDecision;
|
||||
// Optional callback used by MPIN screen to clear and refocus fields when user taps "Retry"
|
||||
VoidCallback? onPinRetry;
|
||||
static const String _prefsUserIdKey = 'userid';
|
||||
static const String _prefsPendingPinUserIdKey = 'pending_pin_userid';
|
||||
static const String _prefsUserNameKey = 'user_name';
|
||||
static const String _prefsUserEmailKey = 'user_email';
|
||||
static const String _prefsContactNoKey = 'contactno';
|
||||
static const String _prefsAddressKey = 'user_address';
|
||||
static const String _prefsForceMasterPinKey = 'force_master_pin';
|
||||
static const String _masterPinValue = '1234';
|
||||
static const String forceMasterPinPrefKey = _prefsForceMasterPinKey;
|
||||
static const String masterPinValue = _masterPinValue;
|
||||
bool _forceMasterPinFlow = false;
|
||||
void _log(String msg) => debugPrint('[AUTH] $msg');
|
||||
Future<void> _notifyProfileController() async {
|
||||
try {
|
||||
if (Get.isRegistered<ProfileController>()) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final pc = Get.find<ProfileController>();
|
||||
await pc.loadFromPrefs();
|
||||
pc.setProfile(
|
||||
name: prefs.getString(_prefsUserNameKey),
|
||||
email: prefs.getString(_prefsUserEmailKey),
|
||||
contact: prefs.getString(_prefsContactNoKey),
|
||||
address: prefs.getString(_prefsAddressKey),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
String _normalizePhone(String input) {
|
||||
final digitsOnly = input.replaceAll(RegExp(r'\D'), '');
|
||||
if (digitsOnly.length >= 10) {
|
||||
return digitsOnly.substring(digitsOnly.length - 10);
|
||||
}
|
||||
return digitsOnly;
|
||||
}
|
||||
|
||||
void _showBottomSheet({required String title, required String message}) {
|
||||
Get.bottomSheet(
|
||||
SafeArea(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.info_outline,
|
||||
color: Color(0xFF662582),
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
// If MPIN screen has registered a retry callback, run it
|
||||
onPinRetry?.call();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF662582),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Retry',
|
||||
style: TextStyle(color: Colors.white, fontSize: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthNext> precheckPhone(String phone) async {
|
||||
try {
|
||||
final normalized = _normalizePhone(phone);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_forceMasterPinFlow = false;
|
||||
await prefs.remove(_prefsForceMasterPinKey);
|
||||
String deviceId;
|
||||
try {
|
||||
deviceId = await DeviceUtils.ensureDeviceId(prefs);
|
||||
} catch (e) {
|
||||
_showBottomSheet(
|
||||
title: 'Device Error',
|
||||
message:
|
||||
'Failed to get device ID. Please restart the app and try again.',
|
||||
);
|
||||
lastDecision = AuthNext.error;
|
||||
return lastDecision!;
|
||||
}
|
||||
final String fcmToken = await DeviceUtils.ensureFcmToken(prefs);
|
||||
final bool fcmWasEmpty = fcmToken.isEmpty;
|
||||
_log(
|
||||
'POST /users/rider/login body=${jsonEncode({'contactno': normalized, 'devicetype': Platform.operatingSystem, 'configid': 6, 'deviceid': deviceId, 'userfcmtoken': fcmToken})}',
|
||||
);
|
||||
final Login loginRes = await _api.loginParsed(
|
||||
contactNo: normalized,
|
||||
deviceType: Platform.operatingSystem,
|
||||
configId: 6,
|
||||
deviceId: deviceId,
|
||||
fcmToken: fcmToken,
|
||||
);
|
||||
|
||||
debugPrint('RAW LOGIN RESPONSE: $loginRes');
|
||||
final String serverMessage = (loginRes.message ?? '').toLowerCase();
|
||||
final bool masterPinFlow = (loginRes.authmode ?? 0) == 1;
|
||||
final bool requiresPinSetup =
|
||||
serverMessage.contains('pin not set') ||
|
||||
serverMessage.contains('mpin not set') ||
|
||||
serverMessage.contains('please set your pin') ||
|
||||
(loginRes.code != null && loginRes.code == 201);
|
||||
if (serverMessage.contains('not registered') ||
|
||||
serverMessage.contains('register first') ||
|
||||
serverMessage.contains('not found') ||
|
||||
serverMessage.contains('no user')) {
|
||||
currentPhone = null;
|
||||
_showBottomSheet(
|
||||
title: 'Not Registered',
|
||||
message: 'Please contact admin and register first.',
|
||||
);
|
||||
lastDecision = AuthNext.notRegistered;
|
||||
return lastDecision!;
|
||||
}
|
||||
if (serverMessage.contains('inactive')) {
|
||||
currentPhone = null;
|
||||
_showBottomSheet(
|
||||
title: 'Rider Inactive',
|
||||
message: 'Rider is inactive. Please contact admin.',
|
||||
);
|
||||
lastDecision = AuthNext.notRegistered;
|
||||
return lastDecision!;
|
||||
}
|
||||
final bool messageSaysEnterPin = (loginRes.message ?? '')
|
||||
.toLowerCase()
|
||||
.contains('enter your pin');
|
||||
|
||||
if (loginRes.status == true) {
|
||||
try {
|
||||
final SharedPreferences prefsSave =
|
||||
await SharedPreferences.getInstance();
|
||||
// Persist from model
|
||||
if (loginRes.userid != null) {
|
||||
await prefsSave.setInt(_prefsUserIdKey, loginRes.userid!);
|
||||
|
||||
final SharedPreferences prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'username',
|
||||
loginRes.fullname ?? loginRes.firstname.toString(),
|
||||
);
|
||||
await prefs.setInt('userid', loginRes.userid ?? 0);
|
||||
await prefs.setInt('userId', loginRes.userid ?? 0);
|
||||
await prefs.setInt('shiftid', loginRes.shiftid ?? 0);
|
||||
await prefs.setInt('shiftId', loginRes.shiftid ?? 0);
|
||||
await prefs.setInt('logid', loginRes.logid ?? 0);
|
||||
await prefs.setInt('logId', loginRes.logid ?? 0);
|
||||
await prefs.setInt('riderid', loginRes.riderid ?? 0);
|
||||
await prefs.setInt('partnerid', loginRes.partnerid ?? 0);
|
||||
await prefs.setInt('partnerId', loginRes.partnerid ?? 0);
|
||||
await prefs.setInt('rconfigid', loginRes.configid ?? 0);
|
||||
await prefs.setInt('locationid', loginRes.locationid ?? 0);
|
||||
await prefs.setInt('tenantid', loginRes.tenantid ?? 0);
|
||||
await prefs.setInt('applocationid', loginRes.applocationid ?? 0);
|
||||
if (loginRes.userfcmtoken != null && loginRes.userfcmtoken!.isNotEmpty) {
|
||||
await prefs.setString('userfcmtoken', loginRes.userfcmtoken!);
|
||||
}
|
||||
|
||||
debugPrint('saved on shared pref :${loginRes.fullname}');
|
||||
}
|
||||
|
||||
final String? name = loginRes.fullname ?? loginRes.firstname;
|
||||
final String? email = loginRes.email;
|
||||
final String contact = (loginRes.contactno ?? normalized).toString();
|
||||
final String? address = loginRes.address;
|
||||
_log(
|
||||
'Saving from model: name=$name, email=$email, contact=$contact, address=$address',
|
||||
);
|
||||
if (name != null && name.trim().isNotEmpty) {
|
||||
await prefsSave.setString(_prefsUserNameKey, name.trim());
|
||||
}
|
||||
if (email != null && email.trim().isNotEmpty) {
|
||||
await prefsSave.setString(_prefsUserEmailKey, email.trim());
|
||||
}
|
||||
if (contact.isNotEmpty) {
|
||||
await prefsSave.setString(
|
||||
_prefsContactNoKey,
|
||||
_normalizePhone(contact),
|
||||
);
|
||||
}
|
||||
if (address != null && address.trim().isNotEmpty) {
|
||||
await prefsSave.setString(_prefsAddressKey, address.trim());
|
||||
}
|
||||
await _notifyProfileController();
|
||||
} catch (_) {}
|
||||
}
|
||||
// Persist basic user profile details from model even if above branch didn't run
|
||||
try {
|
||||
final prefs2 = await SharedPreferences.getInstance();
|
||||
final String? name = loginRes.fullname ?? loginRes.firstname;
|
||||
final String? email = loginRes.email;
|
||||
final String contact = (loginRes.contactno ?? normalized).toString();
|
||||
final String? address = loginRes.address;
|
||||
if (name != null && name.trim().isNotEmpty) {
|
||||
await prefs2.setString(_prefsUserNameKey, name.trim());
|
||||
}
|
||||
if (email != null && email.trim().isNotEmpty) {
|
||||
await prefs2.setString(_prefsUserEmailKey, email.trim());
|
||||
}
|
||||
if (contact.isNotEmpty) {
|
||||
final normalizedContact = _normalizePhone(contact);
|
||||
await prefs2.setString(_prefsContactNoKey, normalizedContact);
|
||||
}
|
||||
if (address != null && address.trim().isNotEmpty) {
|
||||
await prefs2.setString(_prefsAddressKey, address.trim());
|
||||
}
|
||||
await _notifyProfileController();
|
||||
} catch (_) {}
|
||||
|
||||
if (masterPinFlow) {
|
||||
currentPhone = _normalizePhone(phone);
|
||||
_forceMasterPinFlow = true;
|
||||
await prefs.setBool(_prefsForceMasterPinKey, true);
|
||||
await prefs.remove('dbPin');
|
||||
if (loginRes.userid != null) {
|
||||
await prefs.setInt(_prefsUserIdKey, loginRes.userid!);
|
||||
}
|
||||
Get.snackbar(
|
||||
'Temporary PIN',
|
||||
'Use $_masterPinValue as PIN to continue.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 4),
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
lastDecision = AuthNext.verifyPin;
|
||||
return lastDecision!;
|
||||
}
|
||||
|
||||
if (messageSaysEnterPin) {
|
||||
currentPhone = _normalizePhone(phone);
|
||||
lastDecision = AuthNext.verifyPin;
|
||||
return lastDecision!;
|
||||
}
|
||||
|
||||
if (requiresPinSetup && !masterPinFlow) {
|
||||
currentPhone = _normalizePhone(phone);
|
||||
await prefs.remove('dbPin');
|
||||
if (loginRes.userid != null) {
|
||||
await prefs.setInt(_prefsPendingPinUserIdKey, loginRes.userid!);
|
||||
}
|
||||
lastDecision = AuthNext.otp;
|
||||
return lastDecision!;
|
||||
}
|
||||
|
||||
// Immediately create rider log entry after successful login
|
||||
currentPhone = normalized;
|
||||
String? dbPin = loginRes.pin?.toString();
|
||||
if (dbPin != null && dbPin.isNotEmpty) {
|
||||
await prefs.setString('dbPin', dbPin);
|
||||
lastDecision = AuthNext.verifyPin;
|
||||
return lastDecision!;
|
||||
}
|
||||
await prefs.remove('dbPin');
|
||||
lastDecision = AuthNext.verifyPin;
|
||||
// If FCM was empty during the request, try to refresh session once token becomes available
|
||||
if (fcmWasEmpty) {
|
||||
try {
|
||||
final String newToken = await DeviceUtils.ensureFcmToken(prefs);
|
||||
if (newToken.isNotEmpty) {
|
||||
await refreshSession(phone: normalized);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
return lastDecision!;
|
||||
} catch (e) {
|
||||
debugPrint('Precheck phone error: $e');
|
||||
lastDecision = AuthNext.error;
|
||||
return lastDecision!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> sendOtp([String? phoneArg]) async {
|
||||
if (sendingOtp.value) return false;
|
||||
if (phoneArg != null && phoneArg.isNotEmpty) {
|
||||
currentPhone = _normalizePhone(phoneArg);
|
||||
}
|
||||
if (currentPhone == null) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Phone number not set. Please enter your number again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
sendingOtp.value = true;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final phone = currentPhone!;
|
||||
|
||||
// Get cached SMS provider settings or use defaults
|
||||
String templateId =
|
||||
prefs.getString('smsTemplateId') ?? '1107173468024541800';
|
||||
String smsContent =
|
||||
prefs.getString('smsContent') ??
|
||||
'<#> Dear customer, use this One Time Password {#var#} to sign-in to Nearle App. This OTP will be valid for the next 5 mins.';
|
||||
|
||||
// Only fetch SMS provider settings if not cached or cache is old (older than 1 hour)
|
||||
final lastProviderFetch = prefs.getInt('lastProviderFetch') ?? 0;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now - lastProviderFetch > 3600000) {
|
||||
// 1 hour in milliseconds
|
||||
try {
|
||||
final providerUri = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v1/platform/getsmsprovider?templatetypeid=1',
|
||||
);
|
||||
final provRes = await http
|
||||
.get(providerUri)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (provRes.statusCode == 200) {
|
||||
final Map<String, dynamic> prov = json.decode(provRes.body);
|
||||
final details = prov['details'] as Map<String, dynamic>?;
|
||||
if (details != null) {
|
||||
templateId = (details['templateid'] ?? templateId).toString();
|
||||
smsContent = (details['content'] ?? smsContent).toString();
|
||||
// Cache the settings
|
||||
await prefs.setString('smsTemplateId', templateId);
|
||||
await prefs.setString('smsContent', smsContent);
|
||||
await prefs.setInt('lastProviderFetch', now);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// Append app hash for Android SMS Retriever so auto-fill works silently
|
||||
String appHash = '';
|
||||
try {
|
||||
appHash = await SmsAutoFill().getAppSignature;
|
||||
if (appHash.isNotEmpty) {
|
||||
_log('Using app hash for SMS Retriever: $appHash');
|
||||
}
|
||||
} catch (_) {}
|
||||
// Generate OTP ourselves (like the original implementation)
|
||||
String actualOtp = _generateOtp();
|
||||
await prefs.setString('lastOtp', actualOtp);
|
||||
_log('Generated OTP: $actualOtp');
|
||||
|
||||
// Replace {#var#} with actual OTP before sending to Lion SMS
|
||||
final composedSmsBase = smsContent.replaceAll('{#var#}', actualOtp);
|
||||
final composedSms = appHash.isNotEmpty
|
||||
? ('$composedSmsBase\n$appHash')
|
||||
: composedSmsBase;
|
||||
final phoneWithCountry = phone.startsWith('+') ? phone : '+91$phone';
|
||||
final encodedSms = Uri.encodeComponent(composedSms);
|
||||
final smsUrl = Uri.parse(
|
||||
'https://msg.lionsms.com/api/smsapi?key=e57f5c9679af26077be1a7eadabb1b2a&route=7&sender=NEARLE&number=$phoneWithCountry&templateid=$templateId&sms=$encodedSms',
|
||||
);
|
||||
final smsRes = await http
|
||||
.get(smsUrl)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
if (smsRes.statusCode == 200 &&
|
||||
!(smsRes.body.contains('108') ||
|
||||
smsRes.body.contains('110') ||
|
||||
smsRes.body.toLowerCase().contains('error'))) {
|
||||
return true;
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'OTP Send Failed',
|
||||
'Provider: ${smsRes.statusCode} ${smsRes.body}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('sendOtp error: $e');
|
||||
Get.snackbar('Error', 'An unexpected error occurred while sending OTP.');
|
||||
return false;
|
||||
} finally {
|
||||
sendingOtp.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> verifyOtp(String code) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final sentOtp = prefs.getString('lastOtp');
|
||||
if (sentOtp != null && code == sentOtp) {
|
||||
await prefs.remove('lastOtp');
|
||||
_log('OTP verification successful: $code');
|
||||
return true;
|
||||
}
|
||||
_log('OTP verification failed. Expected: $sentOtp, Got: $code');
|
||||
return false;
|
||||
} catch (e) {
|
||||
_log('OTP verification error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 6-digit OTP (same as original implementation)
|
||||
String _generateOtp() {
|
||||
final random = Random();
|
||||
final otp = 100000 + random.nextInt(900000); // Generates 100000-999999
|
||||
return otp.toString();
|
||||
}
|
||||
|
||||
Future<bool> setPin(String newPin) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
int? userId =
|
||||
prefs.getInt(_prefsPendingPinUserIdKey) ??
|
||||
prefs.getInt(_prefsUserIdKey);
|
||||
if (newPin.length != 4 || int.tryParse(newPin) == null) {
|
||||
_showBottomSheet(
|
||||
title: 'Invalid PIN',
|
||||
message: 'Please enter a valid 4-digit PIN.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (userId == null) {
|
||||
_showBottomSheet(
|
||||
title: 'Error',
|
||||
message: 'User ID not found. Please try again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final int pinNum = int.parse(newPin);
|
||||
final res = await _api.updatePin(userId: userId, pin: pinNum);
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
await prefs.setString('dbPin', newPin);
|
||||
await prefs.remove(_prefsPendingPinUserIdKey);
|
||||
return true;
|
||||
}
|
||||
final bodyPreview = res.body.length > 200
|
||||
? '${res.body.substring(0, 200)}...'
|
||||
: res.body;
|
||||
_showBottomSheet(
|
||||
title: 'Failed (${res.statusCode})',
|
||||
message: 'Unable to set PIN. Server said: $bodyPreview',
|
||||
);
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('setPin error: $e');
|
||||
_showBottomSheet(
|
||||
title: 'Error',
|
||||
message: 'Something went wrong while setting the PIN.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh session on backend with latest deviceId/FCM for the current phone.
|
||||
Future<bool> refreshSession({String? phone}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String? usePhone = phone ?? currentPhone;
|
||||
if (usePhone == null || usePhone.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final deviceId = await DeviceUtils.ensureDeviceId(prefs);
|
||||
final fcmToken = await DeviceUtils.ensureFcmToken(prefs);
|
||||
final Login loginRes = await _api.loginParsed(
|
||||
contactNo: usePhone,
|
||||
deviceType: Platform.operatingSystem,
|
||||
configId: 6,
|
||||
deviceId: deviceId,
|
||||
fcmToken: fcmToken,
|
||||
);
|
||||
if (loginRes.userid != null) {
|
||||
await prefs.setInt(_prefsUserIdKey, loginRes.userid!);
|
||||
}
|
||||
try {
|
||||
final String? name = loginRes.fullname ?? loginRes.firstname;
|
||||
final String? email = loginRes.email;
|
||||
final String contact = (loginRes.contactno ?? usePhone).toString();
|
||||
final String? address = loginRes.address;
|
||||
if (name != null && name.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsUserNameKey, name.trim());
|
||||
}
|
||||
if (email != null && email.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsUserEmailKey, email.trim());
|
||||
}
|
||||
if (contact.isNotEmpty) {
|
||||
final normalizedContact = _normalizePhone(contact);
|
||||
await prefs.setString(_prefsContactNoKey, normalizedContact);
|
||||
}
|
||||
if (address != null && address.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsAddressKey, address.trim());
|
||||
}
|
||||
await _notifyProfileController();
|
||||
} catch (_) {}
|
||||
currentPhone = _normalizePhone(usePhone);
|
||||
return loginRes.status == true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> verifyPinWithServer(String inputPin) async {
|
||||
try {
|
||||
if (inputPin.length != 4 || int.tryParse(inputPin) == null) {
|
||||
_showBottomSheet(
|
||||
title: 'Invalid PIN',
|
||||
message: 'Please enter a valid 4-digit PIN.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final String? phone = currentPhone;
|
||||
if (phone == null || phone.isEmpty) {
|
||||
_showBottomSheet(
|
||||
title: 'Session Expired',
|
||||
message: 'Please enter your number again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final bool masterPinActive =
|
||||
_forceMasterPinFlow ||
|
||||
(prefs.getBool(_prefsForceMasterPinKey) ?? false);
|
||||
if (masterPinActive) {
|
||||
if (inputPin != _masterPinValue) {
|
||||
_showBottomSheet(title: 'Invalid PIN', message: 'Please try again.');
|
||||
return false;
|
||||
}
|
||||
_forceMasterPinFlow = false;
|
||||
await prefs.remove(_prefsForceMasterPinKey);
|
||||
await prefs.setString('dbPin', _masterPinValue);
|
||||
await prefs.setBool('logged_out', false);
|
||||
|
||||
// Call loginParsed with the master pin to fetch and save all API data
|
||||
// This ensures shared_preferences has all the necessary data like regular pins
|
||||
String deviceId;
|
||||
try {
|
||||
deviceId = await DeviceUtils.ensureDeviceId(prefs);
|
||||
} catch (e) {
|
||||
_showBottomSheet(
|
||||
title: 'Device Error',
|
||||
message:
|
||||
'Failed to get device ID. Please restart the app and try again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final String fcmToken = await DeviceUtils.ensureFcmToken(prefs);
|
||||
final Login loginRes = await _api.loginParsed(
|
||||
contactNo: phone,
|
||||
deviceType: Platform.operatingSystem,
|
||||
configId: 6,
|
||||
deviceId: deviceId,
|
||||
fcmToken: fcmToken,
|
||||
pin: int.parse(_masterPinValue),
|
||||
);
|
||||
|
||||
// Save user details from API response
|
||||
if (loginRes.userid != null) {
|
||||
await prefs.setInt(_prefsUserIdKey, loginRes.userid!);
|
||||
}
|
||||
final String? name = loginRes.fullname ?? loginRes.firstname;
|
||||
final String? email = loginRes.email;
|
||||
final String contact = (loginRes.contactno ?? currentPhone ?? '')
|
||||
.toString();
|
||||
final String? address = loginRes.address;
|
||||
if (name != null && name.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsUserNameKey, name.trim());
|
||||
}
|
||||
if (email != null && email.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsUserEmailKey, email.trim());
|
||||
}
|
||||
if (contact.isNotEmpty) {
|
||||
await prefs.setString(_prefsContactNoKey, _normalizePhone(contact));
|
||||
}
|
||||
if (address != null && address.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsAddressKey, address.trim());
|
||||
}
|
||||
try {
|
||||
if (currentPhone != null && currentPhone!.isNotEmpty) {
|
||||
currentPhone = _normalizePhone(currentPhone!);
|
||||
}
|
||||
await _notifyProfileController();
|
||||
} catch (_) {}
|
||||
|
||||
return true;
|
||||
}
|
||||
String deviceId;
|
||||
try {
|
||||
deviceId = await DeviceUtils.ensureDeviceId(prefs);
|
||||
} catch (e) {
|
||||
_showBottomSheet(
|
||||
title: 'Device Error',
|
||||
message:
|
||||
'Failed to get device ID. Please restart the app and try again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final String fcmToken = await DeviceUtils.ensureFcmToken(prefs);
|
||||
final Login loginRes = await _api.loginParsed(
|
||||
contactNo: phone,
|
||||
deviceType: Platform.operatingSystem,
|
||||
configId: 6,
|
||||
deviceId: deviceId,
|
||||
fcmToken: fcmToken,
|
||||
pin: int.parse(inputPin),
|
||||
);
|
||||
final String msg = (loginRes.message ?? '').toLowerCase();
|
||||
if ((loginRes.code != null && loginRes.code == 401) ||
|
||||
msg.contains('invalid pin')) {
|
||||
_showBottomSheet(
|
||||
title: 'Invalid PIN',
|
||||
message: 'Incorrect PIN. Please try again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (loginRes.status == true || msg.contains('success')) {
|
||||
await prefs.setString('dbPin', inputPin);
|
||||
if (loginRes.userid != null) {
|
||||
await prefs.setInt(_prefsUserIdKey, loginRes.userid!);
|
||||
}
|
||||
await prefs.setBool('logged_out', false);
|
||||
final String? name = loginRes.fullname ?? loginRes.firstname;
|
||||
final String? email = loginRes.email;
|
||||
final String contact = (loginRes.contactno ?? currentPhone ?? '')
|
||||
.toString();
|
||||
final String? address = loginRes.address;
|
||||
if (name != null && name.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsUserNameKey, name.trim());
|
||||
}
|
||||
if (email != null && email.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsUserEmailKey, email.trim());
|
||||
}
|
||||
if (contact.isNotEmpty) {
|
||||
await prefs.setString(_prefsContactNoKey, _normalizePhone(contact));
|
||||
}
|
||||
if (address != null && address.trim().isNotEmpty) {
|
||||
await prefs.setString(_prefsAddressKey, address.trim());
|
||||
}
|
||||
try {
|
||||
if (currentPhone != null && currentPhone!.isNotEmpty) {
|
||||
currentPhone = _normalizePhone(currentPhone!);
|
||||
}
|
||||
await _notifyProfileController();
|
||||
} catch (_) {}
|
||||
// Ensure backend session is refreshed on this device with latest FCM/device id
|
||||
try {
|
||||
await refreshSession(phone: currentPhone);
|
||||
} catch (_) {}
|
||||
return true;
|
||||
}
|
||||
_showBottomSheet(
|
||||
title: 'Invalid PIN',
|
||||
message: 'Incorrect PIN. Please try again.',
|
||||
);
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('verifyPinWithServer error: $e');
|
||||
_showBottomSheet(
|
||||
title: 'Error',
|
||||
message: 'Failed to verify PIN. Try again.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
lib/controllers/connectivity_mixin.dart
Normal file
55
lib/controllers/connectivity_mixin.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
mixin ConnectivityControllerMixin on GetxController {
|
||||
final RxBool isOnline = true.obs;
|
||||
Timer? _tick;
|
||||
|
||||
@protected
|
||||
Future<bool> checkInternet() async {
|
||||
try {
|
||||
final result = await InternetAddress.lookup('example.com');
|
||||
return result.isNotEmpty && result.first.rawAddress.isNotEmpty;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
void onConnectivityOnline() {}
|
||||
|
||||
@protected
|
||||
void onConnectivityOffline() {}
|
||||
|
||||
void _startWatcher() {
|
||||
_tick?.cancel();
|
||||
_tick = Timer.periodic(const Duration(seconds: 5), (_) async {
|
||||
final ok = await checkInternet();
|
||||
final prev = isOnline.value;
|
||||
if (ok != prev) {
|
||||
isOnline.value = ok;
|
||||
if (ok) {
|
||||
onConnectivityOnline();
|
||||
} else {
|
||||
onConnectivityOffline();
|
||||
}
|
||||
} else {
|
||||
isOnline.value = ok;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_startWatcher();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_tick?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
2042
lib/controllers/deliveries_controller.dart
Normal file
2042
lib/controllers/deliveries_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
125
lib/controllers/delivery.dart
Normal file
125
lib/controllers/delivery.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeliveryController extends GetxController {
|
||||
// Exposed reactive fields if needed in UI
|
||||
final RxString orderId = ''.obs;
|
||||
final RxString orderStatus = ''.obs;
|
||||
final RxInt tenantId = 0.obs;
|
||||
final RxInt partnerId = 0.obs;
|
||||
final RxInt locationId = 0.obs;
|
||||
final RxInt orderHeaderId = 0.obs;
|
||||
final RxInt deliveryId = 0.obs;
|
||||
final RxInt userId = 0.obs;
|
||||
final RxString latitude = ''.obs;
|
||||
final RxString longitude = ''.obs;
|
||||
|
||||
// Preference keys (aligned with existing usage in app)
|
||||
static const String kTenantId = 'delivery_tenantid';
|
||||
static const String kPartnerId = 'delivery_partnerid';
|
||||
static const String kLocationId = 'delivery_locationid';
|
||||
static const String kOrderHeaderId = 'delivery_orderheaderid';
|
||||
static const String kDeliveryId = 'delivery_deliveryid';
|
||||
static const String kUserId = 'delivery_userid';
|
||||
static const String kOrderId = 'delivery_orderid';
|
||||
static const String kOrderStatus = 'delivery_orderstatus';
|
||||
static const String kLat = 'delivery_latitude';
|
||||
static const String kLng = 'delivery_longitude';
|
||||
|
||||
Future<void> saveFromQueueItem(Map<String, dynamic> delivery) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final int? tenantid = delivery['tenantid'] is int
|
||||
? delivery['tenantid'] as int
|
||||
: int.tryParse('${delivery['tenantid'] ?? ''}');
|
||||
final int? partnerid = delivery['partnerid'] is int
|
||||
? delivery['partnerid'] as int
|
||||
: int.tryParse('${delivery['partnerid'] ?? ''}');
|
||||
final int? locationid = delivery['locationid'] is int
|
||||
? delivery['locationid'] as int
|
||||
: int.tryParse('${delivery['locationid'] ?? ''}');
|
||||
final int? orderheaderid = delivery['orderheaderid'] is int
|
||||
? delivery['orderheaderid'] as int
|
||||
: int.tryParse('${delivery['orderheaderid'] ?? ''}');
|
||||
final int? deliveryid = delivery['deliveryid'] is int
|
||||
? delivery['deliveryid'] as int
|
||||
: int.tryParse('${delivery['deliveryid'] ?? ''}');
|
||||
final int? userid = delivery['userid'] is int
|
||||
? delivery['userid'] as int
|
||||
: int.tryParse('${delivery['userid'] ?? ''}');
|
||||
|
||||
final String oid = (delivery['orderid'] ?? '').toString();
|
||||
final String ostatus = (delivery['orderstatus'] ?? '').toString();
|
||||
|
||||
// Prefer deliverylat/long, fallback to droplat/lon
|
||||
final String lat = (delivery['deliverylat'] ?? delivery['droplat'] ?? '')
|
||||
.toString();
|
||||
final String lon = (delivery['deliverylong'] ?? delivery['droplon'] ?? '')
|
||||
.toString();
|
||||
|
||||
if (tenantid != null) await prefs.setInt(kTenantId, tenantid);
|
||||
if (partnerid != null) await prefs.setInt(kPartnerId, partnerid);
|
||||
if (locationid != null) await prefs.setInt(kLocationId, locationid);
|
||||
if (orderheaderid != null) await prefs.setInt(kOrderHeaderId, orderheaderid);
|
||||
if (deliveryid != null) await prefs.setInt(kDeliveryId, deliveryid);
|
||||
if (userid != null) await prefs.setInt(kUserId, userid);
|
||||
if (oid.isNotEmpty) await prefs.setString(kOrderId, oid);
|
||||
if (ostatus.isNotEmpty) await prefs.setString(kOrderStatus, ostatus);
|
||||
if (lat.isNotEmpty) await prefs.setString(kLat, lat);
|
||||
if (lon.isNotEmpty) await prefs.setString(kLng, lon);
|
||||
|
||||
// Update observables
|
||||
orderId.value = oid;
|
||||
orderStatus.value = ostatus;
|
||||
tenantId.value = tenantid ?? 0;
|
||||
partnerId.value = partnerid ?? 0;
|
||||
locationId.value = locationid ?? 0;
|
||||
orderHeaderId.value = orderheaderid ?? 0;
|
||||
deliveryId.value = deliveryid ?? 0;
|
||||
userId.value = userid ?? 0;
|
||||
latitude.value = lat;
|
||||
longitude.value = lon;
|
||||
}
|
||||
|
||||
Future<void> loadFromPrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
tenantId.value = prefs.getInt(kTenantId) ?? 0;
|
||||
partnerId.value = prefs.getInt(kPartnerId) ?? 0;
|
||||
locationId.value = prefs.getInt(kLocationId) ?? 0;
|
||||
orderHeaderId.value = prefs.getInt(kOrderHeaderId) ?? 0;
|
||||
deliveryId.value = prefs.getInt(kDeliveryId) ?? 0;
|
||||
userId.value = prefs.getInt(kUserId) ?? 0;
|
||||
orderId.value = prefs.getString(kOrderId) ?? '';
|
||||
orderStatus.value = prefs.getString(kOrderStatus) ?? '';
|
||||
latitude.value = prefs.getString(kLat) ?? '';
|
||||
longitude.value = prefs.getString(kLng) ?? '';
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(kTenantId);
|
||||
await prefs.remove(kPartnerId);
|
||||
await prefs.remove(kLocationId);
|
||||
await prefs.remove(kOrderHeaderId);
|
||||
await prefs.remove(kDeliveryId);
|
||||
await prefs.remove(kUserId);
|
||||
await prefs.remove(kOrderId);
|
||||
await prefs.remove(kOrderStatus);
|
||||
await prefs.remove(kLat);
|
||||
await prefs.remove(kLng);
|
||||
|
||||
orderId.value = '';
|
||||
orderStatus.value = '';
|
||||
tenantId.value = 0;
|
||||
partnerId.value = 0;
|
||||
locationId.value = 0;
|
||||
orderHeaderId.value = 0;
|
||||
deliveryId.value = 0;
|
||||
userId.value = 0;
|
||||
latitude.value = '';
|
||||
longitude.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
183
lib/controllers/logcontroller.dart
Normal file
183
lib/controllers/logcontroller.dart
Normal file
@@ -0,0 +1,183 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:nearle/providers/deliverylog/deliverylog_provider.dart';
|
||||
import 'package:nearle/views/helpers/constants/apiconstants.dart';
|
||||
import 'package:nearle/background/foreground_service.dart' as fg;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
/// Controller for managing active delivery logs
|
||||
/// Now delegates to the background service for actual logging
|
||||
class LogController extends GetxController {
|
||||
final CreateDeliveryLogProvider _logProvider = CreateDeliveryLogProvider();
|
||||
|
||||
static const String _offlineLogKey = 'offline_delivery_logs';
|
||||
bool _isFlushing = false;
|
||||
|
||||
/// Start the delivery log streaming service (via foreground service)
|
||||
Future<void> startLogging() async {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG] Requesting start logging...');
|
||||
|
||||
// Attempt to flush offline logs on start
|
||||
flushOfflineLogs();
|
||||
|
||||
// Only show foreground notification when rider is actually on duty
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
if (onduty != 1) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG] Skipping startLogging because onduty=$onduty',
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// If prefs fail, continue with best-effort start
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
if (await FlutterForegroundTask.isRunningService) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG] Foreground service already running');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG] Starting foreground service for delivery logs',
|
||||
);
|
||||
|
||||
FlutterForegroundTask.init(
|
||||
androidNotificationOptions: AndroidNotificationOptions(
|
||||
channelId: 'nearle_bg_service',
|
||||
channelName: 'Background Service',
|
||||
channelDescription:
|
||||
'Keeps Nearle online updates running in background.',
|
||||
channelImportance: NotificationChannelImportance.LOW,
|
||||
priority: NotificationPriority.LOW,
|
||||
),
|
||||
iosNotificationOptions: const IOSNotificationOptions(
|
||||
showNotification: true,
|
||||
playSound: false,
|
||||
),
|
||||
foregroundTaskOptions: ForegroundTaskOptions(
|
||||
interval: 30000, // 30 seconds
|
||||
isOnceEvent: false,
|
||||
autoRunOnBoot: false,
|
||||
allowWakeLock: true,
|
||||
allowWifiLock: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Check permissions before starting service to prevent Android 14 crash
|
||||
final permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG] Location permission missing, skipping service start',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await FlutterForegroundTask.startService(
|
||||
notificationTitle: 'Nearle is running',
|
||||
notificationText: 'You are Currently on Duty !',
|
||||
callback: fg.riderLogCallback,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG] Failed to start service: $e');
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG] iOS/Web not fully supported for background service yet',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the delivery log streaming service
|
||||
/// Note: This might stop rider logs too if they share the service.
|
||||
/// Usually we only stop if the user goes off-duty or logs out.
|
||||
void stopLogging() {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG] Stop logging requested (no-op to preserve rider logs)',
|
||||
);
|
||||
// We do not stop the service here because it might be running for Rider Logs.
|
||||
// The service should be stopped by RiderLogController when going off-duty.
|
||||
}
|
||||
|
||||
// ---------------- Offline Queue Logic (Foreground Helper) ----------------
|
||||
|
||||
/// Call this on app start or network restoration
|
||||
Future<void> flushOfflineLogs() async {
|
||||
if (_isFlushing) return;
|
||||
_isFlushing = true;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final List<String> queue = prefs.getStringList(_offlineLogKey) ?? [];
|
||||
if (queue.isEmpty) return;
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][OFFLINE] Flushing ${queue.length} offline logs...',
|
||||
);
|
||||
|
||||
final List<String> remaining = [];
|
||||
bool anySuccess = false;
|
||||
|
||||
// Determine API endpoint
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
for (final itemStr in queue) {
|
||||
try {
|
||||
final Map<String, dynamic> item = jsonDecode(itemStr);
|
||||
final String orderId = item['orderId'] ?? '';
|
||||
final Map<String, dynamic> payload = Map<String, dynamic>.from(
|
||||
item['payload'] ?? {},
|
||||
);
|
||||
|
||||
if (payload.isEmpty) continue;
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][OFFLINE] Retrying for orderId: $orderId',
|
||||
);
|
||||
|
||||
final result = await _logProvider
|
||||
.createDeliveryLog(url, payload)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
|
||||
if (result != null) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][OFFLINE] Success for orderId: $orderId',
|
||||
);
|
||||
anySuccess = true;
|
||||
} else {
|
||||
remaining.add(itemStr);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][OFFLINE] Error processing item: $e',
|
||||
);
|
||||
remaining.add(itemStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (anySuccess || remaining.length != queue.length) {
|
||||
await prefs.setStringList(_offlineLogKey, remaining);
|
||||
debugPrint(
|
||||
'[ACTIVE_DELIVERY_LOG][OFFLINE] Flush complete. Remaining: ${remaining.length}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE_DELIVERY_LOG][OFFLINE] Flush error: $e');
|
||||
} finally {
|
||||
_isFlushing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
lib/controllers/profile_controller.dart
Normal file
36
lib/controllers/profile_controller.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'dart:io';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class ProfileController extends GetxController {
|
||||
final RxString imagePath = ''.obs;
|
||||
final RxString userName = ''.obs;
|
||||
final RxString userEmail = ''.obs;
|
||||
final RxString userContact = ''.obs;
|
||||
final RxString userAddress = ''.obs;
|
||||
|
||||
void setImagePath(String path) {
|
||||
imagePath.value = path;
|
||||
}
|
||||
|
||||
Future<void> loadFromPrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
userName.value = (prefs.getString('user_name') ?? '').trim();
|
||||
userEmail.value = (prefs.getString('user_email') ?? '').trim();
|
||||
userContact.value = (prefs.getString('contactno') ?? '').trim();
|
||||
userAddress.value = (prefs.getString('user_address') ?? '').trim();
|
||||
}
|
||||
|
||||
void setProfile({String? name, String? email, String? contact, String? address}) {
|
||||
if (name != null && name.trim().isNotEmpty) userName.value = name.trim();
|
||||
if (email != null && email.trim().isNotEmpty) userEmail.value = email.trim();
|
||||
if (contact != null && contact.trim().isNotEmpty) userContact.value = contact.trim();
|
||||
if (address != null && address.trim().isNotEmpty) userAddress.value = address.trim();
|
||||
}
|
||||
|
||||
File? get fileOrNull {
|
||||
final path = imagePath.value;
|
||||
if (path.isEmpty) return null;
|
||||
return File(path);
|
||||
}
|
||||
}
|
||||
52
lib/controllers/rewards_controller.dart
Normal file
52
lib/controllers/rewards_controller.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class RewardsController extends GetxController {
|
||||
final RxInt totalPoints = 0.obs;
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxString error = ''.obs;
|
||||
|
||||
Future<void> fetchBonusSummary(int userId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// Using dynamic userid passed from ProfilePage
|
||||
final url = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v1/utils/getuserbonussummary/?userid=$userId',
|
||||
);
|
||||
|
||||
final response = await http.get(
|
||||
url,
|
||||
headers: {'Accept': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
if (data is Map<String, dynamic> && data['status'] == true) {
|
||||
// Check 'details' first, then 'data'
|
||||
final dynamic content = data['details'] ?? data['data'];
|
||||
|
||||
if (content is Map<String, dynamic>) {
|
||||
// "bonuspts": 40
|
||||
totalPoints.value =
|
||||
int.tryParse((content['bonuspts'] ?? '0').toString()) ?? 0;
|
||||
debugPrint('[REWARDS] Fetched bonuspts: ${totalPoints.value}');
|
||||
} else if (content is List && content.isNotEmpty) {
|
||||
final first = content.first;
|
||||
if(first is Map<String, dynamic>) {
|
||||
totalPoints.value = int.tryParse((first['bonuspts'] ?? '0').toString()) ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error.value = 'Failed to load rewards';
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e.toString();
|
||||
debugPrint('[REWARDS] Error: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
lib/controllers/riderkm.dart
Normal file
33
lib/controllers/riderkm.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nearle/Models/summary/riderweeklykms.dart';
|
||||
import 'package:nearle/views/helpers/constants/apiconstants.dart';
|
||||
|
||||
|
||||
class RiderWeeklyKmController {
|
||||
final String baseUrl = ApiConstants.summaryriderkmLive;
|
||||
|
||||
Future<Map<String, dynamic>> getRiderWeeklyKms(int userId) async {
|
||||
final url = Uri.parse("$baseUrl/getriderweeklykms?userid=$userId");
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final body = json.decode(response.body);
|
||||
if (body['status'] == true) {
|
||||
final details = (body['details'] as List)
|
||||
.map((e) => RiderWeeklyKms.fromJson(e))
|
||||
.toList();
|
||||
|
||||
return {
|
||||
'details': details,
|
||||
'total_kms': double.tryParse('${body['total_kms'] ?? 0}') ?? 0.0,
|
||||
};
|
||||
} else {
|
||||
throw Exception(body['message'] ?? "API returned false status");
|
||||
}
|
||||
} else {
|
||||
throw Exception("Failed to fetch (code: ${response.statusCode})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1281
lib/controllers/riderlog.dart
Normal file
1281
lib/controllers/riderlog.dart
Normal file
File diff suppressed because it is too large
Load Diff
35
lib/controllers/summary_controller.dart
Normal file
35
lib/controllers/summary_controller.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/models/summary/deliverystats.dart';
|
||||
import 'package:nearle/providers/summary/summary.dart';
|
||||
|
||||
|
||||
class SummaryController extends GetxController {
|
||||
final SummaryProvider _provider = SummaryProvider();
|
||||
|
||||
// Observables
|
||||
var today = 0.obs;
|
||||
var week = 0.obs;
|
||||
var month = 0.obs;
|
||||
var total = 0.obs;
|
||||
var cancelled = 0.obs;
|
||||
var isLoading = false.obs;
|
||||
|
||||
// Fetch stats and update values
|
||||
Future<void> fetchSummaryStats(int userId) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final DeliveryStats? stats = await _provider.fetchSummaryStats(userId);
|
||||
if (stats != null) {
|
||||
today.value = stats.today;
|
||||
week.value = stats.week;
|
||||
month.value = stats.month;
|
||||
total.value = stats.total;
|
||||
cancelled.value = stats.cancelled;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Controller error: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
lib/controllers/support_ticket.dart
Normal file
209
lib/controllers/support_ticket.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' show Random;
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:minio/io.dart';
|
||||
import 'package:minio/minio.dart';
|
||||
import 'package:nearle/Models/supportticket/support_ticket.dart';
|
||||
|
||||
// Minimal local stub for DigitalOcean Spaces client to avoid undefined name errors.
|
||||
// Replace this with a real package or implementation for production uploads.
|
||||
class dospace {
|
||||
static DOSpaceClient DOSpace({
|
||||
required String region,
|
||||
required String accessKey,
|
||||
required String secretKey,
|
||||
}) =>
|
||||
DOSpaceClient(region: region, accessKey: accessKey, secretKey: secretKey);
|
||||
|
||||
static final ACL = _ACL();
|
||||
}
|
||||
|
||||
class _ACL {
|
||||
final String publicRead = 'public-read';
|
||||
}
|
||||
|
||||
class DOSpaceClient {
|
||||
final String region;
|
||||
final String accessKey;
|
||||
final String secretKey;
|
||||
|
||||
DOSpaceClient({
|
||||
required this.region,
|
||||
required this.accessKey,
|
||||
required this.secretKey,
|
||||
});
|
||||
|
||||
Future<void> putObject({
|
||||
required String bucketName,
|
||||
required String objectName,
|
||||
required File file,
|
||||
required String acl,
|
||||
required String contentType,
|
||||
}) async {
|
||||
// No-op stub: implement actual upload logic here or use a proper package.
|
||||
await Future<void>.value();
|
||||
}
|
||||
}
|
||||
|
||||
class SupportTicketController extends GetxController {
|
||||
final RxList<SupportTicketModel> tickets = <SupportTicketModel>[].obs;
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxBool isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchTickets();
|
||||
}
|
||||
|
||||
// ---------- FETCH TICKETS ----------
|
||||
Future<void> fetchTickets() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
errorMessage('');
|
||||
|
||||
const userId = 1242;
|
||||
final url = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v1/partners/getridersupport/?userid=$userId');
|
||||
|
||||
final response = await http.get(url, headers: {
|
||||
'Accept': 'application/json',
|
||||
});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server error: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> jsonResponse = json.decode(response.body);
|
||||
if (jsonResponse['status'] != true) {
|
||||
throw Exception(jsonResponse['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonResponse['data'];
|
||||
tickets.assignAll(data.map((e) => SupportTicketModel.fromJson(e)).toList());
|
||||
} catch (e) {
|
||||
errorMessage(e.toString());
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- UPLOAD IMAGE TO DO SPACES ----------
|
||||
Future<String?> uploadImageToDOSpaces(File imageFile, int userId) async {
|
||||
try {
|
||||
final rng = Random();
|
||||
const String region = "sgp1";
|
||||
const String accessKey = "DO00NQER7N2FRYZAB2HR";
|
||||
const String secretKey = "nMDewX25IBEu1FM5dakK+v28/WbW3TzBAwq913+dxP0";
|
||||
const String bucketName = "nearle";
|
||||
const String folderName = "support";
|
||||
|
||||
// File name
|
||||
final String fileName = 'ticket-${rng.nextInt(10000)}-$userId.jpg';
|
||||
|
||||
// Object path inside the bucket
|
||||
final String objectPath = "$folderName/$fileName";
|
||||
|
||||
// CDN URL you want
|
||||
final String cdnUrl = "https://images.nearle.app/$objectPath";
|
||||
|
||||
// Initialize Minio
|
||||
final minio = Minio(
|
||||
endPoint: "$region.digitaloceanspaces.com",
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
region: region,
|
||||
useSSL: true,
|
||||
);
|
||||
|
||||
print("Uploading: $objectPath");
|
||||
|
||||
// Upload to DO Spaces
|
||||
await minio.fPutObject(
|
||||
bucketName,
|
||||
objectPath,
|
||||
imageFile.path,
|
||||
metadata: {
|
||||
"Content-Type": "image/jpeg",
|
||||
"x-amz-acl": "public-read",
|
||||
},
|
||||
);
|
||||
|
||||
print("Uploaded Successfully: $cdnUrl");
|
||||
return cdnUrl;
|
||||
} catch (e) {
|
||||
print("Upload error: $e");
|
||||
Get.snackbar("Error", "Image upload failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------- CREATE TICKET ----------
|
||||
// ---------- CREATE TICKET ----------
|
||||
Future<bool> createTicket({
|
||||
required int userid,
|
||||
required String category,
|
||||
required String priority,
|
||||
required String subject,
|
||||
required String issue,
|
||||
List<XFile>? attachments,
|
||||
}) async {
|
||||
try {
|
||||
isSubmitting(true);
|
||||
String imageUrl = "";
|
||||
|
||||
// Upload first image if attached
|
||||
if (attachments != null && attachments.isNotEmpty) {
|
||||
final XFile xFile = attachments.first;
|
||||
final File file = File(xFile.path);
|
||||
final uploadedUrl = await uploadImageToDOSpaces(file, userid);
|
||||
|
||||
// Only assign if a valid URL (short length)
|
||||
if (uploadedUrl != null && uploadedUrl.length < 200) {
|
||||
imageUrl = uploadedUrl;
|
||||
} else {
|
||||
print("⚠️ Skipping image URL because it’s too long or invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
// Create ticket request body
|
||||
final Map<String, dynamic> payload = {
|
||||
'userid': userid,
|
||||
'category': category,
|
||||
'priority': priority,
|
||||
'subject': subject,
|
||||
'issue': issue,
|
||||
'image': imageUrl, // ✅ Always short string or empty
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('https://jupiter.nearle.app/live/api/v1/partners/createridersupport/'),
|
||||
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Ticket creation failed: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final jsonResponse = jsonDecode(response.body);
|
||||
if (jsonResponse['status'] != true) {
|
||||
throw Exception(jsonResponse['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
await fetchTickets(); // Refresh list after success
|
||||
return true;
|
||||
} catch (e) {
|
||||
errorMessage(e.toString());
|
||||
return false;
|
||||
} finally {
|
||||
isSubmitting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
lib/helpers/http_overrides.dart
Normal file
10
lib/helpers/http_overrides.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
class MyHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
return super.createHttpClient(context)
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
}
|
||||
}
|
||||
126
lib/helpers/shift_end_alarm.dart
Normal file
126
lib/helpers/shift_end_alarm.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/background/backgroundservice.dart';
|
||||
|
||||
/// Helper class to schedule/cancel shift end alarms
|
||||
/// Works even when app is killed (uses Android AlarmManager)
|
||||
class ShiftEndAlarm {
|
||||
static const MethodChannel _channel = MethodChannel('nearle/shift_end');
|
||||
|
||||
/// Schedule an alarm for shift end time
|
||||
/// This alarm will fire even if the app is killed
|
||||
/// If shift end time has already passed today, it will trigger immediately
|
||||
static Future<bool> scheduleAlarm({
|
||||
required String endTime, // Format: "HH:mm:ss" or "HH:mm"
|
||||
String startTime = '', // Format: "HH:mm:ss" or "HH:mm" (for overnight shift detection)
|
||||
}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
debugPrint('[SHIFT_END_ALARM] Only supported on Android');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('[SHIFT_END_ALARM] 📅 scheduleAlarm called - endTime: "$endTime", startTime: "$startTime"');
|
||||
|
||||
// Check if shift end time has already passed today
|
||||
final now = DateTime.now();
|
||||
debugPrint('[SHIFT_END_ALARM] 📅 Current time: ${now.toString()}');
|
||||
|
||||
final endParts = endTime.split(':');
|
||||
if (endParts.length >= 2) {
|
||||
final int endHour = int.tryParse(endParts[0]) ?? 0;
|
||||
final int endMinute = int.tryParse(endParts[1]) ?? 0;
|
||||
final int endSecond = endParts.length > 2 ? (int.tryParse(endParts[2]) ?? 0) : 0;
|
||||
|
||||
final DateTime endToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
endHour,
|
||||
endMinute,
|
||||
endSecond,
|
||||
);
|
||||
|
||||
debugPrint('[SHIFT_END_ALARM] 📅 Shift end time today: ${endToday.toString()}');
|
||||
debugPrint('[SHIFT_END_ALARM] 📅 Time comparison: now.isAfter(endToday) = ${now.isAfter(endToday)}');
|
||||
|
||||
// If shift end time has passed, trigger immediately
|
||||
if (now.isAfter(endToday) || now.isAtSameMomentAs(endToday)) {
|
||||
debugPrint('[SHIFT_END_ALARM] ⚡ Shift end time ($endTime) has already passed - triggering break log immediately');
|
||||
await handleShiftEnd();
|
||||
} else {
|
||||
debugPrint('[SHIFT_END_ALARM] ⏰ Shift end time ($endTime) has not passed yet - will schedule alarm');
|
||||
}
|
||||
} else {
|
||||
debugPrint('[SHIFT_END_ALARM] ⚠️ Invalid endTime format: "$endTime"');
|
||||
}
|
||||
|
||||
final result = await _channel.invokeMethod<bool>(
|
||||
'scheduleShiftEndAlarm',
|
||||
{
|
||||
'endTime': endTime,
|
||||
'startTime': startTime,
|
||||
},
|
||||
);
|
||||
final success = result ?? false;
|
||||
if (success) {
|
||||
debugPrint('[SHIFT_END_ALARM] ✅ Successfully scheduled alarm for shift end: $endTime');
|
||||
} else {
|
||||
debugPrint('[SHIFT_END_ALARM] ⚠️ Failed to schedule alarm for shift end: $endTime');
|
||||
}
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('[SHIFT_END_ALARM] ❌ Error scheduling alarm: ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('[SHIFT_END_ALARM] ❌ Unexpected error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the scheduled shift end alarm
|
||||
static Future<bool> cancelAlarm() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _channel.invokeMethod<bool>('cancelShiftEndAlarm');
|
||||
debugPrint('[SHIFT_END_ALARM] ✅ Cancelled shift end alarm');
|
||||
return result ?? false;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('[SHIFT_END_ALARM] ❌ Error cancelling alarm: ${e.message}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('[SHIFT_END_ALARM] ❌ Unexpected error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle shift end when alarm fires (called by BroadcastReceiver)
|
||||
/// This will create the break log and set rider offline
|
||||
static Future<void> handleShiftEnd() async {
|
||||
try {
|
||||
debugPrint('[SHIFT_END_ALARM] 🔔 Shift end alarm fired - creating break log...');
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Check if still on duty (might have been manually set offline)
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
if (onduty != 1) {
|
||||
debugPrint('[SHIFT_END_ALARM] Already offline, skipping break log creation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the background service to create break log
|
||||
// This will create break log and set offline
|
||||
await BackgroundDeliveryLog.checkShiftEnd();
|
||||
|
||||
debugPrint('[SHIFT_END_ALARM] ✅ Break log created successfully');
|
||||
} catch (e) {
|
||||
debugPrint('[SHIFT_END_ALARM] ❌ Error handling shift end: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
347
lib/main.dart
Normal file
347
lib/main.dart
Normal file
@@ -0,0 +1,347 @@
|
||||
import 'dart:io';
|
||||
import 'package:nearle/helpers/http_overrides.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/introscreens/splashscreen.dart';
|
||||
import 'package:nearle/controllers/profile_controller.dart';
|
||||
import 'package:nearle/controllers/riderlog.dart';
|
||||
import 'package:nearle/controllers/delivery.dart';
|
||||
import 'package:nearle/controllers/logcontroller.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:nearle/providers/notifications/notificationservce.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/updatescreen/UpdateScreen.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; // ⭐ ADDED
|
||||
import 'package:nearle/background/backgroundservice.dart';
|
||||
import 'package:nearle/helpers/shift_end_alarm.dart';
|
||||
|
||||
import 'views/offline/offline_page.dart';
|
||||
|
||||
// Background message handler
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
await NotificationServce.display(message);
|
||||
}
|
||||
|
||||
String currentVersion = '';
|
||||
String storeVersion = '';
|
||||
bool updateRequired = false;
|
||||
|
||||
Future<void> getAppVersion() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
String version = packageInfo.version;
|
||||
currentVersion = version;
|
||||
prefs.setString('CurrentVersion', currentVersion);
|
||||
print('Current version from main: $currentVersion');
|
||||
}
|
||||
|
||||
Future<void> checkForUpdate(BuildContext context) async {
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.nearle.partner",
|
||||
);
|
||||
|
||||
final status = await newVersion.getVersionStatus();
|
||||
|
||||
print('The status = $status');
|
||||
|
||||
if (status != null) {
|
||||
print("Current Version: ${status.localVersion}");
|
||||
print("Store Version: ${status.storeVersion}");
|
||||
print("Can Update: ${status.canUpdate}");
|
||||
|
||||
if (status.canUpdate) {
|
||||
currentVersion = status.localVersion;
|
||||
storeVersion = status.storeVersion;
|
||||
updateRequired = true;
|
||||
|
||||
Get.offAll(
|
||||
() => UpdateScreen(
|
||||
mCurrentVersion: status.localVersion,
|
||||
mUpdateVersion: status.storeVersion,
|
||||
mIsForceUpdate: true,
|
||||
),
|
||||
transition: Transition.fadeIn,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> recheckVersion() async {
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.nearle.partner",
|
||||
);
|
||||
|
||||
try {
|
||||
final status = await newVersion.getVersionStatus();
|
||||
if (status != null) {
|
||||
print(
|
||||
"Recheck - Current: ${status.localVersion}, Store: ${status.storeVersion}",
|
||||
);
|
||||
return status.canUpdate;
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error rechecking version: $e");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
|
||||
await Firebase.initializeApp();
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
|
||||
|
||||
Get.put(ProfileController(), permanent: true);
|
||||
Get.put(RiderLogController(), permanent: true);
|
||||
Get.put(DeliveryController(), permanent: true);
|
||||
final logController = Get.put(LogController(), permanent: true);
|
||||
|
||||
await logController.startLogging();
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(const _RootApp());
|
||||
}
|
||||
|
||||
class _RootApp extends StatefulWidget {
|
||||
const _RootApp();
|
||||
|
||||
@override
|
||||
State<_RootApp> createState() => _RootAppState();
|
||||
}
|
||||
|
||||
class _RootAppState extends State<_RootApp> with WidgetsBindingObserver {
|
||||
List<ConnectivityResult> _connectionStatus = [ConnectivityResult.none];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
Connectivity().onConnectivityChanged.listen((result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_connectionStatus = result;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Connectivity().checkConnectivity().then((result) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_connectionStatus = result;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ Check if shift ended while app was killed OR if shift end time has already passed
|
||||
_checkShiftEndOnStartup();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Check if shift ended while app was killed OR if shift end time has already passed
|
||||
/// This will trigger break log immediately if rider is on duty and shift end time passed
|
||||
Future<void> _checkShiftEndOnStartup() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final int onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
// Only check if rider was on duty
|
||||
if (onduty != 1) {
|
||||
debugPrint('[APP_STARTUP] Rider not on duty, skipping shift end check');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if shift has ended
|
||||
final String endTimeStr = prefs.getString('endtime') ?? '';
|
||||
if (endTimeStr.isEmpty) {
|
||||
debugPrint('[APP_STARTUP] No endtime found, skipping shift end check');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[APP_STARTUP] Checking shift end - Current time: ${DateTime.now()}, End time: $endTimeStr',
|
||||
);
|
||||
|
||||
// Use the same logic as BackgroundDeliveryLog.checkShiftEnd()
|
||||
final now = DateTime.now();
|
||||
final endParts = endTimeStr.split(':');
|
||||
if (endParts.length < 2) return;
|
||||
|
||||
final int endHour = int.tryParse(endParts[0]) ?? 0;
|
||||
final int endMinute = int.tryParse(endParts[1]) ?? 0;
|
||||
final int endSecond = endParts.length > 2
|
||||
? (int.tryParse(endParts[2]) ?? 0)
|
||||
: 0;
|
||||
|
||||
final DateTime endToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
endHour,
|
||||
endMinute,
|
||||
endSecond,
|
||||
);
|
||||
|
||||
bool isShiftOver = false;
|
||||
final String startTimeStr = prefs.getString('starttime') ?? '';
|
||||
|
||||
if (startTimeStr.isNotEmpty) {
|
||||
final startParts = startTimeStr.split(':');
|
||||
if (startParts.length >= 2) {
|
||||
final int startHour = int.tryParse(startParts[0]) ?? 0;
|
||||
final int startMinute = int.tryParse(startParts[1]) ?? 0;
|
||||
|
||||
final double startVal = startHour + (startMinute / 60.0);
|
||||
final double endVal = endHour + (endMinute / 60.0);
|
||||
|
||||
if (startVal > endVal) {
|
||||
// Overnight shift
|
||||
final DateTime startToday = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
startHour,
|
||||
startMinute,
|
||||
);
|
||||
|
||||
if (now.isAfter(endToday) && now.isBefore(startToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
} else {
|
||||
// Normal day shift
|
||||
if (now.isAfter(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (now.isAfter(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (now.isAfter(endToday)) {
|
||||
isShiftOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isShiftOver) {
|
||||
debugPrint(
|
||||
'[APP_STARTUP] ⏰ Shift end time ($endTimeStr) has already passed - creating break log IMMEDIATELY...',
|
||||
);
|
||||
// Call the background service to create break log
|
||||
await BackgroundDeliveryLog.checkShiftEnd();
|
||||
debugPrint('[APP_STARTUP] ✅ Break log created (if needed)');
|
||||
} else {
|
||||
debugPrint(
|
||||
'[APP_STARTUP] Shift end time ($endTimeStr) has not passed yet - scheduling alarm',
|
||||
);
|
||||
// Also reschedule the alarm in case it was cancelled
|
||||
try {
|
||||
final String startTime = prefs.getString('starttime') ?? '';
|
||||
await ShiftEndAlarm.scheduleAlarm(
|
||||
endTime: endTimeStr,
|
||||
startTime: startTime,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[APP_STARTUP] Error rescheduling alarm: $e');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[APP_STARTUP] Error checking shift end: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
// When app comes back to foreground, ensure rider logs resume
|
||||
try {
|
||||
if (Get.isRegistered<RiderLogController>()) {
|
||||
final ctl = Get.find<RiderLogController>();
|
||||
// Fire an immediate rider log (best-effort)
|
||||
ctl.createLoginNowV2();
|
||||
// Ensure the periodic background/foreground loop is running
|
||||
ctl.startAutoCreateLoginLoop();
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore lifecycle errors; app should not crash because of logging
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isOffline {
|
||||
return _connectionStatus.isEmpty ||
|
||||
_connectionStatus.every((status) => status == ConnectivityResult.none);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ⭐ RESPONSIVE WRAPPER ADDED
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(390, 844), // YOUR BASE SIZE
|
||||
minTextAdapt: true,
|
||||
splitScreenMode: true,
|
||||
builder: (_, __) {
|
||||
return GetMaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Nearle partner',
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Proxima Nova',
|
||||
useMaterial3: true,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
foregroundColor: Colors.black,
|
||||
systemOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
),
|
||||
home: const Splashscreen(),
|
||||
|
||||
builder: (context, child) {
|
||||
final baseChild = child ?? const SizedBox.shrink();
|
||||
|
||||
if (_isOffline) {
|
||||
return Stack(children: [baseChild, const OfflinePage()]);
|
||||
}
|
||||
|
||||
return baseChild;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
244
lib/providers/Riderlog/riderlog_provider.dart
Normal file
244
lib/providers/Riderlog/riderlog_provider.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Riderlog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
|
||||
/// Confirmed by Python test: 66.116.225.226 = 200 OK, 125.21.240.67 = 404.
|
||||
const _knownGoodIPs = <String, String>{
|
||||
'queue.workolik.com': '66.116.225.226',
|
||||
};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
httpClient.connectionFactory =
|
||||
(Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
// Use known-good IP if available, else resolve normally (prefer IPv4)
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture =
|
||||
Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then((plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host, // SNI = original hostname for Nginx routing
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
))
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
|
||||
class CreateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
const maxAttempts = 3;
|
||||
try {
|
||||
debugPrint('createRiderLog payload ${json.encode(data)}');
|
||||
} catch (_) {}
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createRiderLog url $urldata (attempt $attempt)');
|
||||
debugPrint('createRiderLog status ${response.statusCode}');
|
||||
debugPrint('createRiderLog response ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint(
|
||||
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
|
||||
);
|
||||
// On 404/5xx, wait and retry to potentially hit a different CDN node
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('createRiderLog exception (attempt $attempt): $e');
|
||||
if (attempt < maxAttempts) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('createRiderLog failed after $maxAttempts attempts');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UpdateRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> updateRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('updateRiderLog url: $urldata');
|
||||
debugPrint('updateRiderLog response: ${response.body}');
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded;
|
||||
} else {
|
||||
debugPrint('⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}');
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Exception in updateRiderLog: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GetRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
|
||||
Map<String, dynamic>? getRiderLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(url, headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
});
|
||||
debugPrint('getRiderLog response ${response.body}');
|
||||
debugPrint('getRiderLog url ${urldata.toString()}');
|
||||
getRiderLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
|
||||
Map<String, dynamic>? getRiderCountResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await get(url, headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
});
|
||||
debugPrint('getRiderCount response ${response.body}');
|
||||
debugPrint('getRiderCount url ${urldata.toString()}');
|
||||
getRiderCountResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return getRiderCountResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class BreakRiderLogProvider {
|
||||
Future<Map<String, dynamic>?> createBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await post(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('createBreakRiderLog url $urldata');
|
||||
debugPrint('createBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateBreakRiderLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
Map<String, dynamic>? breakLogResponse;
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
debugPrint('updateBreakRiderLog url $urldata');
|
||||
debugPrint('updateBreakRiderLog response ${response.body}');
|
||||
breakLogResponse =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
return breakLogResponse;
|
||||
}
|
||||
}
|
||||
200
lib/providers/auth/auth_provider.dart
Normal file
200
lib/providers/auth/auth_provider.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:nearle/Models/login/login.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:nearle/controllers/riderlog.dart';
|
||||
|
||||
class AuthProvider {
|
||||
Future<http.Response> login({
|
||||
required String contactNo,
|
||||
required String deviceType,
|
||||
required int configId,
|
||||
required String deviceId,
|
||||
required String fcmToken,
|
||||
int? pin,
|
||||
}) async {
|
||||
final uri = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v2/users/rider/login',
|
||||
);
|
||||
final body = {
|
||||
'contactno': contactNo,
|
||||
'devicetype': deviceType,
|
||||
'configid': configId,
|
||||
'deviceid': deviceId,
|
||||
'userfcmtoken': fcmToken,
|
||||
if (pin != null) 'pin': pin,
|
||||
};
|
||||
debugPrint('[AUTH][LOGIN] URL: ${uri.toString()}');
|
||||
debugPrint('[AUTH][LOGIN] Body: ${json.encode(body)}');
|
||||
final res = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode(body),
|
||||
);
|
||||
debugPrint('[AUTH][LOGIN] Status: ${res.statusCode}');
|
||||
debugPrint('[AUTH][LOGIN] Response: ${res.body}');
|
||||
return res;
|
||||
}
|
||||
|
||||
// Convenience: send using a Login model body
|
||||
Future<http.Response> loginWith(Login request) async {
|
||||
final uri = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v2/users/rider/login',
|
||||
);
|
||||
final body = request.toJson();
|
||||
debugPrint('[AUTH][LOGIN] URL: ${uri.toString()}');
|
||||
debugPrint('[AUTH][LOGIN] Body: ${json.encode(body)}');
|
||||
final res = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode(body),
|
||||
);
|
||||
debugPrint('[AUTH][LOGIN] Status: ${res.statusCode}');
|
||||
debugPrint('[AUTH][LOGIN] Response: ${res.body}');
|
||||
return res;
|
||||
}
|
||||
|
||||
// Convenience: parsed response as Login model
|
||||
Future<Login> loginParsed({
|
||||
required String contactNo,
|
||||
required String deviceType,
|
||||
required int configId,
|
||||
required String deviceId,
|
||||
required String fcmToken,
|
||||
int? pin,
|
||||
}) async {
|
||||
final res = await login(
|
||||
contactNo: contactNo,
|
||||
deviceType: deviceType,
|
||||
configId: configId,
|
||||
deviceId: deviceId,
|
||||
fcmToken: fcmToken,
|
||||
pin: pin,
|
||||
);
|
||||
final Map<String, dynamic> jsonMap = res.body.isNotEmpty
|
||||
? json.decode(res.body) as Map<String, dynamic>
|
||||
: <String, dynamic>{};
|
||||
debugPrint('[AUTH] Raw Login JSON: $jsonMap');
|
||||
|
||||
if (jsonMap.containsKey('details')) {
|
||||
final details = jsonMap['details'];
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('userid', details['userid'] ?? 0);
|
||||
await prefs.setInt('userId', details['userid'] ?? 0);
|
||||
await prefs.setInt('shiftid', details['shiftid'] ?? 0);
|
||||
await prefs.setInt('shiftId', details['shiftid'] ?? 0);
|
||||
await prefs.setInt('logid', details['logid'] ?? 0);
|
||||
await prefs.setInt('logId', details['logid'] ?? 0);
|
||||
await prefs.setInt('riderid', details['riderid'] ?? 0);
|
||||
await prefs.setInt('partnerid', details['partnerid'] ?? 0);
|
||||
await prefs.setInt('partnerId', details['partnerid'] ?? 0);
|
||||
await prefs.setInt('configid', details['configid'] ?? 0);
|
||||
await prefs.setInt('logseconds', details['logseconds'] ?? 0);
|
||||
|
||||
await prefs.setInt('locationid', details['locationid'] ?? 0);
|
||||
await prefs.setInt('tenantid', details['tenantid'] ?? 0);
|
||||
await prefs.setInt('applocationid', details['applocationid'] ?? 0);
|
||||
|
||||
final String fcm = (details['userfcmtoken'] ?? '').toString();
|
||||
if (fcm.isNotEmpty) {
|
||||
await prefs.setString('userfcmtoken', fcm);
|
||||
}
|
||||
|
||||
// Persist rider name variants for downstream usage (e.g. rider logs)
|
||||
final String firstName = (details['firstname'] ?? '').toString();
|
||||
final String lastName = (details['lastname'] ?? '').toString();
|
||||
final String apiUsername = (details['username'] ?? '').toString();
|
||||
final String combinedName = ('$firstName $lastName').trim();
|
||||
|
||||
if (apiUsername.isNotEmpty) {
|
||||
await prefs.setString('username', apiUsername);
|
||||
} else if (combinedName.isNotEmpty) {
|
||||
await prefs.setString('username', combinedName);
|
||||
}
|
||||
if (firstName.isNotEmpty) {
|
||||
await prefs.setString('firstname', firstName);
|
||||
}
|
||||
if (lastName.isNotEmpty) {
|
||||
await prefs.setString('lastname', lastName);
|
||||
}
|
||||
if (details['onduty'] != null) {
|
||||
final int od = (details['onduty'] is num)
|
||||
? (details['onduty'] as num).toInt()
|
||||
: int.tryParse('${details['onduty']}') ?? 0;
|
||||
await prefs.setInt('onduty', od);
|
||||
}
|
||||
// Persist rider payout config (per-kilometer fuel/rider charge) if provided
|
||||
if (details.containsKey('fuelcharge')) {
|
||||
final double fuelCharge =
|
||||
double.tryParse('${details['fuelcharge']}') ?? 0.0;
|
||||
await prefs.setDouble('fuelcharge', fuelCharge);
|
||||
}
|
||||
// Backward compatibility with older field names
|
||||
if (details.containsKey('firstmilecharge')) {
|
||||
final double firstMileCharge =
|
||||
double.tryParse('${details['firstmilecharge']}') ?? 0.0;
|
||||
await prefs.setDouble('firstmilecharge', firstMileCharge);
|
||||
} else if (details.containsKey('firstmilecharges')) {
|
||||
final double firstMileCharge =
|
||||
double.tryParse('${details['firstmilecharges']}') ?? 0.0;
|
||||
await prefs.setDouble('firstmilecharge', firstMileCharge);
|
||||
}
|
||||
// Save shift window for header display
|
||||
if (details['starttime'] != null) {
|
||||
await prefs.setString('starttime', details['starttime'].toString());
|
||||
}
|
||||
await prefs.setString('endtime', details['endtime'].toString());
|
||||
|
||||
// Save delivery radius for geofencing (default 100m if not provided)
|
||||
if (details['deliveryradius'] != null) {
|
||||
final int radius = (details['deliveryradius'] is num)
|
||||
? (details['deliveryradius'] as num).toInt()
|
||||
: int.tryParse('${details['deliveryradius']}') ?? 100;
|
||||
await prefs.setInt('deliveryradius', radius);
|
||||
debugPrint('[AUTH] Saved deliveryradius: $radius meters');
|
||||
} else {
|
||||
await prefs.setInt('deliveryradius', 100); // Default
|
||||
debugPrint('[AUTH] Saved default deliveryradius: 100 meters');
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[AUTH] SharedPrefs Saved: '
|
||||
'userid=${details['userid']}, shiftid=${details['shiftid']}, '
|
||||
'logid=${details['logid']}, riderid=${details['riderid']},'
|
||||
'partnerid=${details['partnerid']}, configid=${details['configid']}',
|
||||
);
|
||||
|
||||
// Rider log creation is deferred until the rider goes ON duty.
|
||||
//
|
||||
// NOTE: We intentionally do NOT auto-navigate from here anymore.
|
||||
// Navigation after login / PIN verification is handled in the UI flows
|
||||
// (e.g. MPIN screen) so that riders cannot reach the homepage before
|
||||
// successfully entering a valid PIN.
|
||||
}
|
||||
|
||||
return Login.fromJson(jsonMap);
|
||||
}
|
||||
|
||||
Future<http.Response> updatePin({
|
||||
required int userId,
|
||||
required int pin,
|
||||
}) async {
|
||||
final uri = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v2/users/update',
|
||||
);
|
||||
final body = {'userid': userId, 'pin': pin};
|
||||
debugPrint('[AUTH][UPDATE PIN] URL: ${uri.toString()}');
|
||||
debugPrint('[AUTH][UPDATE PIN] Body: ${json.encode(body)}');
|
||||
final res = await http.put(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode(body),
|
||||
);
|
||||
debugPrint('[AUTH][UPDATE PIN] Status: ${res.statusCode}');
|
||||
debugPrint('[AUTH][UPDATE PIN] Response: ${res.body}');
|
||||
return res;
|
||||
}
|
||||
}
|
||||
141
lib/providers/delivery/delivery_provider.dart
Normal file
141
lib/providers/delivery/delivery_provider.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nearle/views/helpers/constants/apiconstants.dart';
|
||||
|
||||
class DeliveryProvider {
|
||||
final http.Client _client;
|
||||
|
||||
DeliveryProvider({http.Client? client}) : _client = client ?? http.Client();
|
||||
|
||||
Future<http.Response> _getWithRetry(Uri uri, {int maxAttempts = 3}) async {
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
attempt++;
|
||||
try {
|
||||
return await _client.get(uri);
|
||||
} on SocketException catch (_) {
|
||||
if (attempt >= maxAttempts) rethrow;
|
||||
} on http.ClientException catch (_) {
|
||||
if (attempt >= maxAttempts) rethrow;
|
||||
}
|
||||
// Exponential backoff: 300ms, 600ms
|
||||
final delayMs = 300 * attempt;
|
||||
await Future.delayed(Duration(milliseconds: delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getDeliveryQueues({
|
||||
required bool live,
|
||||
required int userid,
|
||||
String? orderstatus,
|
||||
}) async {
|
||||
final base = live ? ApiConstants.deliveryQueueLive : ApiConstants.deliveryQueueDev;
|
||||
|
||||
// Get current date in YYYY-MM-DD format
|
||||
final now = DateTime.now();
|
||||
final mm = now.month.toString().padLeft(2, '0');
|
||||
final dd = now.day.toString().padLeft(2, '0');
|
||||
final yyyy = now.year.toString();
|
||||
final today = "$yyyy-$mm-$dd";
|
||||
|
||||
final qp = <String, String>{
|
||||
'userid': userid.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
};
|
||||
if (orderstatus != null && orderstatus.isNotEmpty) {
|
||||
qp['orderstatus'] = orderstatus;
|
||||
}
|
||||
final uri = Uri.parse(base).replace(queryParameters: qp);
|
||||
|
||||
final res = await _getWithRetry(uri);
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// Log URL and raw status
|
||||
// ignore: avoid_print
|
||||
print('[DELIVERIES][GET] URL: ${uri.toString()}');
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
try {
|
||||
// Pretty log the list if deliveries are found
|
||||
if (data.isNotEmpty) {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET] Data: ${json.encode(data)}'); // Heavy log, disabled for performance
|
||||
} else {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET] Data: []');
|
||||
}
|
||||
} catch (_) {}
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) return data['items'] as List;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
throw Exception('Failed (${res.statusCode})');
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getDeliveryQueuesPicked({
|
||||
required bool live,
|
||||
required int userid,
|
||||
}) async {
|
||||
// Use v3 getdeliveries with fromdate/todate as today's date (dynamic)
|
||||
final base = live
|
||||
? ApiConstants.currentDeliveryV3Live
|
||||
: ApiConstants.currentDeliveryV3Dev;
|
||||
// Get current date in YYYY-MM-DD format (dynamically updated each day)
|
||||
final now = DateTime.now();
|
||||
final mm = now.month.toString().padLeft(2, '0');
|
||||
final dd = now.day.toString().padLeft(2, '0');
|
||||
final yyyy = now.year.toString();
|
||||
final today = "$yyyy-$mm-$dd";
|
||||
|
||||
final qp = <String, String>{
|
||||
'userid': userid.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
};
|
||||
final uri = Uri.parse(base).replace(queryParameters: qp);
|
||||
|
||||
final res = await _getWithRetry(uri);
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// ignore: avoid_print
|
||||
print('[DELIVERIES][GET_PICKED] URL: ${uri.toString()}');
|
||||
final decoded = json.decode(res.body);
|
||||
|
||||
// The API returns {"code":200,"details":[...],"message":"Success","status":true}
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
if (data is List) {
|
||||
try {
|
||||
if (data.isNotEmpty) {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET_PICKED] Data: ${json.encode(data)}'); // Heavy log, disabled
|
||||
} else {
|
||||
// ignore: avoid_print
|
||||
// print('[DELIVERIES][GET_PICKED] Data: []');
|
||||
}
|
||||
} catch (_) {}
|
||||
return data;
|
||||
}
|
||||
if (data is Map && data['items'] is List) return data['items'] as List;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
throw Exception('Failed (${res.statusCode})');
|
||||
}
|
||||
}
|
||||
268
lib/providers/deliverylog/deliverylog_provider.dart
Normal file
268
lib/providers/deliverylog/deliverylog_provider.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// Combined Deliverylog providers:
|
||||
|
||||
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken CDN nodes.
|
||||
/// Confirmed: 66.116.225.226 = 200 OK, 125.21.240.67 = 404/405.
|
||||
const _knownGoodIPs = <String, String>{
|
||||
'queue.workolik.com': '66.116.225.226',
|
||||
};
|
||||
|
||||
/// Creates an IOClient that:
|
||||
/// 1. Bypasses SSL certificate errors
|
||||
/// 2. Forces known-good IPs to avoid broken CDN nodes from carrier DNS
|
||||
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
|
||||
IOClient _buildSslBypassClient() {
|
||||
final httpClient = HttpClient()
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
httpClient.connectionFactory =
|
||||
(Uri uri, String? proxyHost, int? proxyPort) async {
|
||||
final host = uri.host;
|
||||
final port = uri.port;
|
||||
|
||||
InternetAddress? target;
|
||||
final knownIP = _knownGoodIPs[host];
|
||||
if (knownIP != null) {
|
||||
target = InternetAddress(knownIP);
|
||||
} else {
|
||||
try {
|
||||
final addresses = await InternetAddress.lookup(
|
||||
host,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
if (addresses.isNotEmpty) target = addresses.first;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (uri.scheme == 'https') {
|
||||
final socketFuture =
|
||||
Socket.connect(target ?? InternetAddress(host), port)
|
||||
.then((plain) => SecureSocket.secure(
|
||||
plain,
|
||||
host: host,
|
||||
onBadCertificate: (_) => true,
|
||||
supportedProtocols: ['http/1.1'],
|
||||
))
|
||||
.then((s) => s as Socket);
|
||||
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
|
||||
}
|
||||
|
||||
return Socket.startConnect(target ?? InternetAddress(host), port);
|
||||
};
|
||||
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
class CreateDeliveryLogProvider {
|
||||
Future<Map<String, dynamic>?> createDeliveryLog(
|
||||
String urldata,
|
||||
Map<String, dynamic> data, {
|
||||
bool wrapInArray = true,
|
||||
}) async {
|
||||
Map<String, dynamic>? result;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final body = json.encode(wrapInArray ? [data] : data);
|
||||
final response = await client.post(
|
||||
url,
|
||||
body: body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
debugPrint('createDeliveryLog url $urldata');
|
||||
debugPrint(body);
|
||||
debugPrint('createDeliveryLog response ${response.body}');
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
result = json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
debugPrint('createDeliveryLog parsed ${result.toString()}');
|
||||
} else {
|
||||
debugPrint('createDeliveryLog failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('createDeliveryLog timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('createDeliveryLog error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateDeliveryProvider {
|
||||
Future<Map<String, dynamic>?> updateDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateDelivery url $urldata');
|
||||
debugPrint('updateDelivery status ${response.statusCode}');
|
||||
debugPrint('updateDelivery response ${response.body}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updateDelivery failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updateDelivery timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updateDelivery error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateArrivedDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateArrived url $urldata');
|
||||
debugPrint('updateArrived status ${response.statusCode}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updateArrived failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updateArrived timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updateArrived error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updatePickedDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
debugPrint('updatePicked url $urldata');
|
||||
debugPrint('updatePicked status ${response.statusCode}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updatePicked failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updatePicked timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updatePicked error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> updateActiveDelivery(
|
||||
Map<String, dynamic> data,
|
||||
String urldata,
|
||||
) async {
|
||||
Map<String, dynamic>? updateDeliveryResponse;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
debugPrint('updateActive url $urldata');
|
||||
debugPrint('updateActive status ${response.statusCode}');
|
||||
debugPrint(json.encode(data));
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
updateDeliveryResponse =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
debugPrint('updateActive failed: HTTP ${response.statusCode}');
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
debugPrint('updateActive timeout: $e');
|
||||
} catch (e) {
|
||||
debugPrint('updateActive error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return updateDeliveryResponse;
|
||||
}
|
||||
}
|
||||
|
||||
class GetDeliveryLogProvider {
|
||||
Future<Map<String, dynamic>?> getDeliveryLog(String urldata) async {
|
||||
Map<String, dynamic>? result;
|
||||
final client = _buildSslBypassClient();
|
||||
try {
|
||||
final url = Uri.parse(urldata);
|
||||
final response = await client.get(url, headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
});
|
||||
debugPrint('getDeliveryLog url $urldata');
|
||||
debugPrint('getDeliveryLog response ${response.body}');
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
result =
|
||||
json.decode(response.body.toString()) as Map<String, dynamic>;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('getDeliveryLog error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
402
lib/providers/notifications/notificationservce.dart
Normal file
402
lib/providers/notifications/notificationservce.dart
Normal file
@@ -0,0 +1,402 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:nearle/helpers/http_overrides.dart';
|
||||
|
||||
// Top-level background handler required by Firebase Messaging
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (_) {}
|
||||
|
||||
await NotificationServce.display(message);
|
||||
}
|
||||
|
||||
class NotificationServce {
|
||||
static final FirebaseMessaging _firebaseMessaging =
|
||||
FirebaseMessaging.instance;
|
||||
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
static final AudioPlayer _player = AudioPlayer();
|
||||
static String _channelId = 'Nearle';
|
||||
static bool _isPlaying = false;
|
||||
|
||||
static const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'Nearle',
|
||||
'Nearle Notification',
|
||||
description: 'Channel for Nearle notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
static Future<void> initialize(BuildContext context) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final alreadyInit = prefs.getBool('notifications_init_done') ?? false;
|
||||
if (alreadyInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
final existing = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (existing.isEmpty) {
|
||||
await prefs.setString('order_alert_sound', 'assets/audio/alert-1.mp3');
|
||||
}
|
||||
await prefs.setBool('notifications_init_done', true);
|
||||
} catch (_) {}
|
||||
|
||||
await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
await _applyChannelSoundFromPrefs();
|
||||
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
iOS: DarwinInitializationSettings(
|
||||
requestSoundPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestAlertPermission: true,
|
||||
defaultPresentSound: true,
|
||||
defaultPresentBadge: true,
|
||||
defaultPresentBanner: true,
|
||||
defaultPresentAlert: true,
|
||||
defaultPresentList: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {},
|
||||
);
|
||||
|
||||
RemoteMessage? initialMessage =
|
||||
await _firebaseMessaging.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
await _handleInitialMessage(initialMessage);
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||
await _handleMessage(message);
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
|
||||
await _handleMessageOpenedApp(message);
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Removed duplicated background handler (this was breaking your notifications)
|
||||
|
||||
static Future<void> _handleInitialMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessageOpenedApp(RemoteMessage message) async {}
|
||||
|
||||
static Future<void> _applyChannelSoundFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String sel = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (sel.isEmpty) {
|
||||
sel = 'assets/audio/alert-1.mp3';
|
||||
}
|
||||
|
||||
final fileName = sel.split('/').last;
|
||||
final base = fileName.split('.').first;
|
||||
final rawName = base.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||
|
||||
final androidImpl = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
|
||||
if (androidImpl != null) {
|
||||
_channelId = 'Nearle_$rawName';
|
||||
|
||||
final custom = AndroidNotificationChannel(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
description: 'Channel for Nearle notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound(rawName),
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
await androidImpl.createNotificationChannel(custom);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<String?> _downloadAndSaveImage(
|
||||
String imageUrl, String fileName) async {
|
||||
try {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final filePath = '${directory.path}/$fileName';
|
||||
final response = await http.get(Uri.parse(imageUrl));
|
||||
if (response.statusCode == 200) {
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
return filePath;
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String? _extractImageUrl(RemoteMessage message) {
|
||||
String? imageUrl = message.data['image'] as String?;
|
||||
imageUrl ??= message.notification?.android?.imageUrl;
|
||||
imageUrl ??= message.notification?.apple?.imageUrl;
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
static Future<void> _playSelectedSound({int times = 1}) async {
|
||||
try {
|
||||
if (_isPlaying) return;
|
||||
|
||||
_isPlaying = true;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String selected = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (selected.isEmpty) {
|
||||
selected = 'assets/audio/alert-1.mp3';
|
||||
}
|
||||
|
||||
final rel = selected.startsWith('assets/')
|
||||
? selected.replaceFirst('assets/', '')
|
||||
: selected;
|
||||
|
||||
await _player.stop();
|
||||
await _player.setReleaseMode(ReleaseMode.stop);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
await _player.play(AssetSource(rel));
|
||||
try {
|
||||
await _player.onPlayerComplete.first;
|
||||
} catch (_) {}
|
||||
if (i < times - 1) {
|
||||
await Future.delayed(const Duration(milliseconds: 120));
|
||||
}
|
||||
}
|
||||
} catch (_) {} finally {
|
||||
_isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight local notification helper for in-app events (no FCM message).
|
||||
static Future<void> showLocalNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
bool playSound = true,
|
||||
String? payload,
|
||||
}) async {
|
||||
try {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: playSound,
|
||||
enableVibration: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: playSound,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> display(RemoteMessage message) async {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final payload = jsonEncode({'id': id.toString(), 'data': message.data});
|
||||
|
||||
final appState = WidgetsBinding.instance.lifecycleState;
|
||||
final bool isForeground = appState == AppLifecycleState.resumed;
|
||||
|
||||
if (isForeground) {
|
||||
await _playSelectedSound(times: 5);
|
||||
}
|
||||
|
||||
NotificationDetails notificationDetails;
|
||||
|
||||
final imageUrl = _extractImageUrl(message);
|
||||
String? persistedImageUrl = imageUrl;
|
||||
String? persistedImagePath;
|
||||
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
final imagePath = await _downloadAndSaveImage(
|
||||
imageUrl,
|
||||
'notification_image.jpg',
|
||||
);
|
||||
persistedImagePath = imagePath;
|
||||
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: !isForeground,
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
styleInformation: imagePath != null
|
||||
? BigPictureStyleInformation(FilePathAndroidBitmap(imagePath))
|
||||
: const DefaultStyleInformation(true, true),
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: !isForeground,
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isForeground) {
|
||||
final ctx = Get.context;
|
||||
if (ctx != null) {
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle';
|
||||
final body = message.notification?.body ?? message.data['body'] ?? '';
|
||||
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.w700)),
|
||||
if (body.isNotEmpty) Text(body),
|
||||
],
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle',
|
||||
message.notification?.body ?? message.data['body'] ?? 'Notification',
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final nowIso = DateTime.now().toIso8601String();
|
||||
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle';
|
||||
final body =
|
||||
message.notification?.body ?? message.data['body'] ?? '';
|
||||
|
||||
final entry = {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'time': nowIso,
|
||||
'data': message.data,
|
||||
if (persistedImageUrl != null) 'imageUrl': persistedImageUrl,
|
||||
if (persistedImagePath != null) 'imagePath': persistedImagePath,
|
||||
};
|
||||
|
||||
final existingRaw = prefs.getString('notifications_log');
|
||||
List<dynamic> list = [];
|
||||
|
||||
if (existingRaw != null && existingRaw.isNotEmpty) {
|
||||
try {
|
||||
list = jsonDecode(existingRaw) as List<dynamic>;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
list.insert(0, entry);
|
||||
|
||||
if (list.length > 100) list = list.sublist(0, 100);
|
||||
|
||||
await prefs.setString('notifications_log', jsonEncode(list));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
32
lib/providers/summary/riderweeklykms.dart
Normal file
32
lib/providers/summary/riderweeklykms.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/Models/summary/riderweeklykms.dart';
|
||||
import 'package:nearle/controllers/riderkm.dart';
|
||||
|
||||
|
||||
class RiderWeeklyKmProvider extends ChangeNotifier {
|
||||
final RiderWeeklyKmController _controller = RiderWeeklyKmController();
|
||||
|
||||
bool isLoading = false;
|
||||
String? error;
|
||||
List<RiderWeeklyKms> kmsList = [];
|
||||
double totalKms = 0.0;
|
||||
|
||||
Future<void> fetchRiderWeeklyKms(int userId) async {
|
||||
isLoading = true;
|
||||
error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _controller.getRiderWeeklyKms(userId);
|
||||
kmsList = result['details'];
|
||||
totalKms = result['total_kms'];
|
||||
} catch (e) {
|
||||
error = e.toString();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
lib/providers/summary/summary.dart
Normal file
33
lib/providers/summary/summary.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:nearle/models/summary/deliverystats.dart';
|
||||
import 'package:nearle/views/helpers/constants/apiconstants.dart';
|
||||
|
||||
class SummaryProvider {
|
||||
final String baseUrl = ApiConstants.summaryApiLive;
|
||||
|
||||
Future<DeliveryStats?> fetchSummaryStats(int userId) async {
|
||||
final url = Uri.parse('$baseUrl/getdeliverystats?userid=$userId');
|
||||
|
||||
try {
|
||||
print(url);
|
||||
final response = await http.get(url);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded['status'] == true && decoded['data'] != null) {
|
||||
return DeliveryStats.fromJson(decoded['data']);
|
||||
} else {
|
||||
print('API returned false status: ${decoded['message']}');
|
||||
}
|
||||
} else {
|
||||
print('something went wrong');
|
||||
}
|
||||
} catch (e) {
|
||||
print('something went wrong');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
127
lib/providers/support/support_ticket.dart
Normal file
127
lib/providers/support/support_ticket.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:get/get_state_manager/src/simple/get_controllers.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:nearle/Models/supportticket/support_ticket.dart';
|
||||
|
||||
class SupportTicketController extends GetxController {
|
||||
final RxList<SupportTicketModel> tickets = <SupportTicketModel>[].obs;
|
||||
final RxBool isLoading = true.obs;
|
||||
final RxString errorMessage = ''.obs;
|
||||
final RxBool isSubmitting = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchTickets();
|
||||
}
|
||||
|
||||
Future<void> fetchTickets() async {
|
||||
try {
|
||||
isLoading(true);
|
||||
errorMessage('');
|
||||
|
||||
const userId = 1242;
|
||||
final url = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v1/partners/getridersupport/?userid=$userId');
|
||||
|
||||
final response = await http.get(url, headers: {
|
||||
'Accept': 'application/json',
|
||||
});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Server error: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> jsonResponse = json.decode(response.body);
|
||||
if (jsonResponse['status'] != true) {
|
||||
throw Exception(jsonResponse['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
final List<dynamic> data = jsonResponse['data'];
|
||||
tickets.assignAll(data.map((e) => SupportTicketModel.fromJson(e)).toList());
|
||||
} catch (e) {
|
||||
errorMessage(e.toString());
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> createTicket({
|
||||
required int userid,
|
||||
required String category,
|
||||
required String priority,
|
||||
required String subject,
|
||||
required String issue,
|
||||
List<XFile>? attachments,
|
||||
}) async {
|
||||
try {
|
||||
isSubmitting(true);
|
||||
|
||||
// Step 1: Upload image if attached (adjust endpoint if needed)
|
||||
String? imageUrl;
|
||||
if (attachments != null && attachments.isNotEmpty) {
|
||||
// For simplicity, assume first image; upload to a temp endpoint or your main one
|
||||
final imageFile = File(attachments.first.path);
|
||||
final imageBytes = await imageFile.readAsBytes();
|
||||
final imageName = attachments.first.name;
|
||||
|
||||
// Example image upload (replace with your actual image upload endpoint)
|
||||
final uploadUrl = Uri.parse('https://jupiter.nearle.app/live/api/v1/partners/uploadimage/'); // Adjust URL
|
||||
final imageRequest = http.MultipartRequest('POST', uploadUrl)
|
||||
..files.add(http.MultipartFile.fromBytes('image', imageBytes, filename: imageName));
|
||||
imageRequest.headers['Accept'] = 'application/json';
|
||||
|
||||
final imageResponse = await imageRequest.send();
|
||||
if (imageResponse.statusCode == 200) {
|
||||
final imageJson = await http.Response.fromStream(imageResponse);
|
||||
imageUrl = json.decode(imageJson.body)['image_url']; // Assume response has 'image_url'
|
||||
} else {
|
||||
throw Exception('Image upload failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create ticket with POST
|
||||
final postUrl = Uri.parse('https://jupiter.nearle.app/live/api/v1/partners/createridersupport/');
|
||||
final body = json.encode({
|
||||
'userid': userid,
|
||||
'category': category,
|
||||
'priority': priority,
|
||||
'subject': subject,
|
||||
'issue': issue,
|
||||
'image': imageUrl, // null if no image
|
||||
});
|
||||
|
||||
final response = await http.post(
|
||||
postUrl,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to create ticket: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final Map<String, dynamic> jsonResponse = json.decode(response.body);
|
||||
if (jsonResponse['status'] != true) {
|
||||
throw Exception(jsonResponse['message'] ?? 'Unknown error');
|
||||
}
|
||||
|
||||
// Refresh tickets to show new one
|
||||
await fetchTickets();
|
||||
return true;
|
||||
} catch (e) {
|
||||
errorMessage(e.toString());
|
||||
return false;
|
||||
} finally {
|
||||
isSubmitting(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
lib/utils/device.dart
Normal file
67
lib/utils/device.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeviceUtils {
|
||||
static const String _deviceIdKey = 'deviceId';
|
||||
static const String _fcmTokenKey = 'fcmToken';
|
||||
|
||||
static Future<String> ensureDeviceId(SharedPreferences prefs) async {
|
||||
final String? existing = prefs.getString(_deviceIdKey);
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
if (kDebugMode) print('[DEVICE] Using cached device ID: $existing');
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final android = await deviceInfo.androidInfo;
|
||||
final String androidId = android.id;
|
||||
if (androidId.isNotEmpty) {
|
||||
await prefs.setString(_deviceIdKey, androidId);
|
||||
return androidId;
|
||||
} else {
|
||||
throw Exception('Android ID is empty');
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception('Failed to get device ID: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get device ID: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> ensureFcmToken(SharedPreferences prefs) async {
|
||||
try {
|
||||
final String? existing = prefs.getString(_fcmTokenKey);
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
return existing;
|
||||
}
|
||||
if (Firebase.apps.isEmpty) {
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
final FirebaseMessaging messaging = FirebaseMessaging.instance;
|
||||
final NotificationSettings settings = await messaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
if (settings.authorizationStatus == AuthorizationStatus.authorized ||
|
||||
settings.authorizationStatus == AuthorizationStatus.provisional) {
|
||||
final String? token = await messaging.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
await prefs.setString(_fcmTokenKey, token);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
231
lib/utils/kalman_filter.dart
Normal file
231
lib/utils/kalman_filter.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// A simple 4D Kalman Filter implementation for GPS smoothing.
|
||||
/// State vector x = [lat, lng, velocity_lat, velocity_lng]
|
||||
class NearleKalmanFilter {
|
||||
late List<double> x; // State estimate
|
||||
late List<List<double>> P; // Covariance matrix
|
||||
late List<List<double>> F; // State transition matrix
|
||||
late List<List<double>> H; // Measurement matrix
|
||||
late List<List<double>> R; // Measurement noise covariance
|
||||
late List<List<double>> Q; // Process noise covariance
|
||||
|
||||
NearleKalmanFilter({
|
||||
required double lat,
|
||||
required double lng,
|
||||
}) {
|
||||
// Initial state: [lat, lng, 0, 0]
|
||||
x = [lat, lng, 0, 0];
|
||||
|
||||
// Initial covariance: High uncertainty for initial velocity
|
||||
P = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1000.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1000.0],
|
||||
];
|
||||
|
||||
// State transition matrix (assuming dt = 1 for simplicity, will update in predict)
|
||||
F = [
|
||||
[1.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
// Measurement matrix: We only measure lat and lng
|
||||
H = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
];
|
||||
|
||||
// Measurement noise: GPS is typically accurate to ~5-10 meters.
|
||||
// In degrees, this is roughly 0.0001
|
||||
R = [
|
||||
[0.00001, 0.0],
|
||||
[0.0, 0.00001],
|
||||
];
|
||||
|
||||
// Process noise: How much we trust our prediction vs measurement
|
||||
Q = [
|
||||
[0.00001, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.00001, 0.0, 0.0],
|
||||
[0.0001, 0.0, 0.0001, 0.0],
|
||||
[0.0, 0.0001, 0.0, 0.0001],
|
||||
];
|
||||
}
|
||||
|
||||
/// Predict the next state
|
||||
void predict(double dt) {
|
||||
// Update F based on dt
|
||||
F[0][2] = dt;
|
||||
F[1][3] = dt;
|
||||
|
||||
// x = F * x
|
||||
final newX = List<double>.filled(4, 0);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
newX[i] += F[i][j] * x[j];
|
||||
}
|
||||
}
|
||||
x = newX;
|
||||
|
||||
// P = F * P * F^T + Q
|
||||
final FP = _multiply4x4(F, P);
|
||||
final F_T = _transpose4x4(F);
|
||||
final FPF_T = _multiply4x4(FP, F_T);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
P[i][j] = FPF_T[i][j] + Q[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the state with a new measurement
|
||||
void update(double measuredLat, double measuredLng) {
|
||||
final z = [measuredLat, measuredLng];
|
||||
|
||||
// y = z - H * x (Innovation)
|
||||
final y = [
|
||||
z[0] - (H[0][0] * x[0] + H[0][1] * x[1] + H[0][2] * x[2] + H[0][3] * x[3]),
|
||||
z[1] - (H[1][0] * x[0] + H[1][1] * x[1] + H[1][2] * x[2] + H[1][3] * x[3]),
|
||||
];
|
||||
|
||||
// S = H * P * H^T + R (Innovation covariance)
|
||||
// H is 2x4, P is 4x4, H^T is 4x2
|
||||
final HP = _multiply2x4_4x4(H, P);
|
||||
final H_T = _transpose2x4(H);
|
||||
final HPH_T = _multiply2x4_4x2(HP, H_T);
|
||||
|
||||
final S = [
|
||||
[HPH_T[0][0] + R[0][0], HPH_T[0][1] + R[0][1]],
|
||||
[HPH_T[1][0] + R[1][0], HPH_T[1][1] + R[1][1]],
|
||||
];
|
||||
|
||||
// K = P * H^T * S^-1 (Kalman gain)
|
||||
final Sinv = _inverse2x2(S);
|
||||
final PH_T = _multiply4x4_4x2(P, H_T);
|
||||
final K = _multiply4x2_2x2(PH_T, Sinv);
|
||||
|
||||
// x = x + K * y
|
||||
for (int i = 0; i < 4; i++) {
|
||||
x[i] += K[i][0] * y[0] + K[i][1] * y[1];
|
||||
}
|
||||
|
||||
// P = (I - K * H) * P
|
||||
final KH = _multiply4x2_2x4(K, H);
|
||||
final I = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
final I_KH = List.generate(4, (i) => List.generate(4, (j) => I[i][j] - KH[i][j]));
|
||||
P = _multiply4x4(I_KH, P);
|
||||
}
|
||||
|
||||
// --- Helper Math Functions ---
|
||||
|
||||
List<List<double>> _multiply4x4(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _transpose4x4(List<List<double>> A) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
C[i][j] = A[j][i];
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply2x4_4x4(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(2, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _transpose2x4(List<List<double>> A) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
C[i][j] = A[j][i];
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply2x4_4x2(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(2, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _inverse2x2(List<List<double>> A) {
|
||||
final det = A[0][0] * A[1][1] - A[0][1] * A[1][0];
|
||||
if (det == 0) return [[1, 0], [0, 1]]; // Should not happen with noise
|
||||
return [
|
||||
[A[1][1] / det, -A[0][1] / det],
|
||||
[-A[1][0] / det, A[0][0] / det],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<double>> _multiply4x4_4x2(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply4x2_2x2(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply4x2_2x4(List<List<double>> K, List<List<double>> H) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
C[i][j] += K[i][k] * H[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
}
|
||||
195
lib/utils/mqtt_service.dart
Normal file
195
lib/utils/mqtt_service.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart';
|
||||
import 'package:mqtt_client/mqtt_server_client.dart';
|
||||
import 'package:nearle/views/helpers/constants/mqtt_constants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class NearleMqttService {
|
||||
static final NearleMqttService _instance = NearleMqttService._internal();
|
||||
|
||||
factory NearleMqttService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
NearleMqttService._internal();
|
||||
|
||||
MqttServerClient? _client;
|
||||
bool _isConnected = false;
|
||||
bool _isConnecting = false; // Prevents concurrent connection attempts
|
||||
String? _currentRiderId;
|
||||
|
||||
// Unique per isolate startup — prevents client ID collision between
|
||||
// main isolate and background foreground-service isolate.
|
||||
static final String _sessionSuffix =
|
||||
DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
Future<void> connect() async {
|
||||
if (_isConnected || _isConnecting) return;
|
||||
_isConnecting = true;
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final riderIdLong = prefs.getInt('userId') ?? prefs.getInt('userid');
|
||||
if (riderIdLong == null || riderIdLong <= 0) {
|
||||
debugPrint('[MQTT] Cannot connect: No rider ID found in preferences.');
|
||||
return;
|
||||
}
|
||||
_currentRiderId = riderIdLong.toString();
|
||||
|
||||
const host = MqttConstants.brokerHost;
|
||||
final clientId = 'rider_${_currentRiderId}_$_sessionSuffix';
|
||||
|
||||
_client = MqttServerClient(host, clientId);
|
||||
_client!.port = MqttConstants.brokerPort;
|
||||
_client!.keepAlivePeriod = 30;
|
||||
_client!.autoReconnect = true;
|
||||
_client!.logging(on: false);
|
||||
|
||||
final lwtTopic =
|
||||
MqttConstants.topicRiderStatus.replaceAll('{riderId}', _currentRiderId!);
|
||||
_client!.onDisconnected = _onDisconnected;
|
||||
_client!.onConnected = _onConnected;
|
||||
_client!.onAutoReconnect = _onAutoReconnect;
|
||||
_client!.onSubscribed = _onSubscribed;
|
||||
|
||||
final connMessage = MqttConnectMessage()
|
||||
.withClientIdentifier(clientId)
|
||||
.authenticateAs(MqttConstants.username, MqttConstants.passwordString)
|
||||
.withWillTopic(lwtTopic)
|
||||
.withWillMessage(MqttConstants.statusOffline)
|
||||
.withWillQos(MqttQos.atLeastOnce)
|
||||
.withWillRetain()
|
||||
.startClean();
|
||||
|
||||
_client!.connectionMessage = connMessage;
|
||||
|
||||
debugPrint('[MQTT] Connecting to $host as $clientId...');
|
||||
await _client!.connect();
|
||||
} catch (e) {
|
||||
debugPrint('[MQTT] Connection failed: $e');
|
||||
_cleanDisconnect();
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanly disconnects and resets all state. Call on logout or duty end.
|
||||
void disconnect() {
|
||||
_publishStatus(MqttConstants.statusOffline);
|
||||
_cleanDisconnect();
|
||||
debugPrint('[MQTT] Disconnected and state cleared.');
|
||||
}
|
||||
|
||||
void _cleanDisconnect() {
|
||||
_client?.disconnect();
|
||||
_client = null;
|
||||
_isConnected = false;
|
||||
_currentRiderId = null;
|
||||
}
|
||||
|
||||
void _onConnected() {
|
||||
_isConnected = true;
|
||||
debugPrint('[MQTT] Connected successfully.');
|
||||
_publishStatus(MqttConstants.statusOnline);
|
||||
}
|
||||
|
||||
void _onDisconnected() {
|
||||
_isConnected = false;
|
||||
debugPrint('[MQTT] Disconnected from broker.');
|
||||
}
|
||||
|
||||
void _onAutoReconnect() {
|
||||
debugPrint('[MQTT] Auto-reconnecting...');
|
||||
}
|
||||
|
||||
void _onSubscribed(String topic) {
|
||||
debugPrint('[MQTT] Subscribed to topic: $topic');
|
||||
}
|
||||
|
||||
void _publishStatus(String status) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderStatus.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(status);
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!,
|
||||
retain: true);
|
||||
debugPrint('[MQTT] Published status: $status to $topic');
|
||||
}
|
||||
|
||||
// --- PUBLIC API ---
|
||||
|
||||
void updateStatus(String status) {
|
||||
_publishStatus(status);
|
||||
}
|
||||
|
||||
void publishProfile(Map<String, dynamic> profileData) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderProfile.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(profileData));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!,
|
||||
retain: true);
|
||||
debugPrint('[MQTT] Published profile to $topic');
|
||||
}
|
||||
|
||||
void publishLocation(Map<String, dynamic> locationData) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderLocation.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(locationData));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
|
||||
}
|
||||
|
||||
void publishTelemetry(Map<String, dynamic> telemetryData) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderTelemetry.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(telemetryData));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
debugPrint('[MQTT] Published telemetry data.');
|
||||
}
|
||||
|
||||
void publishLog(String eventName, Map<String, dynamic> data) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
'${MqttConstants.topicRiderLogs.replaceAll('{riderId}', _currentRiderId!)}/$eventName';
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(data));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
debugPrint('[MQTT] Published log: $eventName');
|
||||
}
|
||||
|
||||
void publish(String subTopic, dynamic data) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic = 'nearle/riders/$_currentRiderId/$subTopic';
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
if (data is String) {
|
||||
builder.addString(data);
|
||||
} else {
|
||||
builder.addString(jsonEncode(data));
|
||||
}
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
debugPrint('[MQTT] Published to $topic');
|
||||
}
|
||||
}
|
||||
1
lib/utils/no_internet.json
Normal file
1
lib/utils/no_internet.json
Normal file
File diff suppressed because one or more lines are too long
995
lib/views/Dashboard/Cart/cartpage.dart
Normal file
995
lib/views/Dashboard/Cart/cartpage.dart
Normal file
@@ -0,0 +1,995 @@
|
||||
part of '../deliveries/deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CART PAGE (Active Deliveries)
|
||||
// -------------------------------------------------------------------------
|
||||
class Cartpage extends StatefulWidget {
|
||||
const Cartpage({super.key});
|
||||
|
||||
@override
|
||||
State<Cartpage> createState() => _CartpageState();
|
||||
}
|
||||
|
||||
class _CartpageState extends State<Cartpage> with AutomaticKeepAliveClientMixin {
|
||||
final DeliveryProvider _provider = DeliveryProvider();
|
||||
final CreateDeliveryLogProvider _deliveryLogProvider = CreateDeliveryLogProvider();
|
||||
|
||||
List<Map<String, dynamic>> _activeDeliveries = <Map<String, dynamic>>[];
|
||||
StreamSubscription<void>? _pollerSubscription;
|
||||
bool _fetching = false;
|
||||
final Map<String, Timer> _deliveryTimers = <String, Timer>{};
|
||||
final Map<String, Map<String, dynamic>> _deliveryBasePayload = <String, Map<String, dynamic>>{};
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchActive();
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollerSubscription?.cancel();
|
||||
_stopAllTimers();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startPolling() {
|
||||
_pollerSubscription?.cancel();
|
||||
_pollerSubscription = Stream.periodic(
|
||||
const Duration(seconds: 10),
|
||||
(_) {},
|
||||
).asyncMap((_) async {
|
||||
if (!_fetching && mounted) {
|
||||
await _fetchActive();
|
||||
}
|
||||
}).listen(
|
||||
(_) {},
|
||||
onError: (error) {
|
||||
debugPrint('[CART][STREAM ERROR] $error');
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchActive() async {
|
||||
if (_fetching) return;
|
||||
|
||||
_fetching = true;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
|
||||
if (userId == 0) {
|
||||
debugPrint('[CART] No user ID found');
|
||||
_fetching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current date in YYYY-MM-DD format
|
||||
final now = DateTime.now();
|
||||
final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
|
||||
// Hardcoded API endpoint for cart page only: v2/deliveries/getdeliveries
|
||||
final bool isLive = ApiConstants.mainRoute == 'live';
|
||||
final baseUrl = isLive
|
||||
? 'https://jupiter.nearle.app/live/api/v2/deliveries/getdeliveries'
|
||||
: 'https://jupiter.nearle.app/dev/api/v2/deliveries/getdeliveries';
|
||||
|
||||
final uri = Uri.parse(baseUrl).replace(queryParameters: {
|
||||
'userid': userId.toString(),
|
||||
'fromdate': today,
|
||||
'todate': today,
|
||||
't': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
});
|
||||
|
||||
debugPrint('[CART] Fetching from: $uri');
|
||||
|
||||
// Fetch deliveries from API directly using http
|
||||
// Hardcoded endpoint for cart page: v2/deliveries/getdeliveries
|
||||
final httpClient = http.Client();
|
||||
List<dynamic> items = [];
|
||||
try {
|
||||
final response = await httpClient.get(uri);
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
final decoded = json.decode(response.body);
|
||||
final data = decoded is Map<String, dynamic>
|
||||
? (decoded['details'] ?? decoded['data'] ?? decoded)
|
||||
: decoded;
|
||||
|
||||
items = data is List
|
||||
? data
|
||||
: (data is Map && data['items'] is List ? data['items'] as List : []);
|
||||
} else {
|
||||
debugPrint('[CART] API error: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CART] Error fetching from API: $e');
|
||||
} finally {
|
||||
httpClient.close();
|
||||
}
|
||||
|
||||
debugPrint('[CART] Raw API items count: ${items.length}');
|
||||
|
||||
// Debug: Print all order statuses to see what we're getting
|
||||
for (final item in items) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
final orderId = (item['orderid'] ?? '').toString();
|
||||
final status = (item['orderstatus'] ?? '').toString();
|
||||
debugPrint('[CART] Order $orderId has status: "$status" (raw: ${item['orderstatus']})');
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for ACTIVE status only
|
||||
final activeOrders = items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.where((order) {
|
||||
final status = (order['orderstatus']?.toString().toLowerCase() ?? '').trim();
|
||||
final isActive = status == 'active';
|
||||
if (isActive) {
|
||||
debugPrint('[CART] ✅ Found active order: ${order['orderid']}');
|
||||
} else {
|
||||
debugPrint('[CART] ❌ Order ${order['orderid']} has status: "$status" (not active)');
|
||||
}
|
||||
return isActive;
|
||||
})
|
||||
.toList();
|
||||
|
||||
debugPrint('[CART] Found ${activeOrders.length} active deliveries out of ${items.length} total');
|
||||
|
||||
// Sort by order ID or step number if available
|
||||
activeOrders.sort((a, b) {
|
||||
final stepA = (a['step'] ?? a['Step'] ?? 0).toString();
|
||||
final stepB = (b['step'] ?? b['Step'] ?? 0).toString();
|
||||
final stepAInt = int.tryParse(stepA) ?? 0;
|
||||
final stepBInt = int.tryParse(stepB) ?? 0;
|
||||
if (stepAInt != stepBInt) return stepAInt.compareTo(stepBInt);
|
||||
|
||||
final orderIdA = (a['orderid'] ?? '').toString();
|
||||
final orderIdB = (b['orderid'] ?? '').toString();
|
||||
return orderIdA.compareTo(orderIdB);
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_activeDeliveries = activeOrders;
|
||||
});
|
||||
}
|
||||
|
||||
// Get all active order IDs
|
||||
final activeOrderIds = activeOrders
|
||||
.map((o) => (o['orderid'] ?? '').toString())
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// ✅ CRITICAL: Start/restart timers for ALL active deliveries
|
||||
// This ensures every active delivery posts logs every 30 seconds
|
||||
for (final order in activeOrders) {
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
if (orderId.isEmpty) {
|
||||
debugPrint('[CART] ⚠️ Skipping order with empty orderId');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always restart timer to ensure it's running (handles edge cases)
|
||||
if (_deliveryTimers.containsKey(orderId)) {
|
||||
debugPrint('[CART] 🔄 Restarting timer for active delivery: $orderId');
|
||||
_deliveryTimers[orderId]?.cancel();
|
||||
_deliveryTimers.remove(orderId);
|
||||
// Also clear base payload to force reload
|
||||
_deliveryBasePayload.remove(orderId);
|
||||
}
|
||||
|
||||
debugPrint('[CART] ▶️ Starting timer for active delivery: $orderId');
|
||||
await _startDeliveryPosting(order);
|
||||
debugPrint('[CART] ✅ Timer started successfully for: $orderId');
|
||||
}
|
||||
|
||||
// ✅ Stop timers for deliveries that are no longer active
|
||||
final timersToStop = _deliveryTimers.keys
|
||||
.where((id) => !activeOrderIds.contains(id))
|
||||
.toList();
|
||||
|
||||
for (final id in timersToStop) {
|
||||
debugPrint('[CART] Stopping timer for orderId: $id (no longer active)');
|
||||
_stopDeliveryPosting(id);
|
||||
}
|
||||
|
||||
debugPrint('[CART] Active timers: ${_deliveryTimers.keys.toList()}');
|
||||
} catch (e) {
|
||||
debugPrint('[CART] Error fetching active deliveries: $e');
|
||||
} finally {
|
||||
_fetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startDeliveryPosting(Map<String, dynamic> order) async {
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
if (orderId.isEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Cannot start timer: empty orderId');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety check: If timer already exists, cancel it first (shouldn't happen after cleanup above)
|
||||
if (_deliveryTimers.containsKey(orderId)) {
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Timer already exists for $orderId, canceling old one');
|
||||
_deliveryTimers[orderId]?.cancel();
|
||||
_deliveryTimers.remove(orderId);
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG] 🚀 Starting 30-second timer for orderId: $orderId');
|
||||
|
||||
// Get starttime from SharedPreferences (saved when order became active via updateActiveStatus)
|
||||
// If not found, use activetime from order data, or current time as fallback
|
||||
String startTime = '';
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final deliveryId = (order['deliveryid'] ?? 0).toString();
|
||||
|
||||
// Method 1: Get from SharedPreferences (saved when order became active)
|
||||
startTime = prefs.getString('delivery_starttime_$deliveryId') ?? '';
|
||||
if (startTime.isNotEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from SharedPreferences: $startTime');
|
||||
}
|
||||
|
||||
// Method 2: Fallback - try to get from order data (starttime field)
|
||||
if (startTime.isEmpty) {
|
||||
startTime = (order['starttime'] ?? order['startTime'] ?? '').toString();
|
||||
if (startTime.isNotEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from order data: $startTime');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Fallback - try activetime from order data
|
||||
if (startTime.isEmpty) {
|
||||
final activetime = (order['activetime'] ?? order['activTime'] ?? '').toString();
|
||||
if (activetime.isNotEmpty) {
|
||||
startTime = activetime;
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Loaded starttime from activetime: $startTime');
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Last fallback - current time (shouldn't happen if updateActiveStatus was called)
|
||||
if (startTime.isEmpty) {
|
||||
final now = DateTime.now();
|
||||
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Using current time as starttime fallback: $startTime');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CART][DELIVERYLOG] ❌ Error getting starttime: $e');
|
||||
// Set a fallback starttime even on error
|
||||
final now = DateTime.now();
|
||||
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// CRITICAL: Ensure starttime is never empty
|
||||
if (startTime.isEmpty) {
|
||||
final now = DateTime.now();
|
||||
startTime = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
debugPrint('[CART][DELIVERYLOG] ⚠️ Final fallback: starttime was empty, using: $startTime');
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG] 📝 Final starttime for orderId $orderId: $startTime');
|
||||
|
||||
// Create base payload with starttime
|
||||
final base = <String, dynamic>{
|
||||
'logid': 0,
|
||||
'tenantid': order['tenantid'] ?? 0,
|
||||
'partnerid': order['partnerid'] ?? 0,
|
||||
'locationid': order['locationid'] ?? 0,
|
||||
'orderheaderid': order['orderheaderid'] ?? 0,
|
||||
'deliveryid': order['deliveryid'] ?? 0,
|
||||
'userid': order['userid'] ?? 0,
|
||||
'orderid': orderId,
|
||||
'orderstatus': 'active',
|
||||
'starttime': startTime, // Include starttime in base payload
|
||||
};
|
||||
|
||||
_deliveryBasePayload[orderId] = base;
|
||||
|
||||
// Save to SharedPreferences for persistence
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('deliverylog_${orderId}_tenantid', (base['tenantid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_partnerid', (base['partnerid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_locationid', (base['locationid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_orderheaderid', (base['orderheaderid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_deliveryid', (base['deliveryid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_userid', (base['userid'] ?? 0).toString());
|
||||
await prefs.setString('deliverylog_${orderId}_orderid', orderId);
|
||||
await prefs.setString('deliverylog_${orderId}_orderstatus', 'active');
|
||||
await prefs.setString('deliverylog_${orderId}_starttime', startTime); // Save starttime
|
||||
} catch (e) {
|
||||
debugPrint('[CART][DELIVERYLOG] Error saving payload: $e');
|
||||
}
|
||||
|
||||
// Post once immediately (don't await - let it run in background)
|
||||
_postDeliveryLog(orderId, order);
|
||||
debugPrint('[CART][DELIVERYLOG] 📤 Posted initial log for orderId: $orderId');
|
||||
|
||||
// Then every 30 seconds - CRITICAL: This ensures logs are posted every 30 seconds
|
||||
final timer = Timer.periodic(const Duration(seconds: 30), (t) {
|
||||
debugPrint('[CART][DELIVERYLOG] ⏰ Timer tick for orderId: $orderId (30 seconds elapsed)');
|
||||
_postDeliveryLog(orderId, order);
|
||||
});
|
||||
|
||||
_deliveryTimers[orderId] = timer;
|
||||
debugPrint('[CART][DELIVERYLOG] ✅ Timer registered for orderId: $orderId (will post every 30 seconds)');
|
||||
}
|
||||
|
||||
void _postDeliveryLog(String orderId, Map<String, dynamic> order) {
|
||||
if (!mounted) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Widget disposed, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⏰ Posting log for orderId: $orderId at ${DateTime.now()}');
|
||||
|
||||
// Use Future.microtask to ensure the async operation runs independently
|
||||
Future.microtask(() => _performPost(orderId));
|
||||
}
|
||||
|
||||
Future<void> _performPost(String orderId) async {
|
||||
try {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] 🔄 Starting _performPost for orderId: $orderId');
|
||||
|
||||
Map<String, dynamic>? base = _deliveryBasePayload[orderId];
|
||||
|
||||
if (base == null) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Base is null, loading from SharedPreferences');
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!mounted) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after prefs load');
|
||||
return;
|
||||
}
|
||||
|
||||
base = {
|
||||
'logid': 0,
|
||||
'tenantid': int.tryParse(prefs.getString('deliverylog_${orderId}_tenantid') ?? '0') ?? 0,
|
||||
'partnerid': int.tryParse(prefs.getString('deliverylog_${orderId}_partnerid') ?? '0') ?? 0,
|
||||
'locationid': int.tryParse(prefs.getString('deliverylog_${orderId}_locationid') ?? '0') ?? 0,
|
||||
'orderheaderid': int.tryParse(prefs.getString('deliverylog_${orderId}_orderheaderid') ?? '0') ?? 0,
|
||||
'deliveryid': int.tryParse(prefs.getString('deliverylog_${orderId}_deliveryid') ?? '0') ?? 0,
|
||||
'userid': int.tryParse(prefs.getString('deliverylog_${orderId}_userid') ?? '0') ?? 0,
|
||||
'orderid': prefs.getString('deliverylog_${orderId}_orderid') ?? orderId,
|
||||
'orderstatus': prefs.getString('deliverylog_${orderId}_orderstatus') ?? 'active',
|
||||
'starttime': prefs.getString('deliverylog_${orderId}_starttime') ?? '', // Load starttime
|
||||
};
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Base loaded from prefs: $base');
|
||||
} catch (e) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Error loading base: $e');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, base is guaranteed to be non-null (either from cache or created above)
|
||||
final basePayload = base; // Flow analysis ensures base is non-null here
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Getting coordinates...');
|
||||
|
||||
// CRITICAL: Get coordinates with retry logic - NEVER post with null or '0' coordinates
|
||||
final coords = await _getValidCoordinates().timeout(
|
||||
const Duration(seconds: 10), // Increased timeout to allow retries
|
||||
onTimeout: () {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ Coordinate timeout after retries');
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Widget unmounted after coords');
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Validate coordinates - NEVER post with null, '0', or invalid coordinates
|
||||
if (coords == null || coords.$1.isEmpty || coords.$2.isEmpty ||
|
||||
coords.$1 == '0' || coords.$2 == '0') {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinates (lat=${coords?.$1 ?? 'null'}, lng=${coords?.$2 ?? 'null'})');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
|
||||
return; // Skip this post - don't send invalid coordinates
|
||||
}
|
||||
|
||||
// Validate coordinate ranges
|
||||
final latDouble = double.tryParse(coords.$1) ?? 0.0;
|
||||
final lngDouble = double.tryParse(coords.$2) ?? 0.0;
|
||||
if (latDouble == 0 || lngDouble == 0 ||
|
||||
latDouble.abs() > 90 || lngDouble.abs() > 180) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ SKIPPING POST: Invalid coordinate ranges (lat=$latDouble, lng=$lngDouble)');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ Will retry on next timer tick (30 seconds)');
|
||||
return; // Skip this post - don't send invalid coordinates
|
||||
}
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ✅ Valid coordinates: lat=${coords.$1}, lng=${coords.$2}');
|
||||
|
||||
// Cumulative KM is tracked exclusively by LiveTrackingService (high-frequency, every 3s).
|
||||
// Do not accumulate here to avoid race conditions with concurrent writers.
|
||||
|
||||
final now = DateTime.now();
|
||||
final logdate = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
|
||||
// CRITICAL: Ensure starttime is always included in payload
|
||||
final starttimeValue = basePayload['starttime']?.toString() ?? '';
|
||||
if (starttimeValue.isEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: starttime is empty in basePayload, using fallback');
|
||||
}
|
||||
|
||||
// CRITICAL: Use validated coordinates - guaranteed to be non-null and valid at this point
|
||||
final payload = {
|
||||
...basePayload,
|
||||
'logdate': logdate,
|
||||
'latitude': coords.$1, // Guaranteed non-null and valid
|
||||
'longitude': coords.$2, // Guaranteed non-null and valid
|
||||
'starttime': starttimeValue.isNotEmpty ? starttimeValue : '', // CRITICAL: Always include starttime
|
||||
};
|
||||
|
||||
// Validate payload has all required fields
|
||||
final requiredFields = ['tenantid', 'partnerid', 'locationid', 'orderheaderid', 'deliveryid', 'userid', 'orderid', 'orderstatus', 'starttime'];
|
||||
final missingFields = requiredFields.where((field) => payload[field] == null || payload[field] == '').toList();
|
||||
if (missingFields.isNotEmpty) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ WARNING: Missing fields in payload: $missingFields');
|
||||
}
|
||||
|
||||
final url = ApiConstants.mainRoute == 'live'
|
||||
? ApiConstants.createDeliveryLogLive
|
||||
: ApiConstants.createDeliveryLogDev;
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] 📤 Sending to API: $url');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] 📦 Payload: $payload');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ✅ starttime in payload: "${payload['starttime']}"');
|
||||
|
||||
await _deliveryLogProvider
|
||||
.createDeliveryLog(url, payload)
|
||||
.timeout(
|
||||
const Duration(seconds: 8),
|
||||
onTimeout: () {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ⚠️ API timeout for orderId: $orderId');
|
||||
throw TimeoutException('API timeout', const Duration(seconds: 8));
|
||||
},
|
||||
);
|
||||
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ✅ SUCCESS for orderId: $orderId at ${DateTime.now()}');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('[CART][DELIVERYLOG][POST] ❌ ERROR for orderId: $orderId - $e');
|
||||
debugPrint('[CART][DELIVERYLOG][POST] Stack trace: $stackTrace');
|
||||
}
|
||||
}
|
||||
|
||||
Future<(String lat, String lng)?> _getValidCoordinates({int retryCount = 0}) async {
|
||||
const maxRetries = 3;
|
||||
|
||||
try {
|
||||
final bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[CART][COORDS] Location service disabled, trying last known position');
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
|
||||
final lat = lastPos.latitude.toString();
|
||||
final lng = lastPos.longitude.toString();
|
||||
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
|
||||
return (lat, lng);
|
||||
}
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever ||
|
||||
permission == LocationPermission.denied) {
|
||||
debugPrint('[CART][COORDS] Permission denied, trying last known position');
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && lastPos.latitude != 0 && lastPos.longitude != 0) {
|
||||
final lat = lastPos.latitude.toString();
|
||||
final lng = lastPos.longitude.toString();
|
||||
debugPrint('[CART][COORDS] ✅ Using last known position: $lat, $lng');
|
||||
return (lat, lng);
|
||||
}
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Position? position;
|
||||
try {
|
||||
// Try to get current position with higher accuracy
|
||||
position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high, // Changed to high for better accuracy
|
||||
timeLimit: Duration(seconds: 8), // Increased timeout
|
||||
),
|
||||
).timeout(const Duration(seconds: 8));
|
||||
} catch (e) {
|
||||
debugPrint('[CART][COORDS] Timeout getting current position: $e, trying last known');
|
||||
position = await Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
if (position != null && position.latitude != 0 && position.longitude != 0) {
|
||||
final lat = position.latitude.toString();
|
||||
final lng = position.longitude.toString();
|
||||
|
||||
// Validate coordinates are within valid GPS ranges
|
||||
final latDouble = double.tryParse(lat) ?? 0.0;
|
||||
final lngDouble = double.tryParse(lng) ?? 0.0;
|
||||
if (latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('last_lat', lat);
|
||||
await prefs.setString('last_lng', lng);
|
||||
} catch (_) {}
|
||||
debugPrint('[CART][COORDS] ✅ Got valid coordinates: $lat, $lng');
|
||||
return (lat, lng);
|
||||
} else {
|
||||
debugPrint('[CART][COORDS] ⚠️ Invalid coordinate ranges: $lat, $lng');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to SharedPreferences cached coordinates
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lat = (prefs.getString('last_lat') ?? '').trim();
|
||||
final lng = (prefs.getString('last_lng') ?? '').trim();
|
||||
if (lat.isNotEmpty && lng.isNotEmpty && lat != '0' && lng != '0') {
|
||||
final latDouble = double.tryParse(lat) ?? 0.0;
|
||||
final lngDouble = double.tryParse(lng) ?? 0.0;
|
||||
if (latDouble != 0 && lngDouble != 0 && latDouble.abs() <= 90 && lngDouble.abs() <= 180) {
|
||||
debugPrint('[CART][COORDS] ✅ Using cached coordinates: $lat, $lng');
|
||||
return (lat, lng);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
debugPrint('[CART][COORDS] ⚠️ Retry ${retryCount + 1}/$maxRetries to get coordinates');
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
|
||||
debugPrint('[CART][COORDS] ❌ Failed to get valid coordinates after $maxRetries retries');
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('[CART][COORDS] ❌ Error getting coordinates: $e');
|
||||
// Retry if we haven't exceeded max retries
|
||||
if (retryCount < maxRetries) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
return _getValidCoordinates(retryCount: retryCount + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _stopDeliveryPosting(String orderId) {
|
||||
_deliveryTimers[orderId]?.cancel();
|
||||
_deliveryTimers.remove(orderId);
|
||||
_deliveryBasePayload.remove(orderId);
|
||||
debugPrint('[CART][DELIVERYLOG] Stopped timer for orderId: $orderId');
|
||||
}
|
||||
|
||||
void _stopAllTimers() {
|
||||
for (final timer in _deliveryTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_deliveryTimers.clear();
|
||||
_deliveryBasePayload.clear();
|
||||
debugPrint('[CART][DELIVERYLOG] Stopped all timers');
|
||||
}
|
||||
|
||||
// Method to stop delivery posting for a specific order (called when order is completed)
|
||||
void stopDeliveryPostingForOrder(String orderId) {
|
||||
_stopDeliveryPosting(orderId);
|
||||
// Refresh the list to remove completed orders
|
||||
if (mounted) {
|
||||
_fetchActive();
|
||||
}
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const double R = 6371.0;
|
||||
final double dLat = _toRadians(lat2 - lat1);
|
||||
final double dLon = _toRadians(lon2 - lon1);
|
||||
final double a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(_toRadians(lat1)) *
|
||||
math.cos(_toRadians(lat2)) *
|
||||
math.sin(dLon / 2) *
|
||||
math.sin(dLon / 2);
|
||||
final double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
double _toRadians(double degrees) {
|
||||
return degrees * math.pi / 180.0;
|
||||
}
|
||||
|
||||
String _distanceKmDisplay(Map<String, dynamic> m) {
|
||||
// Try API distance first
|
||||
final String apiKmsStr = (m['actualkms'] ?? m['distance'] ?? '0').toString();
|
||||
final double apiKms = double.tryParse(apiKmsStr) ?? 0.0;
|
||||
if (apiKms > 0) {
|
||||
return apiKms < 10 ? apiKmsStr : apiKms.toStringAsFixed(0);
|
||||
}
|
||||
|
||||
// Try rider location to delivery location
|
||||
final double rLat = _parseD(m['riderslat']);
|
||||
final double rLon = _parseD(m['riderslon']);
|
||||
final double dLat = _parseD(m['droplat'] ?? m['deliverylat']);
|
||||
final double dLon = _parseD(m['droplon'] ?? m['deliverylong']);
|
||||
|
||||
if (rLat != 0 && rLon != 0 && dLat != 0 && dLon != 0) {
|
||||
final double km = _haversineKm(rLat, rLon, dLat, dLon);
|
||||
return km.toStringAsFixed(km < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Fallback: pickup to delivery
|
||||
final double pLat = _parseD(m['pickuplat']);
|
||||
final double pLon = _parseD(m['pickuplon']);
|
||||
if (pLat != 0 && pLon != 0 && dLat != 0 && dLon != 0) {
|
||||
final double km = _haversineKm(pLat, pLon, dLat, dLon);
|
||||
return km.toStringAsFixed(km < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
toolbarHeight: 70,
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
"Active Deliveries",
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: const PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(height: 1, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: _activeDeliveries.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -11),
|
||||
child: Image.asset(
|
||||
"assets/images/Nearle Bike.png",
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.delivery_dining,
|
||||
size: 120,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -11),
|
||||
child: Text(
|
||||
"No Active Deliveries",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _fetchActive,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
itemCount: _activeDeliveries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _activeDeliveries[index];
|
||||
final customerName = (item['deliverycustomer'] ?? item['customer'] ?? 'Customer').toString();
|
||||
final address = (item['deliveryaddress'] ?? item['address'] ?? 'Address not available').toString();
|
||||
final storeName = (item['pickupcustomer'] ?? item['store'] ?? 'Store').toString();
|
||||
final orderId = (item['orderid'] ?? '').toString();
|
||||
final distance = _distanceKmDisplay(item);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// -------------------------------------------
|
||||
// TOP CUSTOMER DETAILS
|
||||
// -------------------------------------------
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 30,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -3),
|
||||
child: Text(
|
||||
customerName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 8),
|
||||
child: Text(
|
||||
address,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Distance: $distance km',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.blueGrey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
final phone =
|
||||
(item['deliverycontactno'] ?? '').toString();
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/phone-call .png',
|
||||
height: 27,
|
||||
width: 27,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.phone, size: 27, color: Colors.green),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
// -------------------------------------------
|
||||
// STORE DETAILS
|
||||
// -------------------------------------------
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/shoppingbag.png',
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.shopping_bag,
|
||||
size: 32,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
storeName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
_showMyOptionsSheet(
|
||||
context,
|
||||
item,
|
||||
null, // No parent state for cart page
|
||||
);
|
||||
// Refresh cart page after skip (order will no longer be active)
|
||||
if (mounted) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_fetchActive();
|
||||
}
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -5),
|
||||
child: Text(
|
||||
'Skip>>',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"Order ID: #$orderId",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
// -------------------------------------------
|
||||
// SLIDER BUTTON
|
||||
// -------------------------------------------
|
||||
SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: 50,
|
||||
buttonSize: 45,
|
||||
width: MediaQuery.of(context).size.width - 56,
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
dismissThresholds: 0.90,
|
||||
action: () async {
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
if (!context.mounted) return false;
|
||||
|
||||
// Navigate to delivery map screen (same as deliveries page)
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _DeliveryMapScreen(
|
||||
delivery: item,
|
||||
parentState: null, // No parent state for cart page
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Refresh cart page when returning from map screen
|
||||
// (in case delivery was completed/cancelled)
|
||||
if (mounted) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_fetchActive();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
label: const Text(
|
||||
'Slide to start Delivery',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
329
lib/views/Dashboard/deliveries/card.dart
Normal file
329
lib/views/Dashboard/deliveries/card.dart
Normal file
@@ -0,0 +1,329 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELIVERY CARD
|
||||
// -------------------------------------------------------------------------
|
||||
class DeliveryCard extends StatelessWidget {
|
||||
final Map<String, dynamic> item;
|
||||
final int displayStep;
|
||||
final String distanceStr;
|
||||
final bool enabled;
|
||||
final bool isSkipped;
|
||||
|
||||
const DeliveryCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.displayStep,
|
||||
required this.distanceStr,
|
||||
this.enabled = true,
|
||||
this.isSkipped = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String customerName = (item['deliverycustomer'] ?? 'Customer')
|
||||
.toString();
|
||||
final String address = (item['deliveryaddress'] ?? 'Address not available')
|
||||
.toString();
|
||||
final String tenantName = (item['tenantname'] ?? 'Store').toString();
|
||||
final String orderId = (item['orderid'] ?? '').toString();
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isSkipped ? Border.all(color: Colors.orange, width: 2) : null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isSkipped)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'SKIPPED',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSkipped) const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.orange,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 2,
|
||||
height: 30,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -3),
|
||||
child: Text(
|
||||
customerName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17.5,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 8),
|
||||
child: Text(
|
||||
address,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Distance: $distanceStr km',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
color: Colors.blueGrey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
final phone = (item['deliverycontactno'] ?? '').toString();
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/phone-call .png',
|
||||
height: 27,
|
||||
width: 27,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.phone, size: 27, color: Colors.green),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/shoppingbag.png',
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.shopping_bag,
|
||||
size: 32,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
tenantName,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (!isSkipped)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final parentState = context
|
||||
.findAncestorStateOfType<
|
||||
_MyDeliveriesState
|
||||
>();
|
||||
if (parentState != null) {
|
||||
_showMyOptionsSheet(
|
||||
context,
|
||||
item,
|
||||
parentState,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -5),
|
||||
child: Text(
|
||||
'Skip>>',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'Order ID: #$orderId',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: 50,
|
||||
buttonSize: 45,
|
||||
width: MediaQuery.of(context).size.width - 56,
|
||||
backgroundColor: enabled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade400,
|
||||
dismissThresholds: 0.90,
|
||||
action: enabled
|
||||
? () async {
|
||||
// Reduce delay to make it feel snappier
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (!context.mounted) return false;
|
||||
final parentState = context
|
||||
.findAncestorStateOfType<_MyDeliveriesState>();
|
||||
|
||||
// ✅ BLOCK: Check if there's already an active delivery (and this isn't it)
|
||||
if (parentState != null) {
|
||||
final currentOrderId = (item['orderid'] ?? '')
|
||||
.toString();
|
||||
final activeOrderIds = parentState._activeDeliveries
|
||||
.map((d) => (d['orderid'] ?? '').toString())
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// Block if there's a different active delivery: just ignore the swipe
|
||||
if (parentState._activeDeliveries.isNotEmpty &&
|
||||
!activeOrderIds.contains(currentOrderId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to delivery map screen
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _DeliveryMapScreen(
|
||||
delivery: item,
|
||||
parentState: parentState,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
: () async => null,
|
||||
label: Transform.translate(
|
||||
offset: const Offset(-0.5, 0),
|
||||
child: Text(
|
||||
isSkipped
|
||||
? 'Slide to resume Delivery'
|
||||
: (enabled
|
||||
? 'Slide to start Delivery'
|
||||
: 'Complete previous delivery'),
|
||||
style: const TextStyle(
|
||||
fontSize: 18.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$displayStep',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: enabled
|
||||
? const ui.Color.fromARGB(255, 0, 0, 0)
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1762
lib/views/Dashboard/deliveries/deliveries.dart
Normal file
1762
lib/views/Dashboard/deliveries/deliveries.dart
Normal file
File diff suppressed because it is too large
Load Diff
278
lib/views/Dashboard/deliveries/done.dart
Normal file
278
lib/views/Dashboard/deliveries/done.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELIVERIES DONE SCREEN
|
||||
// -------------------------------------------------------------------------
|
||||
class DeliveriesDone extends StatefulWidget {
|
||||
final bool isCancelled;
|
||||
final int bonusPoints;
|
||||
|
||||
const DeliveriesDone({
|
||||
super.key,
|
||||
this.isCancelled = false,
|
||||
this.bonusPoints = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DeliveriesDone> createState() => _DeliveriesDoneState();
|
||||
}
|
||||
|
||||
class _DeliveriesDoneState extends State<DeliveriesDone> {
|
||||
late ConfettiController _confettiController;
|
||||
final GlobalKey<ScratcherState> _scratcherKey = GlobalKey<ScratcherState>();
|
||||
double _opacity = 0.0;
|
||||
bool _isScratched = false; // Track scratch state
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_confettiController = ConfettiController(
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Check if we should show the scratch card
|
||||
final bool showScratchCard = !widget.isCancelled && widget.bonusPoints > 0;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Main Content
|
||||
SafeArea(
|
||||
child: showScratchCard
|
||||
? _buildScratchCardContent()
|
||||
: _buildStandardContent(),
|
||||
),
|
||||
|
||||
// Confetti Layer (on top)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: false,
|
||||
colors: const [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.pink,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
],
|
||||
createParticlePath: drawStar,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStandardContent() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Lottie.asset(
|
||||
widget.isCancelled
|
||||
? 'assets/lotties/Error Occurred!.json'
|
||||
: 'assets/lotties/result page succes.json',
|
||||
height: 220,
|
||||
repeat: false,
|
||||
errorBuilder:
|
||||
(c, e, s) => Icon(
|
||||
widget.isCancelled ? Icons.cancel : Icons.check_circle,
|
||||
size: 120,
|
||||
color: widget.isCancelled ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
widget.isCancelled ? 'Delivery Cancelled' : 'Delivery Completed!',
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.isCancelled
|
||||
? 'This order was cancelled.'
|
||||
: 'Great job! Your delivery was successful.',
|
||||
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildDoneButton(isEnabled: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScratchCardContent() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
Text(
|
||||
'You won a Scratch Card!',
|
||||
style: TextStyle(
|
||||
fontSize: 28, // Increased
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Scratch to reveal your bonus points',
|
||||
style: TextStyle(
|
||||
fontSize: 18, // Increased
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Scratcher(
|
||||
key: _scratcherKey,
|
||||
brushSize: 50,
|
||||
threshold: 50,
|
||||
color: ColorConstants.primaryColor,
|
||||
onChange: (value) {
|
||||
// Optional: haptic feedback or sound while scratching
|
||||
},
|
||||
onThreshold: () {
|
||||
_confettiController.play();
|
||||
setState(() {
|
||||
_opacity = 1.0;
|
||||
_isScratched = true; // Enable button
|
||||
});
|
||||
},
|
||||
// Custom cover content instead of just solid color
|
||||
image: Image.asset(
|
||||
'assets/images/nearlelauncher.png',
|
||||
fit: BoxFit.scaleDown,
|
||||
width: 100, // Constrain width so it fits nicely
|
||||
height: 100,
|
||||
),
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.monetization_on,
|
||||
size: 80,
|
||||
color: Colors.amber,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'${widget.bonusPoints}',
|
||||
style: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Points',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildDoneButton(isEnabled: _isScratched),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDoneButton({required bool isEnabled}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isEnabled ? ColorConstants.primaryColor : Colors.grey,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: isEnabled ? () {
|
||||
Get.offAll(() => const BottomPage(initialIndex: 1));
|
||||
} : null,
|
||||
child: const Text(
|
||||
'Done',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Path drawStar(Size size) {
|
||||
// Method to draw star shape for confetti
|
||||
double degToRad(double deg) => deg * (math.pi / 180.0);
|
||||
|
||||
const numberOfPoints = 5;
|
||||
final halfWidth = size.width / 2;
|
||||
final externalRadius = halfWidth;
|
||||
final internalRadius = halfWidth / 2.5;
|
||||
final degreesPerStep = degToRad(360 / numberOfPoints);
|
||||
final halfDegreesPerStep = degreesPerStep / 2;
|
||||
final path = Path();
|
||||
final fullAngle = degToRad(360);
|
||||
path.moveTo(size.width, halfWidth);
|
||||
|
||||
for (double step = 0; step < fullAngle; step += degreesPerStep) {
|
||||
path.lineTo(
|
||||
halfWidth + externalRadius * math.cos(step),
|
||||
halfWidth + externalRadius * math.sin(step),
|
||||
);
|
||||
path.lineTo(
|
||||
halfWidth + internalRadius * math.cos(step + halfDegreesPerStep),
|
||||
halfWidth + internalRadius * math.sin(step + halfDegreesPerStep),
|
||||
);
|
||||
}
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
}
|
||||
831
lib/views/Dashboard/deliveries/map.dart
Normal file
831
lib/views/Dashboard/deliveries/map.dart
Normal file
@@ -0,0 +1,831 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SCREEN 1: DELIVERY MAP PREVIEW (Shows route, has "Start" button)
|
||||
// -------------------------------------------------------------------------
|
||||
class _DeliveryMapScreen extends StatefulWidget {
|
||||
final Map<String, dynamic> delivery;
|
||||
final _MyDeliveriesState? parentState;
|
||||
const _DeliveryMapScreen({
|
||||
required this.delivery,
|
||||
this.parentState,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_DeliveryMapScreen> createState() => _DeliveryMapScreenState();
|
||||
}
|
||||
|
||||
class _DeliveryMapScreenState extends State<_DeliveryMapScreen> {
|
||||
GoogleMapController? _mapController;
|
||||
late final LatLng _pickupLocation;
|
||||
late final LatLng _dropLocation;
|
||||
final Set<Marker> _markers = {};
|
||||
final Set<Polyline> _polylines = {};
|
||||
late final PolylinePoints _polylinePoints;
|
||||
bool _isLoadingRoute = true;
|
||||
bool _isNavigating = false; // Prevent multiple clicks
|
||||
|
||||
static const String _googleApiKey = 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_polylinePoints = PolylinePoints(apiKey: _googleApiKey);
|
||||
_resolveLocationsFromDelivery();
|
||||
_setMarkers();
|
||||
_isLoadingRoute = false;
|
||||
_createRealRoute();
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
void _resolveLocationsFromDelivery() {
|
||||
final d = widget.delivery;
|
||||
final double pickLat = _parseD(d['pickuplat'] ?? d['PickupLat']);
|
||||
final double pickLon = _parseD(d['pickuplon'] ?? d['PickupLon']);
|
||||
final double dropLat = _parseD(
|
||||
d['droplat'] ?? d['DropLat'] ?? d['deliverylat'],
|
||||
);
|
||||
final double dropLon = _parseD(
|
||||
d['droplon'] ?? d['DropLon'] ?? d['deliverylong'],
|
||||
);
|
||||
final double riderLat = _parseD(d['riderslat']);
|
||||
final double riderLon = _parseD(d['riderslon']);
|
||||
|
||||
final bool hasPickup = pickLat != 0 && pickLon != 0;
|
||||
final bool hasDrop = dropLat != 0 && dropLon != 0;
|
||||
|
||||
final LatLng pickup = hasPickup
|
||||
? LatLng(pickLat, pickLon)
|
||||
: (riderLat != 0 && riderLon != 0
|
||||
? LatLng(riderLat, riderLon)
|
||||
: const LatLng(10.998356, 76.977596));
|
||||
|
||||
final LatLng drop = hasDrop
|
||||
? LatLng(dropLat, dropLon)
|
||||
: const LatLng(11.004556, 76.967696);
|
||||
|
||||
_pickupLocation = pickup;
|
||||
_dropLocation = drop;
|
||||
}
|
||||
|
||||
void _setMarkers() {
|
||||
_markers.addAll([
|
||||
Marker(
|
||||
markerId: const MarkerId('pickup'),
|
||||
position: _pickupLocation,
|
||||
infoWindow: const InfoWindow(title: 'Pickup Location'),
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed),
|
||||
),
|
||||
Marker(
|
||||
markerId: const MarkerId('drop'),
|
||||
position: _dropLocation,
|
||||
infoWindow: const InfoWindow(title: 'Drop Location'),
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _createRealRoute() async {
|
||||
try {
|
||||
final request = PolylineRequest(
|
||||
origin: PointLatLng(
|
||||
_pickupLocation.latitude,
|
||||
_pickupLocation.longitude,
|
||||
),
|
||||
destination: PointLatLng(
|
||||
_dropLocation.latitude,
|
||||
_dropLocation.longitude,
|
||||
),
|
||||
mode: TravelMode.driving,
|
||||
);
|
||||
|
||||
final result = await _polylinePoints.getRouteBetweenCoordinates(
|
||||
request: request,
|
||||
);
|
||||
|
||||
if (result.points.isNotEmpty) {
|
||||
final routePoints = result.points
|
||||
.map((e) => LatLng(e.latitude, e.longitude))
|
||||
.toList();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_polylines.add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('real_route'),
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 6,
|
||||
points: routePoints,
|
||||
),
|
||||
);
|
||||
_isLoadingRoute = false;
|
||||
});
|
||||
|
||||
_fitMapToRoute();
|
||||
}
|
||||
} else {
|
||||
debugPrint('[MAP_PREVIEW] No route found');
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingRoute = false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[MAP_PREVIEW] Error creating route: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingRoute = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<void> _fitMapToRoute() async {
|
||||
if (!mounted) return;
|
||||
if (_mapController == null) return;
|
||||
|
||||
// Check if controller is still alive (important!)
|
||||
try {
|
||||
await _mapController!.getVisibleRegion();
|
||||
} catch (e) {
|
||||
debugPrint("❌ Map controller is dead. Skip animateCamera.");
|
||||
return;
|
||||
}
|
||||
|
||||
final bounds = LatLngBounds(
|
||||
southwest: LatLng(
|
||||
math.min(_pickupLocation.latitude, _dropLocation.latitude),
|
||||
math.min(_pickupLocation.longitude, _dropLocation.longitude),
|
||||
),
|
||||
northeast: LatLng(
|
||||
math.max(_pickupLocation.latitude, _dropLocation.latitude),
|
||||
math.max(_pickupLocation.longitude, _dropLocation.longitude),
|
||||
),
|
||||
);
|
||||
|
||||
// Try animate safely
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
await _mapController!.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 80),
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
await Future.delayed(const Duration(milliseconds: 150));
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint("❌ animateCamera failed after retries (map probably disposed)");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
SizedBox.expand(
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: _pickupLocation,
|
||||
zoom: 14.5,
|
||||
),
|
||||
onMapCreated: (controller) {
|
||||
_mapController = controller;
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!_isLoadingRoute && _polylines.isNotEmpty) {
|
||||
_fitMapToRoute();
|
||||
}
|
||||
});
|
||||
},
|
||||
markers: _markers,
|
||||
polylines: _polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 16,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final double maxSheetHeight =
|
||||
MediaQuery.of(ctx).size.height * 0.35;
|
||||
// ignore: unused_local_variable
|
||||
final double allowedHeight = math.min(
|
||||
constraints.maxHeight,
|
||||
maxSheetHeight,
|
||||
);
|
||||
return ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Customer Details',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
// Just launch dialer; PiP is handled only from navigation screen
|
||||
final phone =
|
||||
(widget.delivery['deliverycontactno'] ?? '')
|
||||
.toString();
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/phone-call .png',
|
||||
height: 27,
|
||||
width: 27,
|
||||
errorBuilder: (c, e, s) => const Icon(
|
||||
Icons.phone,
|
||||
size: 27,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 8),
|
||||
_buildCustomerInfo(),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: _isNavigating ? null : () async {
|
||||
// Prevent multiple clicks
|
||||
if (_isNavigating || !mounted || !context.mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isNavigating = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Capture screen size before navigation
|
||||
final screenSize = MediaQuery.of(context).size *
|
||||
MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
final dc = Get.put(
|
||||
DeliveriesController(),
|
||||
permanent: true,
|
||||
);
|
||||
final d = widget.delivery;
|
||||
final int deliveryId =
|
||||
int.tryParse(
|
||||
'${d['deliveryid'] ?? d['DeliveryId'] ?? 0}',
|
||||
) ??
|
||||
0;
|
||||
final String orderId = (d['orderid'] ??
|
||||
d['OrderId'] ??
|
||||
'')
|
||||
.toString();
|
||||
final int orderHeaderId =
|
||||
int.tryParse(
|
||||
'${d['orderheaderid'] ?? d['OrderHeaderId'] ?? 0}',
|
||||
) ??
|
||||
0;
|
||||
// Save ridertime start at the moment navigation is started
|
||||
try {
|
||||
if (deliveryId > 0) {
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
// 1) Save rider time start (existing behaviour)
|
||||
await prefs.setString(
|
||||
'ridertime_start_$deliveryId',
|
||||
DateTime.now().toIso8601String(),
|
||||
);
|
||||
// 2) Save ETA end time for this order so PiP timer can resume correctly
|
||||
final rawEta = d['eta'];
|
||||
int etaMinutes = 0;
|
||||
if (rawEta != null) {
|
||||
etaMinutes =
|
||||
int.tryParse(rawEta.toString()) ?? 0;
|
||||
}
|
||||
if (etaMinutes > 0) {
|
||||
final now = DateTime.now();
|
||||
final endTime = now
|
||||
.add(Duration(minutes: etaMinutes))
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000; // store seconds
|
||||
await prefs.setInt(
|
||||
'eta_endtime_$orderId',
|
||||
endTime,
|
||||
);
|
||||
debugPrint(
|
||||
'[ACTIVE][ETA] Saved eta_endtime_$orderId -> $endTime (eta=$etaMinutes min)',
|
||||
);
|
||||
}
|
||||
debugPrint(
|
||||
'[ACTIVE] Saved ridertime_start for deliveryId=$deliveryId',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error saving ridertime_start: $e',
|
||||
);
|
||||
}
|
||||
|
||||
final parentState =
|
||||
widget.parentState ??
|
||||
context
|
||||
.findAncestorStateOfType<
|
||||
_MyDeliveriesState
|
||||
>();
|
||||
|
||||
// ✅ CRITICAL: Check active delivery BEFORE navigation
|
||||
if (parentState != null) {
|
||||
final currentOrderId = (d['orderid'] ??
|
||||
d['OrderId'] ??
|
||||
'')
|
||||
.toString();
|
||||
final activeOrderIds = parentState
|
||||
._activeDeliveries
|
||||
.map(
|
||||
(del) => (del['orderid'] ??
|
||||
del['OrderId'] ??
|
||||
'')
|
||||
.toString(),
|
||||
)
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
// Block if there's a different active delivery
|
||||
if (parentState._activeDeliveries.isNotEmpty &&
|
||||
!activeOrderIds.contains(currentOrderId)) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Colors.orange.shade400,
|
||||
Colors.red.shade500,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.warning_rounded,
|
||||
color: Colors.white,
|
||||
size: 48,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Title
|
||||
Text(
|
||||
'Active Delivery in Progress',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: FontConstants.xxLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Message
|
||||
Text(
|
||||
'Please complete your active delivery first before starting another delivery.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.95),
|
||||
fontSize: FontConstants.medium(context),
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Action Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
// Navigate to active delivery
|
||||
if (parentState._activeDeliveries.isNotEmpty) {
|
||||
parentState.startDelivery(parentState._activeDeliveries.first);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.red.shade600,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.two_wheeler,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'View Active Delivery',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.regular(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Close Button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Close',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return; // Return early to prevent starting another delivery
|
||||
}
|
||||
}
|
||||
|
||||
if (deliveryId > 0 && orderId.isNotEmpty) {
|
||||
String riderLatStr = '0';
|
||||
String riderLngStr = '0';
|
||||
try {
|
||||
final position =
|
||||
await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.medium,
|
||||
timeLimit: Duration(seconds: 3),
|
||||
),
|
||||
).timeout(const Duration(seconds: 3));
|
||||
riderLatStr = position.latitude
|
||||
.toStringAsFixed(6);
|
||||
riderLngStr = position.longitude
|
||||
.toStringAsFixed(6);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error getting rider location: $e',
|
||||
);
|
||||
try {
|
||||
final lastPos =
|
||||
await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null) {
|
||||
riderLatStr = lastPos.latitude
|
||||
.toStringAsFixed(6);
|
||||
riderLngStr = lastPos.longitude
|
||||
.toStringAsFixed(6);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ✅ CRITICAL: Navigate FIRST, then handle status updates
|
||||
if (!mounted || !context.mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate immediately - this must happen
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _RiderNavigationScreen(
|
||||
delivery: widget.delivery,
|
||||
parentState: widget.parentState,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Reset navigation state after navigation completes
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
|
||||
// Continue with status updates and PiP in background
|
||||
debugPrint(
|
||||
'[ACTIVE] Updating status for deliveryId=$deliveryId orderId=$orderId lat=$riderLatStr lng=$riderLngStr',
|
||||
);
|
||||
|
||||
final ok = await dc.updateActiveStatus(
|
||||
deliveryId: deliveryId,
|
||||
orderHeaderId: orderHeaderId,
|
||||
ridersLat: riderLatStr,
|
||||
ridersLng: riderLngStr,
|
||||
orderId: orderId,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'[ACTIVE] Status update result: $ok',
|
||||
);
|
||||
|
||||
// ✅ CRITICAL: ENFORCE PiP when delivery becomes active (compulsory)
|
||||
if (ok && !dc.isPipEnabled.value) {
|
||||
try {
|
||||
debugPrint('[ACTIVE] Delivery is now active - Enforcing PiP mode');
|
||||
final floating = Floating();
|
||||
const rational = Rational.landscape();
|
||||
|
||||
final height = (screenSize.height * 0.5).toInt();
|
||||
final width = (screenSize.width * 0.9).toInt();
|
||||
|
||||
final arguments = ImmediatePiP(
|
||||
aspectRatio: rational,
|
||||
sourceRectHint: math.Rectangle<int>(
|
||||
((screenSize.width - width) ~/ 2).toInt(),
|
||||
((screenSize.height - height) ~/ 2).toInt(),
|
||||
width,
|
||||
height,
|
||||
),
|
||||
);
|
||||
|
||||
await floating.enable(arguments);
|
||||
dc.isPipEnabled.value = true;
|
||||
debugPrint('[ACTIVE] PiP enabled successfully');
|
||||
|
||||
// Also try method channel as backup
|
||||
try {
|
||||
const channel = MethodChannel('nearle/pip');
|
||||
await channel.invokeMethod<bool>('enterPip');
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
debugPrint('[ACTIVE] Error enabling PiP: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (parentState != null) {
|
||||
final previousActive =
|
||||
parentState._activeDeliveryOrderId;
|
||||
if (previousActive != null &&
|
||||
previousActive != orderId) {
|
||||
parentState._stopDeliveryPosting(
|
||||
previousActive,
|
||||
);
|
||||
}
|
||||
|
||||
parentState._activeDeliveryOrderId = orderId;
|
||||
d['orderstatus'] = 'active';
|
||||
|
||||
final orderKey = parentState._getOrderKey(d);
|
||||
if (parentState._skippedOrdersCache
|
||||
.containsKey(orderKey)) {
|
||||
parentState._skippedOrdersCache.remove(
|
||||
orderKey,
|
||||
);
|
||||
parentState._skippedOrderTimestamps.remove(
|
||||
orderKey,
|
||||
);
|
||||
await parentState._saveSkippedOrdersCache();
|
||||
debugPrint(
|
||||
'[ACTIVE] Removed from skipped cache (resumed): $orderKey',
|
||||
);
|
||||
}
|
||||
|
||||
await parentState._startDeliveryPosting(d);
|
||||
|
||||
try {
|
||||
final prefs =
|
||||
await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
'active_delivery_order_id',
|
||||
orderId,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error saving active delivery ID: $e',
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: invalid_use_of_protected_member
|
||||
parentState.setState(() {});
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[ACTIVE] Invalid deliveryId: $deliveryId or orderId: $orderId',
|
||||
);
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[ACTIVE] Error in navigation flow: $e',
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
child: _isNavigating
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Starting...',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
'Start Navigation',
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerInfo() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
'Name:',
|
||||
(widget.delivery['deliverycustomer'] ?? 'Customer').toString(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Address:',
|
||||
(widget.delivery['deliveryaddress'] ?? 'Address not available')
|
||||
.toString(),
|
||||
isExpanded: true,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Order ID:',
|
||||
'#${(widget.delivery['orderid'] ?? '').toString()}',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_buildInfoRow(
|
||||
'Distance:',
|
||||
'${(widget.delivery['kms'] ?? '0').toString()} km',
|
||||
valueColor: Colors.red,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
String label,
|
||||
String value, {
|
||||
bool isExpanded = false,
|
||||
Color? valueColor,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
isExpanded
|
||||
? Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: valueColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: valueColor ?? Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/views/Dashboard/deliveries/map_btn.dart
Normal file
41
lib/views/Dashboard/deliveries/map_btn.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MAP VIEW BUTTON (Customer locations)
|
||||
// -------------------------------------------------------------------------
|
||||
class MapViewRow extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> deliveries;
|
||||
final Map<String, int>? preservedStepNumbers;
|
||||
|
||||
const MapViewRow({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.preservedStepNumbers,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MultiCustomerMapScreen(
|
||||
deliveries: deliveries,
|
||||
preservedStepNumbers: preservedStepNumbers ?? {},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/customermap.png',
|
||||
color: ColorConstants.primaryColor,
|
||||
height: 32,
|
||||
width: 32,
|
||||
errorBuilder: (c, e, s) =>
|
||||
const Icon(Icons.map, size: 32, color: Colors.blue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
894
lib/views/Dashboard/deliveries/multi_map.dart
Normal file
894
lib/views/Dashboard/deliveries/multi_map.dart
Normal file
@@ -0,0 +1,894 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
class _StepPoint {
|
||||
final int step;
|
||||
final LatLng position;
|
||||
_StepPoint(this.step, this.position);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MULTI CUSTOMER MAP (Shows all deliveries with real step numbers)
|
||||
// -------------------------------------------------------------------------
|
||||
class MultiCustomerMapScreen extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> deliveries;
|
||||
final Map<String, int> preservedStepNumbers;
|
||||
|
||||
const MultiCustomerMapScreen({
|
||||
super.key,
|
||||
required this.deliveries,
|
||||
this.preservedStepNumbers = const {},
|
||||
});
|
||||
|
||||
@override
|
||||
State<MultiCustomerMapScreen> createState() => _MultiCustomerMapScreenState();
|
||||
}
|
||||
|
||||
class _MultiCustomerMapScreenState extends State<MultiCustomerMapScreen> {
|
||||
GoogleMapController? mapController;
|
||||
Set<Marker> markers = {};
|
||||
Set<Polyline> polylines = {};
|
||||
LatLng? currentLocation;
|
||||
bool _isLoading = true;
|
||||
bool _mapReady = false;
|
||||
final PolylinePoints _polylinePoints = PolylinePoints(
|
||||
apiKey: 'AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q',
|
||||
);
|
||||
final Map<String, Map<String, dynamic>> _deliveryMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.deliveries.isNotEmpty) {
|
||||
final firstDelivery = widget.deliveries.first;
|
||||
final dropLat = _parseD(
|
||||
firstDelivery['droplat'] ?? firstDelivery['deliverylat'] ?? 0,
|
||||
);
|
||||
final dropLon = _parseD(
|
||||
firstDelivery['droplon'] ?? firstDelivery['deliverylong'] ?? 0,
|
||||
);
|
||||
if (dropLat != 0 && dropLon != 0) {
|
||||
currentLocation = LatLng(dropLat, dropLon);
|
||||
} else {
|
||||
currentLocation = const LatLng(11.018356, 77.012596);
|
||||
}
|
||||
} else {
|
||||
currentLocation = const LatLng(11.018356, 77.012596);
|
||||
}
|
||||
_loadMapData();
|
||||
}
|
||||
|
||||
double _parseD(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
int _getStepNumber(Map<String, dynamic> order) {
|
||||
final dynamic raw = order['step'] ?? order['Step'];
|
||||
final int step = raw == null
|
||||
? 0
|
||||
: (raw is num ? raw.toInt() : int.tryParse(raw.toString()) ?? 0);
|
||||
return step;
|
||||
}
|
||||
|
||||
String _getOrderKey(Map<String, dynamic> order) {
|
||||
final deliveryId = (order['deliveryid'] ?? '').toString();
|
||||
final orderId = (order['orderid'] ?? '').toString();
|
||||
return deliveryId.isNotEmpty ? 'delivery_$deliveryId' : 'order_$orderId';
|
||||
}
|
||||
|
||||
int _getPreservedOrCurrentStep(Map<String, dynamic> order) {
|
||||
final orderKey = _getOrderKey(order);
|
||||
if (widget.preservedStepNumbers.containsKey(orderKey)) {
|
||||
return widget.preservedStepNumbers[orderKey]!;
|
||||
}
|
||||
final currentStep = _getStepNumber(order);
|
||||
return currentStep;
|
||||
}
|
||||
|
||||
Future<void> _loadMapData() async {
|
||||
try {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
_getCurrentLocation().then((_) {
|
||||
if (mounted) {
|
||||
_createDeliveryMarkers();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Error loading map data: $e');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Position? position;
|
||||
try {
|
||||
position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.medium,
|
||||
distanceFilter: 0,
|
||||
timeLimit: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[CUSTOMER_MAP] Timeout getting location, using last known: $e',
|
||||
);
|
||||
position = await Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
final safePosition = position;
|
||||
if (safePosition != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(
|
||||
safePosition.latitude,
|
||||
safePosition.longitude,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Error getting location: $e');
|
||||
try {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null && mounted) {
|
||||
setState(() {
|
||||
currentLocation = LatLng(lastPos.latitude, lastPos.longitude);
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
int? _getNextDeliveryStep() {
|
||||
// Find the first non-skipped delivery (next delivery from user location)
|
||||
for (int i = 0; i < widget.deliveries.length; i++) {
|
||||
final delivery = widget.deliveries[i];
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
if (status != 'skipped') {
|
||||
final stepNumber = _getPreservedOrCurrentStep(delivery);
|
||||
if (stepNumber > 0) {
|
||||
return stepNumber;
|
||||
} else {
|
||||
// Calculate display step for orders without step
|
||||
final ordersWithStepBefore = widget.deliveries
|
||||
.sublist(0, i)
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
final totalOrdersWithStep = widget.deliveries
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
return totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _createDeliveryMarkers() async {
|
||||
Set<Marker> tempMarkers = {};
|
||||
List<_StepPoint> stepPoints = [];
|
||||
_deliveryMap.clear();
|
||||
|
||||
final nextStep = _getNextDeliveryStep();
|
||||
|
||||
for (int i = 0; i < widget.deliveries.length; i++) {
|
||||
final delivery = widget.deliveries[i];
|
||||
final stepNumber = _getPreservedOrCurrentStep(delivery);
|
||||
|
||||
int displayStep;
|
||||
if (stepNumber > 0) {
|
||||
displayStep = stepNumber;
|
||||
} else {
|
||||
final ordersWithStepBefore = widget.deliveries
|
||||
.sublist(0, i)
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
final totalOrdersWithStep = widget.deliveries
|
||||
.where((o) => _getPreservedOrCurrentStep(o) > 0)
|
||||
.length;
|
||||
displayStep = totalOrdersWithStep + (i - ordersWithStepBefore) + 1;
|
||||
}
|
||||
|
||||
final double lat = _parseD(
|
||||
delivery['droplat'] ?? delivery['deliverylat'],
|
||||
);
|
||||
final double lon = _parseD(
|
||||
delivery['droplon'] ?? delivery['deliverylong'],
|
||||
);
|
||||
if (lat == 0 || lon == 0) continue;
|
||||
|
||||
final customerName = (delivery['deliverycustomer'] ?? 'Customer ${i + 1}')
|
||||
.toString();
|
||||
final orderId = (delivery['orderid'] ?? '').toString();
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isSkipped = status == 'skipped';
|
||||
final isNext = displayStep == nextStep;
|
||||
|
||||
// Store delivery data for dialog
|
||||
_deliveryMap['delivery_$orderId'] = delivery;
|
||||
|
||||
final icon = await _createCircularMarkerBitmap(
|
||||
displayStep,
|
||||
isSkipped: isSkipped,
|
||||
isNext: isNext,
|
||||
);
|
||||
|
||||
tempMarkers.add(
|
||||
Marker(
|
||||
markerId: MarkerId('delivery_$orderId'),
|
||||
position: LatLng(lat, lon),
|
||||
icon: icon,
|
||||
infoWindow: InfoWindow(
|
||||
title: isSkipped
|
||||
? 'Step $displayStep: $customerName (SKIPPED)'
|
||||
: 'Step $displayStep: $customerName',
|
||||
snippet: 'Order #$orderId',
|
||||
),
|
||||
onTap: () {
|
||||
_showCustomerDetailsSheet(delivery, displayStep);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon)));
|
||||
}
|
||||
|
||||
if (currentLocation != null) {
|
||||
tempMarkers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('current_location'),
|
||||
position: currentLocation!,
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(
|
||||
BitmapDescriptor.hueAzure,
|
||||
),
|
||||
infoWindow: const InfoWindow(title: 'You are here'),
|
||||
),
|
||||
);
|
||||
stepPoints.insert(0, _StepPoint(0, currentLocation!));
|
||||
}
|
||||
|
||||
stepPoints.sort((a, b) => a.step.compareTo(b.step));
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
int segmentIndex = 0;
|
||||
|
||||
for (int i = 0; i < stepPoints.length - 1; i++) {
|
||||
final start = stepPoints[i].position;
|
||||
final end = stepPoints[i + 1].position;
|
||||
|
||||
try {
|
||||
// ignore: deprecated_member_use
|
||||
final request = PolylineRequest(
|
||||
origin: PointLatLng(start.latitude, start.longitude),
|
||||
destination: PointLatLng(end.latitude, end.longitude),
|
||||
mode: TravelMode.driving,
|
||||
);
|
||||
|
||||
final result = await _polylinePoints.getRouteBetweenCoordinates(
|
||||
request: request,
|
||||
);
|
||||
|
||||
if (result.points.isNotEmpty) {
|
||||
final routePoints = result.points
|
||||
.map((p) => LatLng(p.latitude, p.longitude))
|
||||
.toList();
|
||||
|
||||
newPolylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('segment_$segmentIndex'),
|
||||
points: routePoints,
|
||||
width: 6,
|
||||
color: Colors.blue,
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
jointType: JointType.round,
|
||||
geodesic: true,
|
||||
),
|
||||
);
|
||||
segmentIndex++;
|
||||
} else {
|
||||
newPolylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('segment_fallback_$segmentIndex'),
|
||||
points: [start, end],
|
||||
width: 4,
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
);
|
||||
segmentIndex++;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Directions error for segment $i: $e');
|
||||
newPolylines.add(
|
||||
Polyline(
|
||||
polylineId: PolylineId('segment_error_$segmentIndex'),
|
||||
points: [start, end],
|
||||
width: 4,
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
);
|
||||
segmentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
markers = tempMarkers;
|
||||
polylines = newPolylines;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
_fitBoundsToMarkersAndPolylines();
|
||||
}
|
||||
|
||||
Future<void> _fitBoundsToMarkersAndPolylines() async {
|
||||
if (mapController == null) return;
|
||||
|
||||
double minLat = double.infinity;
|
||||
double maxLat = -double.infinity;
|
||||
double minLng = double.infinity;
|
||||
double maxLng = -double.infinity;
|
||||
|
||||
bool hasPoint = false;
|
||||
|
||||
for (final m in markers) {
|
||||
final pos = m.position;
|
||||
minLat = math.min(minLat, pos.latitude);
|
||||
maxLat = math.max(maxLat, pos.latitude);
|
||||
minLng = math.min(minLng, pos.longitude);
|
||||
maxLng = math.max(maxLng, pos.longitude);
|
||||
hasPoint = true;
|
||||
}
|
||||
|
||||
for (final poly in polylines) {
|
||||
for (final pos in poly.points) {
|
||||
minLat = math.min(minLat, pos.latitude);
|
||||
maxLat = math.max(maxLat, pos.latitude);
|
||||
minLng = math.min(minLng, pos.longitude);
|
||||
maxLng = math.max(maxLng, pos.longitude);
|
||||
hasPoint = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPoint) return;
|
||||
|
||||
final bounds = LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
await mapController!.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 80),
|
||||
);
|
||||
} catch (e) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () async {
|
||||
try {
|
||||
await mapController!.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 80),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[CUSTOMER_MAP] Retry failed: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<BitmapDescriptor> _createCircularMarkerBitmap(
|
||||
int number, {
|
||||
bool isSkipped = false,
|
||||
bool isNext = false,
|
||||
}) async {
|
||||
const double size = 70;
|
||||
final pictureRecorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(pictureRecorder);
|
||||
final center = Offset(size / 2, size / 2);
|
||||
|
||||
final paint = Paint()
|
||||
..color = isSkipped ? Colors.orange : ColorConstants.primaryColor;
|
||||
canvas.drawCircle(center, 15, paint);
|
||||
|
||||
final border = Paint()
|
||||
..color = isSkipped ? Colors.orange.shade900 : Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = isSkipped ? 4 : 3;
|
||||
canvas.drawCircle(center, 15, border);
|
||||
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: number.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
textPainter.layout();
|
||||
textPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
center.dx - textPainter.width / 2,
|
||||
center.dy - textPainter.height / 2,
|
||||
),
|
||||
);
|
||||
|
||||
// Draw "NEXT" indicator (road sign with down arrow) on top
|
||||
if (isNext) {
|
||||
// Draw road sign background (rectangle)
|
||||
final signPaint = Paint()
|
||||
..color = Colors.green
|
||||
..style = PaintingStyle.fill;
|
||||
final signRect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(center: Offset(size / 2, 8), width: 45, height: 30),
|
||||
const Radius.circular(4),
|
||||
);
|
||||
canvas.drawRRect(signRect, signPaint);
|
||||
|
||||
// Draw border
|
||||
final signBorder = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5;
|
||||
canvas.drawRRect(signRect, signBorder);
|
||||
|
||||
// Draw "NEXT" text
|
||||
final nextTextPainter = TextPainter(
|
||||
text: const TextSpan(
|
||||
text: 'NEXT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
);
|
||||
nextTextPainter.layout();
|
||||
nextTextPainter.paint(
|
||||
canvas,
|
||||
Offset(size / 2 - nextTextPainter.width / 2, 4),
|
||||
);
|
||||
|
||||
// Draw down arrow
|
||||
final arrowPath = Path();
|
||||
arrowPath.moveTo(size / 2, 18);
|
||||
arrowPath.lineTo(size / 2 - 4, 24);
|
||||
arrowPath.lineTo(size / 2 + 4, 24);
|
||||
arrowPath.close();
|
||||
final arrowPaint = Paint()
|
||||
..color = Colors.white
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawPath(arrowPath, arrowPaint);
|
||||
}
|
||||
|
||||
final img = await pictureRecorder.endRecording().toImage(
|
||||
size.toInt(),
|
||||
size.toInt(),
|
||||
);
|
||||
final data = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
return BitmapDescriptor.bytes(data!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
void _showCustomerDetailsSheet(
|
||||
Map<String, dynamic> delivery,
|
||||
int stepNumber,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
final customerName = (delivery['deliverycustomer'] ?? 'Customer')
|
||||
.toString();
|
||||
final address = (delivery['deliveryaddress'] ?? 'Address not available')
|
||||
.toString();
|
||||
final phone = (delivery['deliverycontactno'] ?? '').toString();
|
||||
final orderId = (delivery['orderid'] ?? '').toString();
|
||||
final status = (delivery['orderstatus']?.toString().toLowerCase() ?? '')
|
||||
.trim();
|
||||
final isSkipped = status == 'skipped';
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with title and close icon
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Customer Details',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.cancel,
|
||||
size: 34,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Divider(thickness: 1.5),
|
||||
|
||||
// Step number and status
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isSkipped
|
||||
? Colors.orange
|
||||
: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Step $stepNumber',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSkipped) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'SKIPPED',
|
||||
style: TextStyle(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Customer name
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.person, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Customer Name',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
customerName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery address
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 20,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Delivery Address',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Phone number
|
||||
if (phone.isNotEmpty)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.phone, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Contact Number',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
phone,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Order ID
|
||||
Text(
|
||||
'Order ID: $orderId',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Call button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
// Close bottom sheet first
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Small delay to ensure bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
// Then make the call
|
||||
final bool success = await launchPhoneDialer(
|
||||
phone.isNotEmpty ? phone : '9876543210',
|
||||
);
|
||||
if (!success && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Could not launch dialer'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
label: const Text(
|
||||
'Call',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
LatLng initialTarget;
|
||||
double initialZoom = 13;
|
||||
|
||||
if (currentLocation != null) {
|
||||
initialTarget = currentLocation!;
|
||||
} else if (widget.deliveries.isNotEmpty) {
|
||||
final firstDelivery = widget.deliveries.first;
|
||||
final lat = _parseD(
|
||||
firstDelivery['droplat'] ?? firstDelivery['deliverylat'],
|
||||
);
|
||||
final lon = _parseD(
|
||||
firstDelivery['droplon'] ?? firstDelivery['deliverylong'],
|
||||
);
|
||||
initialTarget = (lat != 0 && lon != 0)
|
||||
? LatLng(lat, lon)
|
||||
: const LatLng(11.0168, 76.9558);
|
||||
} else {
|
||||
initialTarget = const LatLng(11.0168, 76.9558);
|
||||
}
|
||||
|
||||
final initialCameraPosition = CameraPosition(
|
||||
target: initialTarget,
|
||||
zoom: initialZoom,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(70),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
elevation: 0,
|
||||
toolbarHeight: 80,
|
||||
leadingWidth: double.infinity,
|
||||
leading: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
const Text(
|
||||
'Delivery Route',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: GoogleMap(
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: polylines,
|
||||
myLocationEnabled: true,
|
||||
myLocationButtonEnabled: true,
|
||||
onMapCreated: (controller) {
|
||||
mapController = controller;
|
||||
_mapReady = true;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (_mapReady) _fitBoundsToMarkersAndPolylines();
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1004
lib/views/Dashboard/deliveries/nav.dart
Normal file
1004
lib/views/Dashboard/deliveries/nav.dart
Normal file
File diff suppressed because it is too large
Load Diff
172
lib/views/Dashboard/deliveries/pip.dart
Normal file
172
lib/views/Dashboard/deliveries/pip.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PIP INFO CARD (Shown when PiP mode is enabled)
|
||||
// -------------------------------------------------------------------------
|
||||
class PipInfoCard extends StatefulWidget {
|
||||
final String orderId;
|
||||
final int etaMinutes; // ETA in minutes from API (always treat as minutes)
|
||||
|
||||
const PipInfoCard({
|
||||
super.key,
|
||||
required this.orderId,
|
||||
required this.etaMinutes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PipInfoCard> createState() => _PipInfoCardState();
|
||||
}
|
||||
|
||||
class _PipInfoCardState extends State<PipInfoCard> {
|
||||
late final CountDownController _controller;
|
||||
int _durationSeconds = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = CountDownController();
|
||||
// Load remaining ETA from SharedPreferences so timer doesn't reset
|
||||
_initDuration();
|
||||
}
|
||||
|
||||
Future<void> _initDuration() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final endKey = 'eta_endtime_${widget.orderId}';
|
||||
final endSeconds = prefs.getInt(endKey);
|
||||
final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
int remaining = 0;
|
||||
if (endSeconds != null && endSeconds > nowSeconds) {
|
||||
remaining = endSeconds - nowSeconds;
|
||||
} else {
|
||||
// Fallback: use full ETA from API
|
||||
final int safeEtaMinutes =
|
||||
widget.etaMinutes < 0 ? 0 : widget.etaMinutes;
|
||||
remaining = safeEtaMinutes * 60;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_durationSeconds = remaining.clamp(0, 24 * 60 * 60);
|
||||
});
|
||||
} catch (_) {
|
||||
// In case of error, just fall back to raw ETA minutes
|
||||
final int safeEtaMinutes = widget.etaMinutes < 0 ? 0 : widget.etaMinutes;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_durationSeconds = safeEtaMinutes * 60;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// Expanded PiP: timer + key delivery details, but still relatively compact
|
||||
body: Center(
|
||||
child: widget.orderId.isEmpty || widget.orderId == 'N/A'
|
||||
? Text(
|
||||
'No Active Order',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
)
|
||||
: Card(
|
||||
color: Colors.white,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Container(
|
||||
width: 220,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 6,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Timer
|
||||
SizedBox(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: _durationSeconds <= 0
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 3,
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Out',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircularCountDownTimer(
|
||||
duration: _durationSeconds,
|
||||
initialDuration: 0,
|
||||
controller: _controller,
|
||||
width: 72,
|
||||
height: 72,
|
||||
ringColor: ColorConstants.primaryColor
|
||||
.withValues(alpha: 0.15),
|
||||
fillColor: ColorConstants.primaryColor,
|
||||
backgroundColor: Colors.white,
|
||||
strokeWidth: 5,
|
||||
strokeCap: StrokeCap.round,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
isReverse: true,
|
||||
isReverseAnimation: true,
|
||||
isTimerTextShown: true,
|
||||
autoStart: true,
|
||||
timeFormatterFunction:
|
||||
(defaultFormatter, duration) {
|
||||
return duration.inSeconds <= 0
|
||||
? 'Out'
|
||||
: defaultFormatter(duration);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// Active order id
|
||||
Text(
|
||||
'Active Order ID: ${widget.orderId}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1031
lib/views/Dashboard/deliveries/sheet.dart
Normal file
1031
lib/views/Dashboard/deliveries/sheet.dart
Normal file
File diff suppressed because it is too large
Load Diff
411
lib/views/Dashboard/deliveries/skip_sheet.dart
Normal file
411
lib/views/Dashboard/deliveries/skip_sheet.dart
Normal file
@@ -0,0 +1,411 @@
|
||||
part of 'deliveries.dart';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SKIP REASONS SHEET
|
||||
// -------------------------------------------------------------------------
|
||||
Future<void> _showMyOptionsSheet(
|
||||
BuildContext context,
|
||||
Map<String, dynamic> delivery,
|
||||
_MyDeliveriesState? parentState,
|
||||
) async {
|
||||
int selected = -1;
|
||||
bool isLoading = false;
|
||||
|
||||
final List<String> skipReasons = [
|
||||
'Customer unreachable',
|
||||
'Customer not at the location',
|
||||
];
|
||||
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
Widget optionBox(String title, int index) {
|
||||
final bool isSelected = selected == index;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (!isLoading) {
|
||||
setState(() => selected = index);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor.withOpacity(0.1)
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: isSelected
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey,
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleConfirm() async {
|
||||
if (selected == -1 || isLoading) return;
|
||||
|
||||
// Check skip limits before proceeding
|
||||
final dc = Get.put(DeliveriesController(), permanent: true);
|
||||
final skipStatus = await dc.checkSkipStatus();
|
||||
final int skipCount = skipStatus['count'] ?? 0;
|
||||
bool applyPenalty = false;
|
||||
|
||||
if (skipCount >= 2) {
|
||||
// Show Styled Alert Dialog
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
elevation: 5,
|
||||
backgroundColor: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Colors.red.shade600,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Skip Limit Reached',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'You have exceeded the limit of 2 skips within 3 hours.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.orange.shade200,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.orange.shade800, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Proceeding will forfeit your bonus points for this session.",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.orange.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Confirm Skip",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirm != true) return; // User cancelled or dismissed
|
||||
applyPenalty = true;
|
||||
}
|
||||
|
||||
setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
final dc = Get.put(DeliveriesController(), permanent: true);
|
||||
final d = delivery;
|
||||
final int deliveryId =
|
||||
int.tryParse('${d['deliveryid'] ?? 0}') ?? 0;
|
||||
final int orderHeaderId =
|
||||
int.tryParse('${d['orderheaderid'] ?? 0}') ?? 0;
|
||||
final String reason = selected >= 0 && selected < skipReasons.length
|
||||
? skipReasons[selected]
|
||||
: '';
|
||||
|
||||
if (deliveryId > 0 && orderHeaderId > 0) {
|
||||
String riderLatStr = '0';
|
||||
String riderLngStr = '0';
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
timeLimit: const Duration(seconds: 5),
|
||||
);
|
||||
riderLatStr = position.latitude.toStringAsFixed(6);
|
||||
riderLngStr = position.longitude.toStringAsFixed(6);
|
||||
} catch (e) {
|
||||
debugPrint('[SKIP] Error getting rider location: $e');
|
||||
try {
|
||||
final lastPos = await Geolocator.getLastKnownPosition();
|
||||
if (lastPos != null) {
|
||||
riderLatStr = lastPos.latitude.toStringAsFixed(6);
|
||||
riderLngStr = lastPos.longitude.toStringAsFixed(6);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[SKIP] Updating status for deliveryId=$deliveryId orderHeaderId=$orderHeaderId reason="$reason"',
|
||||
);
|
||||
|
||||
final ok = await dc.updateSkippedStatus(
|
||||
deliveryId: deliveryId,
|
||||
orderHeaderId: orderHeaderId,
|
||||
ridersLat: riderLatStr,
|
||||
ridersLng: riderLngStr,
|
||||
notes: reason,
|
||||
);
|
||||
|
||||
debugPrint('[SKIP] Status update result: $ok');
|
||||
|
||||
if (ok && context.mounted) {
|
||||
// Register skip locally only on success
|
||||
await dc.registerSkip(applyPenalty: applyPenalty);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // Close the sheet
|
||||
|
||||
if (parentState != null) {
|
||||
// Only update list if called from the list view
|
||||
parentState.markOrderAsSkipped(d, reason);
|
||||
} else {
|
||||
// If called from Navigation/Map (where parentState is null),
|
||||
// we just close the sheet and let the caller handle the UI update
|
||||
// typically by popping the route or showing a snackbar.
|
||||
// We DO NOT force navigation to "MyDeliveries".
|
||||
debugPrint(
|
||||
'[SKIP] Skipped from Nav/Map screen, sheet closed.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Failed to skip delivery. Please try again.',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'[SKIP] Invalid deliveryId: $deliveryId or orderHeaderId: $orderHeaderId',
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Invalid delivery information.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[SKIP] Error updating skip status: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('An error occurred. Please try again.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (context.mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 30),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Select Reason',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cancel,
|
||||
color: Colors.red, size: 32),
|
||||
onPressed: isLoading ? null : () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
optionBox('Customer unreachable', 0),
|
||||
optionBox('Customer not at the location', 1),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: (selected == -1 || isLoading)
|
||||
? Colors.grey.shade300
|
||||
: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
(selected == -1 || isLoading) ? null : handleConfirm,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Confirm',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: (selected == -1 || isLoading)
|
||||
? Colors.black45
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
3085
lib/views/Dashboard/home/homepage.dart
Normal file
3085
lib/views/Dashboard/home/homepage.dart
Normal file
File diff suppressed because it is too large
Load Diff
145
lib/views/Dashboard/home/homepage_banner.dart
Normal file
145
lib/views/Dashboard/home/homepage_banner.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
// Active Delivery Banner Widget for Home Page
|
||||
// ignore_for_file: unused_import
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class ActiveDeliveryBanner extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> activeDeliveries;
|
||||
final Function(Map<String, dynamic>) onTap;
|
||||
|
||||
const ActiveDeliveryBanner({
|
||||
super.key,
|
||||
required this.activeDeliveries,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
String _getDeliveryAddress(Map<String, dynamic> delivery) {
|
||||
final address =
|
||||
delivery['deliveryaddress'] ??
|
||||
delivery['DeliveryAddress'] ??
|
||||
delivery['address'] ??
|
||||
'';
|
||||
if (address.toString().length > 40) {
|
||||
return '${address.toString().substring(0, 40)}...';
|
||||
}
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (activeDeliveries.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Show first active delivery (or show count if multiple)
|
||||
final delivery = activeDeliveries.first;
|
||||
final count = activeDeliveries.length;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => onTap(delivery),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
// Active indicator icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.two_wheeler,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Delivery info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
count > 1
|
||||
? '$count Active Deliveries'
|
||||
: 'Active Delivery',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 19,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (count > 1) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getDeliveryAddress(delivery),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 17,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Arrow icon
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
611
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
611
lib/views/Dashboard/orders/orderstaus_button.dart
Normal file
@@ -0,0 +1,611 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'dart:async';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:slide_to_submit_button/slide_to_submit_button.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
/// ------------------------------
|
||||
/// MAIN WIDGET WITH TWO BUTTONS
|
||||
/// ------------------------------
|
||||
class OrderStatusRow extends StatefulWidget {
|
||||
final String currentStatus;
|
||||
final Future<bool> Function(String newStatus, {String? notes, String? proofImagePath}) onStatusChange;
|
||||
final bool enabled;
|
||||
|
||||
const OrderStatusRow({
|
||||
super.key,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChange,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderStatusRow> createState() => _OrderStatusRowState();
|
||||
}
|
||||
|
||||
class _OrderStatusRowState extends State<OrderStatusRow> {
|
||||
bool _isProcessing = false;
|
||||
String? _overrideStatus;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant OrderStatusRow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentStatus != widget.currentStatus &&
|
||||
mounted &&
|
||||
_overrideStatus != null &&
|
||||
widget.currentStatus == _overrideStatus) {
|
||||
setState(() {
|
||||
_overrideStatus = null;
|
||||
});
|
||||
} else if (oldWidget.currentStatus != widget.currentStatus &&
|
||||
_overrideStatus != null) {
|
||||
_overrideStatus = null;
|
||||
}
|
||||
}
|
||||
|
||||
String get _effectiveStatus => _overrideStatus ?? widget.currentStatus;
|
||||
|
||||
Future<bool> _onStatusChanged(String newStatus, {String? notes, String? proofImagePath}) async {
|
||||
debugPrint('[OSR] _onStatusChanged: $newStatus, proofImagePath: $proofImagePath');
|
||||
if (_isProcessing) return false;
|
||||
|
||||
// Optimistic UI: flip status immediately to avoid visible lag.
|
||||
setState(() {
|
||||
_isProcessing = true;
|
||||
_overrideStatus = newStatus;
|
||||
});
|
||||
|
||||
bool success = false;
|
||||
bool timedOut = false;
|
||||
try {
|
||||
// Hard cap wait time to keep UI from spinning indefinitely.
|
||||
success = await widget
|
||||
.onStatusChange(newStatus, notes: notes, proofImagePath: proofImagePath)
|
||||
.timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () {
|
||||
timedOut = true;
|
||||
// Assume success on timeout to avoid UI rollback; data will
|
||||
// refresh from server on next fetch.
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (_) {
|
||||
success = false;
|
||||
} finally {
|
||||
if (!mounted) return success;
|
||||
setState(() {
|
||||
_isProcessing = false;
|
||||
// If API failed, revert the optimistic status.
|
||||
// If timed out, keep the optimistic status (server likely completed).
|
||||
_overrideStatus = (success || timedOut) ? newStatus : null;
|
||||
});
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// REJECT SHEET
|
||||
// ------------------------------
|
||||
void _showRejectSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
String selectedReason = "";
|
||||
bool isLoading = false;
|
||||
final List<String> reasons = [
|
||||
"Customer not reachable",
|
||||
"Wrong address",
|
||||
"Out of delivery area",
|
||||
"Other reason",
|
||||
];
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Reject Order",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.red),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
for (String reason in reasons)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: GestureDetector(
|
||||
onTap: () => setModalState(() {
|
||||
selectedReason = reason;
|
||||
}),
|
||||
child: Container(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedReason == reason
|
||||
? Colors.red
|
||||
: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
reason,
|
||||
style: TextStyle(
|
||||
color: selectedReason == reason
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: selectedReason.isEmpty || isLoading
|
||||
? null
|
||||
: () async {
|
||||
setModalState(() => isLoading = true);
|
||||
final success = await _onStatusChanged(
|
||||
"REJECTED",
|
||||
notes: selectedReason,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, success);
|
||||
}
|
||||
},
|
||||
child: isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: const Text(
|
||||
"Reject Order",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// CANCEL SHEET
|
||||
// ------------------------------
|
||||
void _showCancelSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
bool confirmCancel = false;
|
||||
bool isLoading = false;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Cancel this order?",
|
||||
style:
|
||||
TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"Once cancelled, this order will return to pending state.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
CheckboxListTile(
|
||||
value: confirmCancel,
|
||||
onChanged: (value) => setModalState(
|
||||
() => confirmCancel = value ?? false,
|
||||
),
|
||||
title: const Text("I confirm to cancel this order"),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
minimumSize: const Size(double.infinity, 50),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: !confirmCancel
|
||||
? null
|
||||
: () async {
|
||||
setModalState(() => isLoading = true);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Order Cancelled"),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
)
|
||||
: const Text(
|
||||
"Confirm Cancel",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String currentStatus = _effectiveStatus;
|
||||
final bool showCancel =
|
||||
currentStatus == "ARRIVED" || currentStatus == "PICKED";
|
||||
final String leftText = showCancel ? "CANCEL" : "REJECT";
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// LEFT BUTTON (Reject / Cancel)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (showCancel) {
|
||||
_showCancelSheet(context);
|
||||
} else {
|
||||
_showRejectSheet(context);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red, //
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
leftText,
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// RIGHT BUTTON (ACCEPT / ARRIVED / PICKED)
|
||||
OrderStatusButton(
|
||||
key: ValueKey(currentStatus),
|
||||
currentStatus: currentStatus,
|
||||
onStatusChange: (status, {proofImagePath}) =>
|
||||
_onStatusChanged(status, proofImagePath: proofImagePath),
|
||||
enabled: widget.enabled && !_isProcessing,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isProcessing)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: SizedBox(
|
||||
height: 22,
|
||||
width: 22,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ------------------------------
|
||||
/// STATUS BUTTON LOGIC (NO DELIVERY)
|
||||
/// ------------------------------
|
||||
class OrderStatusButton extends StatefulWidget {
|
||||
final String currentStatus;
|
||||
final Future<bool> Function(String newStatus, {String? proofImagePath}) onStatusChange;
|
||||
final bool enabled;
|
||||
|
||||
const OrderStatusButton({
|
||||
super.key,
|
||||
required this.currentStatus,
|
||||
required this.onStatusChange,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OrderStatusButton> createState() => _OrderStatusButtonState();
|
||||
}
|
||||
|
||||
class _OrderStatusButtonState extends State<OrderStatusButton> {
|
||||
String get _buttonText => widget.currentStatus;
|
||||
|
||||
Color _getButtonColor() {
|
||||
switch (_buttonText) {
|
||||
case "ARRIVED":
|
||||
return Colors.orange;
|
||||
case "PICKED":
|
||||
return ColorConstants.primaryColor;
|
||||
default:
|
||||
return Colors.green;
|
||||
}
|
||||
}
|
||||
|
||||
void _showStatusSheet() {
|
||||
if (_buttonText == "PICKED") return; // final stage now
|
||||
|
||||
bool isConfirmLoading = false;
|
||||
|
||||
// Determine next status (single step)
|
||||
String? nextStatus;
|
||||
if (_buttonText == "ACCEPT") {
|
||||
nextStatus = "ACCEPTED";
|
||||
} else if (_buttonText == "ACCEPTED") {
|
||||
nextStatus = "ARRIVED";
|
||||
} else if (_buttonText == "ARRIVED") {
|
||||
nextStatus = "PICKED";
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
// If for some reason we don't have a valid next status, show nothing
|
||||
if (nextStatus == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Move Your Order to",
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -5),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.cancel,
|
||||
color: Colors.red,
|
||||
size: 36,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Slider to confirm moving to next status wrapped with SafeArea
|
||||
SafeArea(
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
bottom: true,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SlideToSubmit.custom(
|
||||
height: 55,
|
||||
sliderWidth: 40,
|
||||
padding: const EdgeInsets.all(8),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: _getButtonColor().withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
color: _getButtonColor(),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
slider: Center(
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 24,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
hint: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Slide to mark $nextStatus',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: const Color.fromARGB(255, 15, 14, 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
onSubmit: (controller) async {
|
||||
if (isConfirmLoading) return;
|
||||
|
||||
setModalState(() => isConfirmLoading = true);
|
||||
|
||||
// Close sheet after slide completes
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
// Trigger status change callback
|
||||
WidgetsBinding.instance.addPostFrameCallback((
|
||||
_,
|
||||
) async {
|
||||
if (nextStatus == "PICKED") {
|
||||
debugPrint('[OSB] Status is PICKED, launching camera...');
|
||||
// Trigger Camera
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? photo = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 50, // Optimize size
|
||||
);
|
||||
|
||||
if (photo != null) {
|
||||
debugPrint('[OSB] Photo taken: ${photo.path}');
|
||||
// Process with image
|
||||
await widget.onStatusChange(
|
||||
nextStatus!,
|
||||
proofImagePath: photo.path,
|
||||
);
|
||||
} else {
|
||||
debugPrint('[OSB] Camera cancelled or photo null');
|
||||
}
|
||||
} else {
|
||||
debugPrint('[OSB] Status NOT PICKED (is $nextStatus), normal flow');
|
||||
// Normal flow
|
||||
await widget.onStatusChange(nextStatus!);
|
||||
}
|
||||
|
||||
try {
|
||||
controller.reset();
|
||||
} catch (_) {}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: widget.enabled ? _showStatusSheet : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.enabled ? _getButtonColor() : Colors.grey,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_buttonText,
|
||||
style: TextStyle(
|
||||
fontSize: 19,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
464
lib/views/Dashboard/profile/Profilepage.dart
Normal file
464
lib/views/Dashboard/profile/Profilepage.dart
Normal file
@@ -0,0 +1,464 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; // ⭐ REQUIRED
|
||||
import 'package:nearle/views/Dashboard/profile/informations/help_center.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/profile.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/saved_address.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/faq.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/notifications_page.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/support_ticket.dart';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/controllers/profile_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/order_alert_sound.dart';
|
||||
import 'package:nearle/controllers/rewards_controller.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/rewards_card.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/rider_rewards_page.dart';
|
||||
import 'package:nearle/utils/mqtt_service.dart';
|
||||
|
||||
class ProfilePage extends StatefulWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
State<ProfilePage> createState() => _ProfilePageState();
|
||||
}
|
||||
|
||||
class _ProfilePageState extends State<ProfilePage> {
|
||||
late final ProfileController _profileController =
|
||||
Get.isRegistered<ProfileController>()
|
||||
? Get.find<ProfileController>()
|
||||
: Get.put(ProfileController(), permanent: true);
|
||||
|
||||
final RewardsController _rewardsController = Get.put(RewardsController());
|
||||
|
||||
String _name = '';
|
||||
String _email = '';
|
||||
String _contact = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfilePrefs();
|
||||
ever(_profileController.userName, (_) => _assignFromController());
|
||||
ever(_profileController.userEmail, (_) => _assignFromController());
|
||||
ever(_profileController.userContact, (_) => _assignFromController());
|
||||
ever(_profileController.userAddress, (_) => _assignFromController());
|
||||
_profileController.loadFromPrefs();
|
||||
}
|
||||
|
||||
Future<void> _loadProfilePrefs() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_name = prefs.getString('user_name') ?? '';
|
||||
_email = prefs.getString('user_email') ?? '';
|
||||
_contact = prefs.getString('contactno') ?? '';
|
||||
_contact = prefs.getString('contactno') ?? '';
|
||||
});
|
||||
|
||||
final userId = prefs.getInt('userid') ?? 0;
|
||||
if (userId > 0) {
|
||||
_rewardsController.fetchBonusSummary(userId);
|
||||
}
|
||||
}
|
||||
|
||||
void _assignFromController() {
|
||||
setState(() {
|
||||
if (_profileController.userName.value.trim().isNotEmpty) {
|
||||
_name = _profileController.userName.value.trim();
|
||||
}
|
||||
if (_profileController.userEmail.value.trim().isNotEmpty) {
|
||||
_email = _profileController.userEmail.value.trim();
|
||||
}
|
||||
if (_profileController.userContact.value.trim().isNotEmpty) {
|
||||
_contact = _profileController.userContact.value.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
elevation: 0,
|
||||
toolbarHeight: 70.h, // ⭐ responsive
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(top: 12.h),
|
||||
child: Text(
|
||||
"PROFILE",
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxxLarge(context).sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.h),
|
||||
child: Divider(height: 1.h, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
|
||||
body: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 40.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
/// ⭐ PROFILE CARD RESPONSIVE
|
||||
Container(
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 22.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_name.isNotEmpty ? _name : "—",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone,
|
||||
color: Colors.white,
|
||||
size: 20.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_contact.isNotEmpty ? _contact : "—",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.email,
|
||||
color: Colors.white,
|
||||
size: 20.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
_email.isNotEmpty ? _email : "—",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// ⭐ Circle Avatar Responsive
|
||||
Obx(() {
|
||||
final path = _profileController.imagePath.value;
|
||||
final hasImage =
|
||||
path.isNotEmpty && File(path).existsSync();
|
||||
|
||||
return CircleAvatar(
|
||||
radius: 40.r,
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
backgroundImage: hasImage
|
||||
? FileImage(File(path))
|
||||
: null,
|
||||
child: !hasImage
|
||||
? Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40.sp,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
// ⭐ REWARDS SECTION
|
||||
RewardsCard(controller: _rewardsController),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
_buildHeader("Your Information"),
|
||||
_buildBox([
|
||||
_buildInfoTile(Icons.person, "Profile"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.location_on, "Saved Address"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.card_giftcard, "Rewards"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.notifications, "Notification"),
|
||||
]),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
_buildHeader("Support"),
|
||||
_buildBox([
|
||||
_buildInfoTile(Icons.support_agent, "Help Centre"),
|
||||
Divider(),
|
||||
_buildInfoTile(Icons.local_activity, "Support tickets"),
|
||||
]),
|
||||
|
||||
SizedBox(height: 20.h),
|
||||
|
||||
_buildHeader("Other Information"),
|
||||
_buildBox([
|
||||
_buildInfoTile(Icons.translate, "Faq"),
|
||||
Divider(),
|
||||
// _buildInfoTile(Icons.sticky_note_2, "Terms & Conditions"),
|
||||
// Divider(),
|
||||
_buildInfoTile(
|
||||
Icons.notifications_active,
|
||||
"Order alert sound",
|
||||
),
|
||||
]),
|
||||
|
||||
SizedBox(height: 55.h),
|
||||
|
||||
/// ⭐ Logout Button Responsive
|
||||
SizedBox(
|
||||
height: 55.h,
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _showLogoutDialog(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
side: BorderSide(color: Colors.black, width: 0.2.w),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Logout",
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 15.h),
|
||||
|
||||
Center(
|
||||
child: Text(
|
||||
"App Version 1.2.19",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(String title) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(8.r),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBox(List<Widget> children) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Column(children: children),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoTile(IconData icon, String title) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, size: 26.sp),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
trailing: Icon(Icons.arrow_forward_ios, size: 22.sp),
|
||||
onTap: () => _handleNavigation(title),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleNavigation(String title) {
|
||||
switch (title) {
|
||||
case "Profile":
|
||||
Get.to(() => Profile());
|
||||
break;
|
||||
case "Saved Address":
|
||||
Get.to(() => const SavedAddressPage());
|
||||
break;
|
||||
case "Notification":
|
||||
Get.to(() => const NotificationsPage());
|
||||
break;
|
||||
case "Help Centre":
|
||||
Get.to(() => HelpCenter());
|
||||
break;
|
||||
case "Support tickets":
|
||||
Get.to(() => SupportTicket());
|
||||
break;
|
||||
case "Rewards":
|
||||
Get.to(() => const RiderRewardsPage());
|
||||
break;
|
||||
case "Faq":
|
||||
Get.to(() => const FaqPage());
|
||||
break;
|
||||
// case "Terms & Conditions":
|
||||
// Get.to(() => const TermsCondition());
|
||||
// break;
|
||||
case "Order alert sound":
|
||||
Get.to(() => const OrderAlertSoundPage());
|
||||
break;
|
||||
default:
|
||||
debugPrint("Tapped on $title — no page linked yet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showLogoutDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: FittedBox(
|
||||
child: AlertDialog(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
title: Text(
|
||||
"Logout",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
"Are you sure you want to logout?",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
"No",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color.fromARGB(255, 153, 121, 167),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userId =
|
||||
prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
if (userId != 0) {
|
||||
await prefs.remove('skipped_orders_cache_$userId');
|
||||
}
|
||||
NearleMqttService().disconnect();
|
||||
prefs.setBool('logged_out', true);
|
||||
Get.offAll(() => const SignIn());
|
||||
},
|
||||
child: Text(
|
||||
"Yes",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
color: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
95
lib/views/Dashboard/profile/informations/faq.dart
Normal file
95
lib/views/Dashboard/profile/informations/faq.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class FaqController extends GetxController {
|
||||
WebViewController? webViewController;
|
||||
var isLoading = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initializeWebView();
|
||||
}
|
||||
|
||||
void initializeWebView() {
|
||||
webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (url) {
|
||||
isLoading.value = true;
|
||||
print('Started loading: $url');
|
||||
},
|
||||
onPageFinished: (url) {
|
||||
isLoading.value = false;
|
||||
print('Finished loading: $url');
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
isLoading.value = false;
|
||||
print('WebView error: ${error.description}');
|
||||
},
|
||||
),
|
||||
);
|
||||
loadFaqUrl();
|
||||
}
|
||||
|
||||
Future<void> loadFaqUrl() async {
|
||||
if (webViewController != null) {
|
||||
try {
|
||||
await webViewController!.loadRequest(
|
||||
Uri.parse('https://nearle.in/faq'),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error loading URL: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FaqPage extends StatelessWidget {
|
||||
const FaqPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(FaqController());
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
), // :small_blue_diamond: white back arrow
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // goes back to previous screen
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'FAQ',
|
||||
style: TextStyle(
|
||||
fontSize: 26, // :small_blue_diamond: larger font size
|
||||
color: Colors.white, // :small_blue_diamond: white text
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: Obx(() {
|
||||
final wvc = controller.webViewController;
|
||||
return Stack(
|
||||
children: [
|
||||
if (wvc != null) WebViewWidget(controller: wvc),
|
||||
if (controller.isLoading.value)
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
380
lib/views/Dashboard/profile/informations/help_center.dart
Normal file
380
lib/views/Dashboard/profile/informations/help_center.dart
Normal file
@@ -0,0 +1,380 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
class HelpCenter extends StatelessWidget {
|
||||
const HelpCenter({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Help Center',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header text
|
||||
Text(
|
||||
"We're here to help you with anything and \neverything on Nearle Xpress",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFamily: FontConstants.fontFamily
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"We make sure your delivery experience is smooth and clear. Whether you’re on your first trip or your hundredth, we’ve got your back. Browse through frequently asked questions or reach out directly if you need further help.",
|
||||
style: TextStyle(fontSize: 18,color: Colors.grey.shade700, height: 1.4, fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search help',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
'FAQ',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily,color: ColorConstants.primaryColor),
|
||||
),
|
||||
Divider(),
|
||||
|
||||
|
||||
|
||||
_FaqTile(
|
||||
title: 'What is Nearle Xpress?',
|
||||
initiallyExpanded: true,
|
||||
child: const Text(
|
||||
'Nearle Xpress is a delivery app for riders who complete local deliveries for nearby stores and markets. It helps riders accept tasks, manage pickup and drop points, and update deliveries in real time.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'How do I accept a delivery task?',
|
||||
child: const Text(
|
||||
'You can accept tasks from the Home screen when a new order appears. Tap on the order to view details and then press Accept.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'How do I update the delivery status?',
|
||||
child: const Text(
|
||||
'Open the active task and use the status buttons to mark Pickup, On the way, and Delivered. Ensure accurate updates for better tracking.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'Can I view my past deliveries?',
|
||||
child: const Text(
|
||||
'Yes. Go to the History section from your dashboard to see completed deliveries and earnings.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
_FaqTile(
|
||||
title: 'What if I face an issue during delivery?',
|
||||
child: const Text(
|
||||
'Use the Help Center to report an issue or contact support. Provide order details and a short description of the problem.',
|
||||
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10,),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Still stuck? Help is a mail away!",style: TextStyle(fontSize: 18,fontFamily: FontConstants.fontFamily,fontWeight: FontWeight.bold,color: ColorConstants.primaryColor),),
|
||||
],
|
||||
)
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(padding: EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
foregroundColor: ColorConstants.primaryColor,
|
||||
side: BorderSide(color: ColorConstants.primaryColor, width: 1.2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const HelpCenterMessage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'Send a message',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold,fontFamily: FontConstants.fontFamily,color: Colors.white),
|
||||
),
|
||||
),
|
||||
),),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FaqTile extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget child;
|
||||
final bool initiallyExpanded;
|
||||
|
||||
const _FaqTile({
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.initiallyExpanded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(fontWeight: FontWeight.w600,fontSize: 18,fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// -------------------------------message page------------------------------------
|
||||
class HelpCenterMessage extends StatefulWidget {
|
||||
const HelpCenterMessage({super.key});
|
||||
|
||||
@override
|
||||
State<HelpCenterMessage> createState() => _HelpCenterMessageState();
|
||||
}
|
||||
|
||||
class _HelpCenterMessageState extends State<HelpCenterMessage> {
|
||||
final _subjectController = TextEditingController();
|
||||
final _messageController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subjectController.dispose();
|
||||
_messageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 80,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'Help Centre',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Heading
|
||||
Text(
|
||||
'Send Us a Message',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Not finding what you're looking for in the FAQs? Don't worry—we're here to help!",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey.shade700,
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Subject label
|
||||
Text(
|
||||
'Subject',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _subjectController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type Something',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter a subject' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Message label
|
||||
Text(
|
||||
'Your Message',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _messageController,
|
||||
minLines: 5,
|
||||
maxLines: 8,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type Something',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
alignLabelWithHint: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
|
||||
),
|
||||
),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter your message' : null,
|
||||
),
|
||||
SizedBox(height: 30,),
|
||||
SizedBox(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Message sent')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Send a message',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
176
lib/views/Dashboard/profile/informations/notifications_page.dart
Normal file
176
lib/views/Dashboard/profile/informations/notifications_page.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class NotificationsPage extends StatefulWidget {
|
||||
const NotificationsPage({super.key});
|
||||
|
||||
@override
|
||||
State<NotificationsPage> createState() => _NotificationsPageState();
|
||||
}
|
||||
|
||||
class _NotificationsPageState extends State<NotificationsPage> {
|
||||
List<Map<String, dynamic>> _items = const [];
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString('notifications_log');
|
||||
List<Map<String, dynamic>> parsed = [];
|
||||
if (raw != null && raw.isNotEmpty) {
|
||||
try {
|
||||
final list = jsonDecode(raw) as List<dynamic>;
|
||||
parsed = list.map((e) => (e as Map).map((k, v) => MapEntry(k.toString(), v))).toList();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = parsed;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70, // increases AppBar height
|
||||
elevation: 4,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white), // white back arrow
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
'Notifications',
|
||||
style: const TextStyle(
|
||||
fontSize: 26, // larger font size
|
||||
color: Colors.white, // white text
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
).copyWith(fontFamily: FontConstants.fontFamily), // keep your font
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_sweep, color: Colors.white), // white icon
|
||||
onPressed: () async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('notifications_log');
|
||||
if (!mounted) return;
|
||||
setState(() => _items = const []);
|
||||
// ignore: use_build_context_synchronously
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Notifications cleared')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _items.isEmpty
|
||||
? Center(child: Text('No notifications yet',style: TextStyle(fontSize: 20,fontFamily: FontConstants.fontFamily),))
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: _items.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final it = _items[index];
|
||||
final title = (it['title'] ?? 'Nearle').toString();
|
||||
final body = (it['body'] ?? '').toString();
|
||||
final time = (it['time'] ?? '').toString();
|
||||
final imageUrl = (it['imageUrl'] ?? '').toString();
|
||||
final imagePath = (it['imagePath'] ?? '').toString();
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 1.5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.notifications_active, color: Colors.purple),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (time.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
time,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (body.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
body,
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 14),
|
||||
),
|
||||
),
|
||||
if (imagePath.isNotEmpty || imageUrl.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: imagePath.isNotEmpty
|
||||
? Image.file(
|
||||
File(imagePath),
|
||||
height: 170,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image.network(
|
||||
imageUrl,
|
||||
height: 170,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
206
lib/views/Dashboard/profile/informations/order_alert_sound.dart
Normal file
206
lib/views/Dashboard/profile/informations/order_alert_sound.dart
Normal file
@@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class OrderAlertSoundPage extends StatefulWidget {
|
||||
const OrderAlertSoundPage({super.key});
|
||||
|
||||
@override
|
||||
State<OrderAlertSoundPage> createState() => _OrderAlertSoundPageState();
|
||||
}
|
||||
|
||||
class _OrderAlertSoundPageState extends State<OrderAlertSoundPage> {
|
||||
static const String _prefsKey = 'order_alert_sound';
|
||||
static const String _defaultSound = 'assets/audio/alert-1.mp3';
|
||||
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
String _selected = _defaultSound;
|
||||
bool _loading = true;
|
||||
|
||||
// Available sounds from assets/audio/ folder
|
||||
final List<_SoundOption> _options = const [
|
||||
_SoundOption(
|
||||
label: 'Alert 1 (Default)',
|
||||
assetPath: 'assets/audio/alert-1.mp3',
|
||||
),
|
||||
_SoundOption(label: 'Alert 2', assetPath: 'assets/audio/alert-2.mp3'),
|
||||
_SoundOption(label: 'Alert 3', assetPath: 'assets/audio/alert-3.mp3'),
|
||||
_SoundOption(label: 'Alert 4', assetPath: 'assets/audio/alert-4.mp3'),
|
||||
_SoundOption(label: 'Alert 5', assetPath: 'assets/audio/alert-5.mp3'),
|
||||
_SoundOption(label: 'Alert 6', assetPath: 'assets/audio/alert-6.mp3'),
|
||||
_SoundOption(label: 'Alert 7', assetPath: 'assets/audio/alert-7.mp3'),
|
||||
_SoundOption(label: 'Alert 8', assetPath: 'assets/audio/alert-8.mp3'),
|
||||
_SoundOption(label: 'Alert 9', assetPath: 'assets/audio/alert-9.mp3'),
|
||||
_SoundOption(label: 'Alert 10', assetPath: 'assets/audio/alert-10.mp3'),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSelection();
|
||||
}
|
||||
|
||||
Future<void> _loadSelection() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final saved = prefs.getString(_prefsKey);
|
||||
setState(() {
|
||||
_selected = (saved != null && saved.isNotEmpty) ? saved : _defaultSound;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveSelection(BuildContext context, String assetPath) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsKey, assetPath);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Order alert sound updated'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _preview(BuildContext context, String assetPath) async {
|
||||
try {
|
||||
await _player.stop();
|
||||
await _player.play(AssetSource(assetPath.replaceFirst('assets/', '')));
|
||||
// Note: AssetSource expects relative to assets/ root; hence replaceFirst
|
||||
} catch (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Preview unavailable for: $assetPath'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70, // :small_blue_diamond: increases app bar height
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
), // :small_blue_diamond: white back arrow
|
||||
onPressed: () {
|
||||
Navigator.pop(context); // goes back to previous screen
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Orders alert Sound',
|
||||
style: TextStyle(
|
||||
fontSize: 26, // :small_blue_diamond: larger font size
|
||||
color: Colors.white, // :small_blue_diamond: white text
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
color: Colors.grey.shade200,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.volume_up, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Current: ${_options.firstWhereOrNull((o) => o.assetPath == _selected)?.label ?? 'Unknown'}',
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Tap a sound to select',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: _options.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final opt = _options[index];
|
||||
final isSelected = _selected == opt.assetPath;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
opt.label,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
leading: Radio<String>(
|
||||
value: opt.assetPath,
|
||||
groupValue: _selected,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() => _selected = value);
|
||||
_saveSelection(context, value);
|
||||
},
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: () => _preview(context, opt.assetPath),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _selected = opt.assetPath);
|
||||
_saveSelection(context, opt.assetPath);
|
||||
},
|
||||
subtitle: isSelected
|
||||
? const Text(
|
||||
'Selected',
|
||||
style: TextStyle(fontSize: 12),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SoundOption {
|
||||
final String label;
|
||||
final String assetPath;
|
||||
const _SoundOption({required this.label, required this.assetPath});
|
||||
}
|
||||
280
lib/views/Dashboard/profile/informations/profile.dart
Normal file
280
lib/views/Dashboard/profile/informations/profile.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/controllers/profile_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Profile extends StatefulWidget {
|
||||
const Profile({super.key});
|
||||
|
||||
@override
|
||||
State<Profile> createState() => _ProfileState();
|
||||
}
|
||||
|
||||
class _ProfileState extends State<Profile> {
|
||||
String _name = '';
|
||||
String _email = '';
|
||||
String _contact = '';
|
||||
String _address = '';
|
||||
final List<Worker> _workers = [];
|
||||
late final ProfileController _profileController =
|
||||
Get.isRegistered<ProfileController>()
|
||||
? Get.find<ProfileController>()
|
||||
: Get.put(ProfileController(), permanent: true);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfile();
|
||||
// Keep in sync with controller
|
||||
_workers.addAll([
|
||||
ever(_profileController.userName, (_) => _assignFromController()),
|
||||
ever(_profileController.userEmail, (_) => _assignFromController()),
|
||||
ever(_profileController.userContact, (_) => _assignFromController()),
|
||||
ever(_profileController.userAddress, (_) => _assignFromController()),
|
||||
]);
|
||||
_profileController.loadFromPrefs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final worker in _workers) {
|
||||
worker.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadProfile() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_name = prefs.getString('user_name') ?? '';
|
||||
_email = prefs.getString('user_email') ?? '';
|
||||
_contact = prefs.getString('contactno') ?? '';
|
||||
_address = prefs.getString('user_address') ?? '';
|
||||
});
|
||||
debugPrint('[PROFILE_DETAILS] Loaded - Name: "$_name", Email: "$_email", Contact: "$_contact"');
|
||||
}
|
||||
|
||||
void _assignFromController() {
|
||||
setState(() {
|
||||
if (_profileController.userName.value.trim().isNotEmpty) {
|
||||
_name = _profileController.userName.value.trim();
|
||||
}
|
||||
if (_profileController.userEmail.value.trim().isNotEmpty) {
|
||||
_email = _profileController.userEmail.value.trim();
|
||||
}
|
||||
if (_profileController.userContact.value.trim().isNotEmpty) {
|
||||
_contact = _profileController.userContact.value.trim();
|
||||
}
|
||||
if (_profileController.userAddress.value.trim().isNotEmpty) {
|
||||
_address = _profileController.userAddress.value.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = size.width;
|
||||
// ignore: unused_local_variable
|
||||
final height = size.height;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Profile image
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
// White border circle
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4), // border thickness
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white, // white border
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: Colors.grey.shade400,
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 60,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Name
|
||||
_buildLabel("Enter name", required: true),
|
||||
_buildTextField(
|
||||
hintText: _name.isNotEmpty ? _name : "EX: Vijayan",
|
||||
value: _name.isNotEmpty ? _name : null,
|
||||
readOnly: true,
|
||||
disabled: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Contact No
|
||||
_buildLabel("Contact no", required: true),
|
||||
_buildTextField(
|
||||
hintText: _contact.isNotEmpty
|
||||
? "+91 $_contact"
|
||||
: "EX: +91 8838304677",
|
||||
value: _contact.isNotEmpty ? "+91 $_contact" : null,
|
||||
keyboardType: TextInputType.phone,
|
||||
readOnly: true,
|
||||
disabled: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Email Id
|
||||
_buildLabel("Email Id"),
|
||||
_buildTextField(
|
||||
hintText: _email.isNotEmpty ? _email : "EX: gmail@gmail.com",
|
||||
value: _email.isNotEmpty ? _email : null,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
readOnly: true,
|
||||
disabled: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Location
|
||||
_buildLabel("Address"),
|
||||
_buildTextField(hintText: _address.isNotEmpty ? _address : " EX: R.s puram", value: _address.isNotEmpty ? _address : null, readOnly: true, disabled: true),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleBackNavigation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF5C1D8D), // Purple button color
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Back",
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleBackNavigation() async {
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) {
|
||||
navigator.pop();
|
||||
return;
|
||||
}
|
||||
final rootNavigator = Get.key.currentState;
|
||||
if (rootNavigator != null && rootNavigator.canPop()) {
|
||||
rootNavigator.pop();
|
||||
return;
|
||||
}
|
||||
if (Get.isOverlaysOpen) {
|
||||
Get.back(closeOverlays: true);
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// Text label widget
|
||||
Widget _buildLabel(String text, {bool required = false}) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
children: required
|
||||
? const [
|
||||
TextSpan(
|
||||
text: " *",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Reusable TextField
|
||||
Widget _buildTextField({
|
||||
required String hintText,
|
||||
String? value,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
required bool readOnly,
|
||||
bool disabled = false,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
width: 350,
|
||||
child: TextFormField(
|
||||
keyboardType: keyboardType,
|
||||
readOnly: readOnly,
|
||||
enabled: !disabled,
|
||||
enableInteractiveSelection: false,
|
||||
initialValue: value,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/controllers/rewards_controller.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/rewards_card.dart';
|
||||
|
||||
class RiderRewardsPage extends StatelessWidget {
|
||||
const RiderRewardsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final RewardsController rewardsController = Get.isRegistered<RewardsController>()
|
||||
? Get.find<RewardsController>()
|
||||
: Get.put(RewardsController());
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"REWARDS",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: FontConstants.xxxLarge(context).sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
||||
child: RewardsCard(
|
||||
controller: rewardsController,
|
||||
showFullDetails: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
169
lib/views/Dashboard/profile/informations/saved_address.dart
Normal file
169
lib/views/Dashboard/profile/informations/saved_address.dart
Normal file
@@ -0,0 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
|
||||
class SavedAddressPage extends StatefulWidget {
|
||||
const SavedAddressPage({super.key});
|
||||
|
||||
@override
|
||||
State<SavedAddressPage> createState() => _SavedAddressPageState();
|
||||
}
|
||||
|
||||
class _SavedAddressPageState extends State<SavedAddressPage> {
|
||||
final TextEditingController _addressController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAddress();
|
||||
}
|
||||
|
||||
Future<void> _loadAddress() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final address = (prefs.getString('user_address') ?? '').trim();
|
||||
_addressController.text = address;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_addressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final addressText = _addressController.text.trim();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FB),
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
elevation: 3,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'Saved Address',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: size.width * 0.05, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 🏠 Header Section
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.15),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Icon(
|
||||
Icons.location_on_rounded,
|
||||
color: ColorConstants.primaryColor,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Current Address',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
addressText.isNotEmpty ? addressText : 'No address saved yet.',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey.shade700,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// ✨ Info Section
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline_rounded,
|
||||
color: ColorConstants.primaryColor, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your saved address is used for deliveries, pickups, and nearby service accuracy.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade800,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
503
lib/views/Dashboard/profile/informations/support_ticket.dart
Normal file
503
lib/views/Dashboard/profile/informations/support_ticket.dart
Normal file
@@ -0,0 +1,503 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:nearle/controllers/support_ticket.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class SupportTicket extends StatefulWidget {
|
||||
const SupportTicket({super.key});
|
||||
|
||||
@override
|
||||
State<SupportTicket> createState() => _SupportTicketState();
|
||||
}
|
||||
|
||||
class _SupportTicketState extends State<SupportTicket>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
// Form
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _subjectCtrl = TextEditingController();
|
||||
final _messageCtrl = TextEditingController();
|
||||
String _category = 'Account';
|
||||
String _priority = 'Medium';
|
||||
int _attachmentCount = 0;
|
||||
|
||||
// Image
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
final List<XFile> _attachments = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
_tabController.addListener(() => setState(() {}));
|
||||
Get.put(SupportTicketController());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subjectCtrl.dispose();
|
||||
_messageCtrl.dispose();
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
'Support Ticket',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicatorColor: ColorConstants.primaryColor,
|
||||
labelColor: ColorConstants.primaryColor,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
labelStyle: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'Create Tickets',
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 20),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'My Tickets',
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [_buildCreateForm(), _buildTicketsList()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _tabController.index == 0
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 55,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: _submitTicket,
|
||||
child: Text(
|
||||
'Submit Ticket',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// CREATE FORM
|
||||
// ===============================================
|
||||
Widget _buildCreateForm() {
|
||||
final controller = Get.find<SupportTicketController>();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Create a new support ticket',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tell us what went wrong. We\'ll get back to you as soon as possible.',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey.shade700, height: 1.4, fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Category
|
||||
Text('Category', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
// ignore: deprecated_member_use
|
||||
value: _category,
|
||||
items: ['Account', 'Orders', 'Payments', 'App issue', 'Other']
|
||||
.map((e) => DropdownMenuItem(value: e, child: Text(e, style: TextStyle(fontFamily: FontConstants.fontFamily))))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => _category = v ?? _category),
|
||||
decoration: _inputDecoration(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Priority
|
||||
Text('Priority', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: ['Low', 'Medium', 'High'].map((p) {
|
||||
final selected = _priority == p;
|
||||
return ChoiceChip(
|
||||
label: Text(p, style: TextStyle(fontFamily: FontConstants.fontFamily, fontWeight: FontWeight.w600, fontSize: 16)),
|
||||
selected: selected,
|
||||
selectedColor: ColorConstants.primaryColor.withOpacity(0.15),
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? ColorConstants.primaryColor : Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
onSelected: (_) => setState(() => _priority = p),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Subject
|
||||
Text('Subject', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _subjectCtrl,
|
||||
decoration: _inputDecoration(hint: 'Type Something'),
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter a subject' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Message
|
||||
Text('Describe the issue', style: _labelStyle()),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _messageCtrl,
|
||||
minLines: 5,
|
||||
maxLines: 8,
|
||||
decoration: _inputDecoration(hint: 'Type Something'),
|
||||
style: TextStyle(fontFamily: FontConstants.fontFamily),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter your message' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Attachments
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: _addAttachment,
|
||||
icon: const Icon(Icons.attach_file),
|
||||
label: Text('Add screenshot', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (_attachmentCount > 0)
|
||||
Text('$_attachmentCount attached', style: TextStyle(fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_attachments.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _attachments.asMap().entries.map((entry) {
|
||||
final idx = entry.key;
|
||||
final file = entry.value;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(File(file.path), width: 80, height: 80, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
top: -8,
|
||||
right: -8,
|
||||
child: InkWell(
|
||||
onTap: () => setState(() {
|
||||
_attachments.removeAt(idx);
|
||||
_attachmentCount = _attachments.length;
|
||||
}),
|
||||
child: Container(
|
||||
width: 22,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(color: Colors.black.withOpacity(0.6), shape: BoxShape.circle),
|
||||
child: const Icon(Icons.close, size: 16, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// Submit loading
|
||||
Obx(() => controller.isSubmitting.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// MY TICKETS LIST
|
||||
// ===============================================
|
||||
Widget _buildTicketsList() {
|
||||
final controller = Get.find<SupportTicketController>();
|
||||
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.errorMessage.value.isNotEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Colors.red),
|
||||
const SizedBox(height: 12),
|
||||
Text('Failed to load tickets', style: TextStyle(fontFamily: FontConstants.fontFamily, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 8),
|
||||
Text(controller.errorMessage.value, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600, fontFamily: FontConstants.fontFamily)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(onPressed: controller.fetchTickets, child: const Text('Retry')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.tickets.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
itemCount: controller.tickets.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, i) {
|
||||
final t = controller.tickets[i];
|
||||
final statusColor = _getPriorityColor(t.priority);
|
||||
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 2,
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
title: Text(t.subject, style: TextStyle(fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text('Category: ${t.category} • Priority: ${t.priority}', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
const SizedBox(height: 4),
|
||||
Text('Created: ${_formatDate(t.created)}', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), borderRadius: BorderRadius.circular(20)),
|
||||
child: Text(t.priority, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.support_agent, size: 48, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
Text('No tickets yet', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily)),
|
||||
const SizedBox(height: 8),
|
||||
Text('Create your first ticket from the Create tab.', style: TextStyle(color: Colors.grey.shade700, fontFamily: FontConstants.fontFamily)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// HELPERS
|
||||
// ===============================================
|
||||
TextStyle _labelStyle() => TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily);
|
||||
|
||||
InputDecoration _inputDecoration({String? hint}) {
|
||||
return InputDecoration(
|
||||
hintText: hint,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300)),
|
||||
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5)),
|
||||
hintStyle: TextStyle(fontFamily: FontConstants.fontFamily),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getPriorityColor(String priority) {
|
||||
return switch (priority.toLowerCase()) {
|
||||
'high' => Colors.red,
|
||||
'medium' => Colors.orange,
|
||||
'low' => Colors.green,
|
||||
_ => Colors.grey,
|
||||
};
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// IMAGE PICKER
|
||||
// ===============================================
|
||||
Future<void> _addAttachment() async {
|
||||
final source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: Text('Gallery', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: Text('Camera', style: TextStyle(fontFamily: FontConstants.fontFamily)),
|
||||
onTap: () => Navigator.pop(ctx, ImageSource.camera),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (source == null) return;
|
||||
|
||||
try {
|
||||
if (source == ImageSource.gallery) {
|
||||
final multi = await _picker.pickMultiImage(imageQuality: 85);
|
||||
if (multi.isNotEmpty) {
|
||||
setState(() => _attachments.addAll(multi));
|
||||
} else {
|
||||
final one = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
|
||||
if (one != null) setState(() => _attachments.add(one));
|
||||
}
|
||||
} else {
|
||||
final captured = await _picker.pickImage(source: ImageSource.camera, imageQuality: 85);
|
||||
if (captured != null) setState(() => _attachments.add(captured));
|
||||
}
|
||||
} catch (_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to pick image', style: TextStyle(fontFamily: FontConstants.fontFamily))),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() => _attachmentCount = _attachments.length);
|
||||
}
|
||||
|
||||
// ===============================================
|
||||
// SUBMIT TICKET
|
||||
// ===============================================
|
||||
Future<void> _submitTicket() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
final controller = Get.find<SupportTicketController>();
|
||||
final success = await controller.createTicket(
|
||||
userid: 1242,
|
||||
category: _category,
|
||||
priority: _priority,
|
||||
subject: _subjectCtrl.text.trim(),
|
||||
issue: _messageCtrl.text.trim(),
|
||||
attachments: _attachments.isEmpty ? null : _attachments,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
_subjectCtrl.clear();
|
||||
_messageCtrl.clear();
|
||||
_attachments.clear();
|
||||
_attachmentCount = 0;
|
||||
setState(() {});
|
||||
|
||||
_tabController.animateTo(1);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Ticket Submitted!'),
|
||||
content: const Text('Your ticket has been created and saved. Our team will get back to you soon.'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK')),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to submit ticket: ${controller.errorMessage.value}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
lib/views/Dashboard/profile/informations/terms_condition.dart
Normal file
100
lib/views/Dashboard/profile/informations/terms_condition.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
// ===== Controller =====
|
||||
class TermsController extends GetxController {
|
||||
WebViewController? webViewController;
|
||||
var isLoading = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initializeWebView();
|
||||
}
|
||||
|
||||
void initializeWebView() {
|
||||
webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (url) {
|
||||
isLoading.value = true;
|
||||
print('Started loading: $url');
|
||||
},
|
||||
onPageFinished: (url) {
|
||||
isLoading.value = false;
|
||||
print('Finished loading: $url');
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
isLoading.value = false;
|
||||
print('WebView error: ${error.description}');
|
||||
},
|
||||
),
|
||||
);
|
||||
loadTermsUrl();
|
||||
}
|
||||
|
||||
Future<void> loadTermsUrl() async {
|
||||
if (webViewController != null) {
|
||||
try {
|
||||
await webViewController!.loadRequest(
|
||||
Uri.parse('https://nearle.in/terms'),
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error loading URL: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Page =====
|
||||
class TermsCondition extends StatelessWidget {
|
||||
const TermsCondition({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.put(TermsController());
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
centerTitle: true,
|
||||
toolbarHeight: 70,
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Terms & Conditions',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Obx(() {
|
||||
final wvc = controller.webViewController;
|
||||
return Stack(
|
||||
children: [
|
||||
if (wvc != null) WebViewWidget(controller: wvc),
|
||||
if (controller.isLoading.value)
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
474
lib/views/Dashboard/profile/rewards_card.dart
Normal file
474
lib/views/Dashboard/profile/rewards_card.dart
Normal file
@@ -0,0 +1,474 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/controllers/rewards_controller.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/informations/rider_rewards_page.dart';
|
||||
|
||||
class RewardsCard extends StatelessWidget {
|
||||
final RewardsController controller;
|
||||
final bool showFullDetails;
|
||||
|
||||
const RewardsCard({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.showFullDetails = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Original Rewards Card (The Gradient Card)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!showFullDetails) {
|
||||
Get.to(() => const RiderRewardsPage());
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF2C3E50), // Dark blue/grey
|
||||
Color(0xFF4CA1AF), // Tealish
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Nearle Rewards",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Ride more to get more rewards 🚴",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white70,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
border: Border.all(color: Colors.white30, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.stars_rounded, // Coin-like icon
|
||||
color: Colors.amberAccent,
|
||||
size: 24.sp,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Obx(() {
|
||||
return Text(
|
||||
controller.isLoading.value
|
||||
? "..."
|
||||
: "${controller.totalPoints.value}",
|
||||
style: TextStyle(
|
||||
fontSize: 24.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amberAccent,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Container(
|
||||
padding: EdgeInsets.all(12.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Keep riding correctly to get more points!",
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"Earn 100 points to unlock new rewards",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.white70,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.emoji_events,
|
||||
color: Colors.amber,
|
||||
size: 32.sp,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (showFullDetails) ...[
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// 2. Surprise Gift Section
|
||||
_buildSurpriseGiftCard(),
|
||||
|
||||
SizedBox(height: 24.h),
|
||||
|
||||
// 3. The 4 Cards Section
|
||||
Text(
|
||||
"Redeem Your Points",
|
||||
style: TextStyle(
|
||||
fontSize: 22.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 1: Data Recharge
|
||||
_buildRewardOptionCard(
|
||||
title: "Data Recharge",
|
||||
subtitle: "Get free data for 1 month",
|
||||
points: "300 Points",
|
||||
icon: Icons.wifi,
|
||||
color1: const Color(0xFF11998e),
|
||||
color2: const Color(0xFF38ef7d),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 2: Bonus Fuel
|
||||
_buildRewardOptionCard(
|
||||
title: "Bonus Fuel",
|
||||
subtitle: "Fuel support for your vehicle",
|
||||
points: "600 Points",
|
||||
icon: Icons.local_gas_station,
|
||||
color1: const Color(0xFFFF5F6D),
|
||||
color2: const Color(0xFFFFC371),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 3: Gadgets
|
||||
_buildRewardOptionCard(
|
||||
title: "Gadgets Support",
|
||||
subtitle: "Powerbank or New Mobile support",
|
||||
points: "1000 - 1500 Points",
|
||||
icon: Icons.devices_other,
|
||||
color1: const Color(0xFF2193b0),
|
||||
color2: const Color(0xFF6dd5ed),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Card 4: Vehicle Support
|
||||
_buildRewardOptionCard(
|
||||
title: "Vehicle Support",
|
||||
subtitle: "New vehicle or 50% loan support",
|
||||
points: "2000 Bonus Points",
|
||||
description: "Earn 2000 bonus without any loses in bonus point",
|
||||
icon: Icons.motorcycle,
|
||||
color1: const Color(0xFF8E2DE2),
|
||||
color2: const Color(0xFF4A00E0),
|
||||
isPremium: true,
|
||||
),
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
|
||||
// 4. Bottom Warning / Info Section
|
||||
Container(
|
||||
padding: EdgeInsets.all(16.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.red.shade700, size: 24.sp),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Note: If you miss any deliveries or if requirements are not met for each delivery, negative bonus points will affect your board.",
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color: Colors.red.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSurpriseGiftCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.purple.withOpacity(0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -20,
|
||||
top: -20,
|
||||
child: Icon(
|
||||
Icons.card_giftcard,
|
||||
size: 100.sp,
|
||||
color: Colors.purple.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20.r),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(12.r),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.purple.shade50,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
),
|
||||
child: Icon(Icons.card_giftcard, color: Colors.purple, size: 30.sp),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Monthly Surprise Gift",
|
||||
style: TextStyle(
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
"If you didn't skip any orders in 1 month!",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRewardOptionCard({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required String points,
|
||||
required IconData icon,
|
||||
required Color color1,
|
||||
required Color color2,
|
||||
String? description,
|
||||
bool isPremium = false,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Decorative Background Circle
|
||||
Positioned(
|
||||
right: -30,
|
||||
top: -30,
|
||||
child: Container(
|
||||
width: 120.w,
|
||||
height: 120.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
colors: [color1.withOpacity(0.2), color2.withOpacity(0.0)],
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.topRight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.all(20.r),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [color1, color2],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 24.sp),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade50,
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
border: Border.all(color: Colors.amber.shade200),
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
points,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber.shade900,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
if (description != null) ...[
|
||||
SizedBox(height: 12.h),
|
||||
Container(
|
||||
padding: EdgeInsets.all(10.r),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.star_outline, size: 16.sp, color: Colors.blueGrey),
|
||||
SizedBox(width: 6.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black87,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
587
lib/views/Dashboard/summary/summary.dart
Normal file
587
lib/views/Dashboard/summary/summary.dart
Normal file
@@ -0,0 +1,587 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:nearle/Models/summary/riderweeklykms.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/controllers/summary_controller.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Summary extends StatefulWidget {
|
||||
const Summary({super.key});
|
||||
|
||||
@override
|
||||
State<Summary> createState() => _SummaryState();
|
||||
}
|
||||
|
||||
class _SummaryState extends State<Summary> {
|
||||
final SummaryController controller = Get.put(SummaryController());
|
||||
int _userId = 0;
|
||||
int _refreshTick = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshData();
|
||||
}
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final uid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_userId = uid;
|
||||
});
|
||||
}
|
||||
|
||||
if (uid > 0) {
|
||||
await controller.fetchSummaryStats(uid);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("❌ Error fetching summary: $e");
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_refreshTick++;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// RESPONSIVE CARD
|
||||
// ------------------------
|
||||
Widget _buildCard({
|
||||
required String title,
|
||||
required String value,
|
||||
required String imagePath,
|
||||
bool isCancelled = false,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
border: Border.all(
|
||||
color: isCancelled
|
||||
? const Color(0xFFFF5C5C)
|
||||
: const Color.fromARGB(255, 159, 139, 163),
|
||||
width: 1.2.w,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 14.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
imagePath,
|
||||
height: 36.h,
|
||||
width: 36.w,
|
||||
color: isCancelled
|
||||
? const Color(0xFFFF5C5C)
|
||||
: const Color(0xFF9C27B0),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 30.h),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 29.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// RESPONSIVE CANCELLED CARD
|
||||
// ------------------------
|
||||
Widget _buildCancelledCard(String value) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: const Color.fromARGB(255, 232, 167, 167),
|
||||
width: 1.3.w,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/cancel.png',
|
||||
height: 36.h,
|
||||
width: 36.w,
|
||||
color: const Color(0xFFFF5C5C),
|
||||
),
|
||||
SizedBox(width: 15.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Cancelled Orders',
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontSize: 34.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
// MAIN UI
|
||||
// ------------------------
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
toolbarHeight: 70.h,
|
||||
title: Padding(
|
||||
padding: EdgeInsets.only(top: 12.h),
|
||||
child: Text(
|
||||
"SUMMARY",
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.xxxLarge(context).sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.h),
|
||||
child: Divider(height: 1.h, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
body: Obx(() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.all(16.r),
|
||||
child: Column(
|
||||
children: [
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 12.w,
|
||||
mainAxisSpacing: 12.h,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildCard(
|
||||
title: 'Today',
|
||||
value: controller.today.value.toString(),
|
||||
imagePath: 'assets/images/today.png',
|
||||
),
|
||||
_buildCard(
|
||||
title: 'Week',
|
||||
value: controller.week.value.toString(),
|
||||
imagePath: 'assets/images/week.png',
|
||||
),
|
||||
_buildCard(
|
||||
title: 'Month',
|
||||
value: controller.month.value.toString(),
|
||||
imagePath: 'assets/images/week.png',
|
||||
),
|
||||
_buildCard(
|
||||
title: 'Total',
|
||||
value: controller.total.value.toString(),
|
||||
imagePath: 'assets/images/total.png',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 12.h),
|
||||
_buildCancelledCard(controller.cancelled.value.toString()),
|
||||
SizedBox(height: 12.h),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"Statistics",
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 10.h),
|
||||
TotalDistanceCard(userId: _userId, refreshTick: _refreshTick),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// RESPONSIVE GRAPH CARD
|
||||
// ========================================================
|
||||
class TotalDistanceCard extends StatefulWidget {
|
||||
final int userId;
|
||||
final int refreshTick;
|
||||
|
||||
const TotalDistanceCard({
|
||||
super.key,
|
||||
required this.userId,
|
||||
this.refreshTick = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TotalDistanceCard> createState() => _TotalDistanceCardState();
|
||||
}
|
||||
|
||||
class _TotalDistanceCardState extends State<TotalDistanceCard> {
|
||||
late Future<Map<String, dynamic>> _futureKms;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureKms = _fetchWeeklyKms();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TotalDistanceCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.userId != widget.userId ||
|
||||
oldWidget.refreshTick != widget.refreshTick) {
|
||||
setState(() {
|
||||
_futureKms = _fetchWeeklyKms();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
double _toDouble(dynamic v) {
|
||||
if (v == null) return 0.0;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _fetchWeeklyKms() async {
|
||||
if (widget.userId == 0) {
|
||||
return {'details': <RiderWeeklyKms>[], 'total_kms': 0.0};
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'https://jupiter.nearle.app/live/api/v1/partners/getriderweeklykms?userid=${widget.userId}',
|
||||
);
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
|
||||
if (data is Map && data['status'] == true) {
|
||||
final rawDetails = (data['details'] is List)
|
||||
? data['details'] as List
|
||||
: const [];
|
||||
|
||||
final details = rawDetails
|
||||
.map((e) => RiderWeeklyKms.fromJson(e))
|
||||
.toList();
|
||||
|
||||
final total = _toDouble(data['total_kms']);
|
||||
return {'details': details, 'total_kms': total};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ _fetchWeeklyKms Error: $e');
|
||||
}
|
||||
|
||||
return {'details': <RiderWeeklyKms>[], 'total_kms': 0.0};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
return FutureBuilder<Map<String, dynamic>>(
|
||||
future: _futureKms,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24.h),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.deepPurple),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<RiderWeeklyKms> details =
|
||||
snapshot.data?['details'] ?? <RiderWeeklyKms>[];
|
||||
|
||||
final double totalKms = snapshot.data?['total_kms'] ?? 0.0;
|
||||
|
||||
return _buildDistanceCard(details, totalKms, size);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDistanceCard(
|
||||
List<RiderWeeklyKms> details,
|
||||
double totalKms,
|
||||
Size size,
|
||||
) {
|
||||
final double maxY = details.isEmpty ? 10 : _getMaxY(details);
|
||||
final double chartHeight = (size.height * 0.25).clamp(160.h, 280.h);
|
||||
final double leftInterval = _calculateInterval(maxY);
|
||||
final double maxK = _getMaxKms(details);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: EdgeInsets.only(top: 4.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: const Color.fromARGB(255, 222, 161, 235),
|
||||
width: 1.3.w,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Total Distance",
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${totalKms.toStringAsFixed(2)} Km",
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 30.h),
|
||||
|
||||
if (details.isEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.h),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No weekly data available",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 16.sp),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
height: chartHeight,
|
||||
width: double.infinity,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
maxY: maxY,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) => FlLine(
|
||||
color: Colors.grey.withOpacity(0.12),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 60.w,
|
||||
interval: leftInterval,
|
||||
getTitlesWidget: (value, _) => Text(
|
||||
"${value.toInt()} km",
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) {
|
||||
final idx = value.toInt();
|
||||
if (idx >= 0 && idx < details.length) {
|
||||
return Text(
|
||||
details[idx].day,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Colors.black87,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
barGroups: List.generate(details.isEmpty ? 7 : details.length, (
|
||||
i,
|
||||
) {
|
||||
final kms = details.isEmpty ? 0.0 : details[i].kms.toDouble();
|
||||
|
||||
return BarChartGroupData(
|
||||
x: i,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: kms,
|
||||
color: (details.isNotEmpty && kms == maxK)
|
||||
? const Color(0xFF8124DB)
|
||||
: const Color(0xFFB274F3),
|
||||
width: 20.w,
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
barTouchData: BarTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
tooltipPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 8.h,
|
||||
),
|
||||
getTooltipItem: (group, index, rod, rodIndex) {
|
||||
return BarTooltipItem(
|
||||
"${rod.toY.toStringAsFixed(2)} Km",
|
||||
TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getMaxY(List<RiderWeeklyKms> details) {
|
||||
if (details.isEmpty) return 10;
|
||||
double maxVal = details.map((e) => e.kms).reduce(max);
|
||||
if (maxVal <= 5) return 10;
|
||||
final double withPadding = maxVal * 1.2;
|
||||
return _roundUpNice(withPadding);
|
||||
}
|
||||
|
||||
double _roundUpNice(double v) {
|
||||
final exponent = pow(10, (log(v) / ln10).floor());
|
||||
final mantissa = v / exponent;
|
||||
|
||||
double niceMantissa;
|
||||
if (mantissa <= 1) {
|
||||
niceMantissa = 1;
|
||||
} else if (mantissa <= 2)
|
||||
niceMantissa = 2;
|
||||
else if (mantissa <= 5)
|
||||
niceMantissa = 5;
|
||||
else
|
||||
niceMantissa = 10;
|
||||
|
||||
return (niceMantissa * exponent).ceilToDouble();
|
||||
}
|
||||
|
||||
double _calculateInterval(double maxY) {
|
||||
const int desiredTicks = 5;
|
||||
double rough = max(1, (maxY / desiredTicks));
|
||||
|
||||
final exponent = pow(10, (log(rough) / ln10).floor());
|
||||
final mantissa = rough / exponent;
|
||||
|
||||
double niceMantissa;
|
||||
if (mantissa <= 1) {
|
||||
niceMantissa = 1;
|
||||
} else if (mantissa <= 2)
|
||||
niceMantissa = 2;
|
||||
else if (mantissa <= 5)
|
||||
niceMantissa = 5;
|
||||
else
|
||||
niceMantissa = 10;
|
||||
|
||||
return (niceMantissa * exponent).toDouble();
|
||||
}
|
||||
|
||||
double _getMaxKms(List<RiderWeeklyKms> details) {
|
||||
if (details.isEmpty) return 0;
|
||||
return details.map((e) => e.kms).reduce(max);
|
||||
}
|
||||
}
|
||||
36
lib/views/helpers/constants/Colorconstants.dart
Normal file
36
lib/views/helpers/constants/Colorconstants.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorConstants {
|
||||
static const primaryColor = Color(0xFF662582);
|
||||
static Color? primaryColor1 = const Color(0xFFE7D3EF);
|
||||
static Color? secondaryColor = Colors.white;
|
||||
static Color? ternaryColor = "#E7D3EF".toColor();
|
||||
static Color? darkGreyColor = "575756".toColor();
|
||||
static Color? lightGrey = "b2b2b2".toColor();
|
||||
static Color? lightGreyBg = Colors.grey.shade100;
|
||||
static Color? greenColor = "00b894".toColor();
|
||||
static Color? mintColor = "69c0ac".toColor();
|
||||
static Color restaurantColor = Colors.amber[100]!;
|
||||
static Color groceriesColor = Colors.purple[100]!;
|
||||
static Color shoppingColor = Colors.orange[100]!;
|
||||
static Color healthColor = Colors.cyan[100]!;
|
||||
static Color handymanColor = Colors.red[100]!;
|
||||
static const blueColor = 0xff007AC2;
|
||||
static const redColor = 0xffEF3F42;
|
||||
static const orangeColor = 0xffFAAB53;
|
||||
static Color? lightColor = const Color.fromRGBO(244, 244, 244, 1);
|
||||
}
|
||||
|
||||
extension ColorExtenstion on String {
|
||||
// ignore: body_might_complete_normally_nullable
|
||||
Color? toColor() {
|
||||
var hexColor = replaceAll("#", "");
|
||||
if (hexColor.length == 6) {
|
||||
hexColor = "FF$hexColor";
|
||||
}
|
||||
if (hexColor.length == 8) {
|
||||
return Color(int.parse("0x$hexColor"));
|
||||
}
|
||||
}
|
||||
}
|
||||
111
lib/views/helpers/constants/Font_constant.dart
Normal file
111
lib/views/helpers/constants/Font_constant.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
// ignore: file_names
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FontConstants {
|
||||
static String fontFamily = 'Proxima Nova';
|
||||
|
||||
// Base screen width for scaling (iPhone standard: 375)
|
||||
static const double _baseWidth = 375.0;
|
||||
|
||||
// Base font sizes (for baseWidth = 375)
|
||||
static const double _baseExtraSmall = 10.0;
|
||||
static const double _baseSmall = 12.0;
|
||||
static const double _baseMedium = 14.0;
|
||||
static const double _baseRegular = 16.0;
|
||||
static const double _baseLarge = 20.0;
|
||||
static const double _baseXLarge = 21.0;
|
||||
static const double _baseXXLarge = 22.0;
|
||||
static const double _baseXXXLarge = 24.0;
|
||||
static const double _baseHuge = 30.0;
|
||||
|
||||
/// Get fixed font size (no width scaling) so small & large phones look same.
|
||||
static double getResponsiveFontSize(BuildContext context, double baseSize) {
|
||||
return baseSize;
|
||||
}
|
||||
|
||||
/// Extra Small Text (10px base) - For labels, captions
|
||||
static double extraSmall(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseExtraSmall);
|
||||
|
||||
/// Small Text (12px base) - For small labels, timestamps
|
||||
static double small(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseSmall);
|
||||
|
||||
/// Medium Text (14px base) - For body text, descriptions
|
||||
static double medium(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseMedium);
|
||||
|
||||
/// Regular Text (16px base) - Standard body text, most common
|
||||
static double regular(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseRegular);
|
||||
|
||||
/// Large Text (18px base) - For subheadings, important text
|
||||
static double large(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseLarge);
|
||||
|
||||
/// Extra Large Text (20px base) - For headings, titles
|
||||
static double xLarge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseXLarge);
|
||||
|
||||
/// 2X Large Text (22px base) - For main headings
|
||||
static double xxLarge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseXXLarge);
|
||||
|
||||
/// 3X Large Text (24px base) - For prominent headings
|
||||
static double xxxLarge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseXXXLarge);
|
||||
|
||||
/// Huge Text (26px base) - For hero text, very prominent headings
|
||||
static double huge(BuildContext context) =>
|
||||
getResponsiveFontSize(context, _baseHuge);
|
||||
}
|
||||
|
||||
class ReusableTextWidget extends StatelessWidget {
|
||||
final String text;
|
||||
final double? fontSize;
|
||||
final double? textHeight;
|
||||
final String? fontFamily;
|
||||
final FontWeight? fontWeight;
|
||||
final FontStyle? fontStyle;
|
||||
final Color? color;
|
||||
final TextAlign? textAlign;
|
||||
final int? maxLines;
|
||||
final TextDecoration? isUnderText;
|
||||
|
||||
const ReusableTextWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.fontSize,
|
||||
this.textHeight,
|
||||
this.fontFamily,
|
||||
this.fontWeight,
|
||||
this.fontStyle,
|
||||
this.color,
|
||||
this.textAlign,
|
||||
this.maxLines,
|
||||
this.isUnderText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
softWrap: true,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize ?? FontConstants.medium(context),
|
||||
decoration: isUnderText,
|
||||
fontFamily: fontFamily ?? FontConstants.fontFamily,
|
||||
decorationColor: color,
|
||||
fontWeight: fontWeight ?? FontWeight.normal,
|
||||
fontStyle: fontStyle ?? FontStyle.normal,
|
||||
color: color ?? Colors.grey.shade900,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
decorationStyle: TextDecorationStyle.solid,
|
||||
decorationThickness: 1,
|
||||
height: textHeight,
|
||||
),
|
||||
maxLines: maxLines,
|
||||
textAlign: textAlign ?? TextAlign.start,
|
||||
);
|
||||
}
|
||||
}
|
||||
78
lib/views/helpers/constants/apiconstants.dart
Normal file
78
lib/views/helpers/constants/apiconstants.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
class ApiConstants {
|
||||
static String mainDev = "dev";
|
||||
static String mainRoute = "live";
|
||||
|
||||
//Delivery Queue - v2
|
||||
static String deliveryQueueDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v2/deliveries/getdeliveryqueues";
|
||||
static String deliveryQueueLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v2/deliveries/getdeliveryqueues";
|
||||
|
||||
//Current Delivery - v1
|
||||
static String currentDeliveryDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/deliveries/getdeliveries";
|
||||
static String currentDeliveryLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/deliveries/getdeliveries";
|
||||
|
||||
//Current Delivery V3 - v3 (date-bounded)
|
||||
static String currentDeliveryV3Dev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v3/deliveries/getdeliveries";
|
||||
static String currentDeliveryV3Live =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v3/deliveries/getdeliveries";
|
||||
|
||||
//Update Delivery - v1
|
||||
static String updateDeliveryDev =
|
||||
"https://queue.workolik.com/live/api/v1/deliveries/updatedelivery";
|
||||
static String updateDeliveryLive =
|
||||
"https://queue.workolik.com/live/api/v1/deliveries/updatedelivery";
|
||||
|
||||
//Get Rider Log - v1
|
||||
static String getRiderLogDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/partners/getriderlog";
|
||||
static String getRiderLogLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/partners/getriderlog";
|
||||
|
||||
//Create Rider Log - v2
|
||||
static String createRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
|
||||
static String createRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
|
||||
|
||||
//Update Rider Log - v1
|
||||
static String updateRiderLogDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/partners/updateriderlog";
|
||||
static String updateRiderLogLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/partners/updateriderlog";
|
||||
|
||||
//Get Rider Count - v1
|
||||
static String getRiderCountDev =
|
||||
"https://jupiter.nearle.app/$mainDev/api/v1/partners/getridercount";
|
||||
static String getRiderCountLive =
|
||||
"https://jupiter.nearle.app/$mainRoute/api/v1/partners/getridercount";
|
||||
|
||||
//Create Break Rider Log - v2
|
||||
static String createBreakRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
|
||||
static String createBreakRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
|
||||
|
||||
//Update Break Rider Log - v2
|
||||
static String updateBreakRiderLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
|
||||
static String updateBreakRiderLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
|
||||
|
||||
//Create Delivery Log - v2
|
||||
static String createDeliveryLogDev =
|
||||
"https://queue.workolik.com/live/api/v2/deliveries/createdeliverylog";
|
||||
static String createDeliveryLogLive =
|
||||
"https://queue.workolik.com/live/api/v2/deliveries/createdeliverylog";
|
||||
|
||||
//Summary API - v2
|
||||
static String summaryApiLive =
|
||||
'https://jupiter.nearle.app/$mainRoute/api/v2/partners';
|
||||
|
||||
//Summary Rider Weekly KMs - v1
|
||||
static String summaryriderkmLive =
|
||||
'https://jupiter.nearle.app/$mainRoute/api/v1/partners/getriderweeklykms';
|
||||
}
|
||||
18
lib/views/helpers/constants/mqtt_constants.dart
Normal file
18
lib/views/helpers/constants/mqtt_constants.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
class MqttConstants {
|
||||
static const String brokerHost = '66.116.225.226'; // Updated with VPS IP
|
||||
static const int brokerPort = 1883;
|
||||
static const String username = 'admin';
|
||||
static const String passwordString = 'Package@321#'; // Provided by user
|
||||
|
||||
// Topic Structure
|
||||
static const String topicRiderStatus = 'nearle/riders/{riderId}/status';
|
||||
static const String topicRiderProfile = 'nearle/riders/{riderId}/profile';
|
||||
static const String topicRiderLocation = 'nearle/riders/{riderId}/location';
|
||||
static const String topicRiderTelemetry = 'nearle/riders/{riderId}/telemetry';
|
||||
static const String topicRiderLogs = 'nearle/riders/{riderId}/logs';
|
||||
|
||||
// Status Values
|
||||
static const String statusOnline = 'Online';
|
||||
static const String statusOffline = 'Offline';
|
||||
static const String statusIdle = 'Idle';
|
||||
}
|
||||
140
lib/views/introscreens/intro1.dart
Normal file
140
lib/views/introscreens/intro1.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Intro1 extends GetResponsiveView {
|
||||
Intro1({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
// 🔹 Use `screen.height` and `screen.width` safely here
|
||||
final height = screen.height;
|
||||
final width = screen.width;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
// Make the status bar area white instead of black, with dark icons
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height * 0.12),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
flexibleSpace: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Container(
|
||||
height: height * 0.14,
|
||||
width: width * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomRight: Radius.circular(width * 0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: -height * 0.12,
|
||||
left: -width * 0.1,
|
||||
right: -width * 0.1,
|
||||
child: Container(
|
||||
width: width * 1.2,
|
||||
height: height * 0.28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3EAF9),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width * 0.8),
|
||||
topRight: Radius.circular(width * 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: height * 0.08),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Welcome to',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.1),
|
||||
child: Text(
|
||||
'Nearle !',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Find delivery opportunities anytime,\nanywhere | Earn with ease!',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.022,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.09),
|
||||
child: Image.asset(
|
||||
'assets/images/intro1.png',
|
||||
height: height * 0.35,
|
||||
width: width * 0.75,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/views/introscreens/intro2.dart
Normal file
144
lib/views/introscreens/intro2.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Intro2 extends GetResponsiveView {
|
||||
Intro2({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
final height = screen.height;
|
||||
final width = screen.width;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
// Make the status bar area white instead of black, with dark icons
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height * 0.12),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
flexibleSpace: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
height: height * 0.14,
|
||||
width: width * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(width * 0.25),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Bottom curve
|
||||
Positioned(
|
||||
bottom: -height * 0.12,
|
||||
left: -width * 0.1,
|
||||
right: -width * 0.1,
|
||||
child: Container(
|
||||
width: width * 1.2,
|
||||
height: height * 0.28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3EAF9),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width * 0.8),
|
||||
topRight: Radius.circular(width * 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Center content
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: height * 0.08),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Title
|
||||
Column(
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Orders That',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.1),
|
||||
child: Text(
|
||||
'Find You',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.08),
|
||||
child: Text(
|
||||
'Get assigned deliveries based on your\nlocation for faster and smarter work.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.022,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Image in center
|
||||
Transform.translate(
|
||||
offset: Offset(0, -height * 0.09),
|
||||
child: Image.asset(
|
||||
'assets/images/intro2.png',
|
||||
height: height * 0.35,
|
||||
width: width * 0.75,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
152
lib/views/introscreens/intro3.dart
Normal file
152
lib/views/introscreens/intro3.dart
Normal file
@@ -0,0 +1,152 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
class Intro3 extends GetResponsiveView {
|
||||
final VoidCallback onFinish;
|
||||
|
||||
Intro3({
|
||||
super.key,
|
||||
required this.onFinish,
|
||||
required PageController controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
final height = screen.height;
|
||||
final width = screen.width;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light.copyWith(
|
||||
// Keep status bar transparent over the purple gradient on this screen
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(height * 0.35),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
),
|
||||
flexibleSpace: Container(
|
||||
width: double.infinity,
|
||||
height: height * 0.36,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
ColorConstants.primaryColor,
|
||||
ColorConstants.primaryColor,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(width * 0.49),
|
||||
bottomRight: Radius.circular(width * 0.49),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.06,
|
||||
vertical: height * 0.02,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.06),
|
||||
Text(
|
||||
'Deliver. Earn.\nRepeat.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.045,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
height: 1.2,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.02),
|
||||
Text(
|
||||
'Track your trips, and enjoy \npayouts every week.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.019,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, -height * 0.01),
|
||||
child: Image.asset(
|
||||
'assets/images/intro3.png',
|
||||
height: height * 0.35,
|
||||
width: width * 0.75,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
// Button at bottom
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.08),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: onFinish,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"Start",
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.03),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/views/introscreens/introscreen.dart
Normal file
82
lib/views/introscreens/introscreen.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/introscreens/intro1.dart';
|
||||
import 'package:nearle/views/introscreens/intro2.dart';
|
||||
import 'package:nearle/views/introscreens/intro3.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
|
||||
class Introscreen extends StatefulWidget {
|
||||
const Introscreen({super.key});
|
||||
|
||||
@override
|
||||
State<Introscreen> createState() => _IntroscreenState();
|
||||
}
|
||||
|
||||
class _IntroscreenState extends State<Introscreen> {
|
||||
late final PageController _controller;
|
||||
static const String _prefsHasSeenIntroKey = 'has_seen_intro';
|
||||
|
||||
void _finishOnboarding() {
|
||||
_completeOnboarding();
|
||||
}
|
||||
|
||||
Future<void> _completeOnboarding() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefsHasSeenIntroKey, true);
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted) return;
|
||||
Get.offAll(() => const SignIn());
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PageController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
alignment: Alignment.bottomCenter, // positions the indicator
|
||||
children: [
|
||||
PageView(
|
||||
controller: _controller,
|
||||
children: [
|
||||
Intro1(),
|
||||
Intro2(),
|
||||
Intro3(controller: _controller, onFinish: _finishOnboarding),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
// Move indicator slightly up so it doesn't sit under the bottom curve
|
||||
alignment: const Alignment(0, 0.55),
|
||||
child: SmoothPageIndicator(
|
||||
controller: _controller,
|
||||
count: 3,
|
||||
effect: ExpandingDotsEffect(
|
||||
expansionFactor: 3, // How much the active dot expands
|
||||
dotHeight: 7,
|
||||
dotWidth: 7,
|
||||
spacing: 8,
|
||||
dotColor: Colors.grey,
|
||||
activeDotColor: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
188
lib/views/introscreens/splashscreen.dart
Normal file
188
lib/views/introscreens/splashscreen.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/introscreens/introscreen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
||||
import 'package:nearle/views/updatescreen/UpdateScreen.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:nearle/views/onboardscreens/Mpin.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
|
||||
class Splashscreen extends StatefulWidget {
|
||||
const Splashscreen({super.key});
|
||||
|
||||
@override
|
||||
State<Splashscreen> createState() => _SplashscreenState();
|
||||
}
|
||||
|
||||
class _SplashscreenState extends State<Splashscreen> {
|
||||
late final ImageProvider _logoProvider;
|
||||
bool _imagePrecached = false;
|
||||
bool _hasCheckedUpdate = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_logoProvider = const AssetImage("assets/images/nearlesplash2.png");
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
try {
|
||||
await precacheImage(_logoProvider, context);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imagePrecached = true;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_imagePrecached = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// FAST splash: 1 second
|
||||
Timer(const Duration(seconds: 1), () {
|
||||
if (mounted) {
|
||||
_startNextStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FAST second step: 0.2 sec
|
||||
void _startNextStep() {
|
||||
Timer(const Duration(milliseconds: 200), () {
|
||||
if (mounted && !_hasCheckedUpdate) {
|
||||
_checkForUpdateAndNavigate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdateAndNavigate() async {
|
||||
if (_hasCheckedUpdate) return;
|
||||
_hasCheckedUpdate = true;
|
||||
|
||||
try {
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.nearle.partner",
|
||||
);
|
||||
|
||||
final status = await newVersion.getVersionStatus();
|
||||
|
||||
if (status != null && status.canUpdate) {
|
||||
if (mounted) {
|
||||
Get.offAll(
|
||||
() => UpdateScreen(
|
||||
mCurrentVersion: status.localVersion,
|
||||
mUpdateVersion: status.storeVersion,
|
||||
mIsForceUpdate: true,
|
||||
),
|
||||
transition: Transition.fadeIn,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
_navigateToNextScreen();
|
||||
}
|
||||
|
||||
Future<void> _navigateToNextScreen() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final bool isLoggedOut = prefs.getBool('logged_out') ?? false;
|
||||
final bool hasSeenIntro = prefs.getBool('has_seen_intro') ?? false;
|
||||
final int? savedUserId = prefs.getInt('userid');
|
||||
|
||||
// 🚀 Check for App Update (Force Login if version changed)
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
final lastRunVersion = prefs.getString('last_run_version');
|
||||
|
||||
if (lastRunVersion != null && lastRunVersion != currentVersion) {
|
||||
debugPrint(
|
||||
'[SPLASH] App update detected: $lastRunVersion -> $currentVersion. Forcing re-verification.',
|
||||
);
|
||||
|
||||
final String? savedPhone = prefs.getString('contactno');
|
||||
final int? savedUserId = prefs.getInt('userid');
|
||||
|
||||
// Update stored version
|
||||
await prefs.setString('last_run_version', currentVersion);
|
||||
|
||||
if (savedUserId != null && savedPhone != null && savedPhone.isNotEmpty) {
|
||||
debugPrint('[SPLASH] User was logged in. Redirecting to MPIN page.');
|
||||
|
||||
// Initialize AuthController and set the phone for MPIN verification
|
||||
final auth = Get.put(AuthController());
|
||||
auth.currentPhone = savedPhone;
|
||||
|
||||
// Clear sensitive session data to force re-verification
|
||||
await prefs.remove('userid');
|
||||
await prefs.remove('partnerid');
|
||||
await prefs.remove('onduty');
|
||||
await prefs.setBool('logged_out', true);
|
||||
|
||||
if (mounted) {
|
||||
Get.offAll(() => Mpin());
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
debugPrint('[SPLASH] User was not logged in. Redirecting to Sign In.');
|
||||
if (mounted) {
|
||||
Get.offAll(() => const SignIn());
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save current version for next run
|
||||
await prefs.setString('last_run_version', currentVersion);
|
||||
} catch (e) {
|
||||
debugPrint('[SPLASH] Version check error: $e');
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (!isLoggedOut && savedUserId != null && savedUserId > 0) {
|
||||
final onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
if (onduty == 0) {
|
||||
Get.offAll(() => const SigninBanner());
|
||||
} else {
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
} else {
|
||||
if (hasSeenIntro) {
|
||||
Get.offAll(() => const SignIn());
|
||||
} else {
|
||||
Get.offAll(() => Introscreen());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: _imagePrecached
|
||||
? Image(
|
||||
image: _logoProvider,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/views/offline/offline_page.dart
Normal file
128
lib/views/offline/offline_page.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:nearle/views/introscreens/splashscreen.dart';
|
||||
|
||||
class OfflinePage extends StatelessWidget {
|
||||
const OfflinePage({super.key});
|
||||
|
||||
Future<void> _handleRetry(BuildContext context) async {
|
||||
try {
|
||||
final results = await Connectivity().checkConnectivity();
|
||||
final isOnline = results.isNotEmpty && results.any((r) => r != ConnectivityResult.none);
|
||||
if (isOnline) {
|
||||
// Return to normal app flow; Splashscreen decides login vs home
|
||||
Get.offAll(() => Splashscreen());
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Still offline. Please check your internet connection.'),
|
||||
backgroundColor: Colors.black87,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: -height * 0.12,
|
||||
left: -width * 0.1,
|
||||
right: -width * 0.1,
|
||||
child: Container(
|
||||
width: width * 1.2,
|
||||
height: height * 0.28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3EAF9),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(width * 0.8),
|
||||
topRight: Radius.circular(width * 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.wifi_off,
|
||||
color: ColorConstants.primaryColor,
|
||||
size: 64,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'You are offline',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Please check your internet connection. We\'ll reconnect automatically when you\'re back online.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[700],
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: 180,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () => _handleRetry(context),
|
||||
child: const Text(
|
||||
'Retry',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
322
lib/views/onboardscreens/Creat_mpin.dart
Normal file
322
lib/views/onboardscreens/Creat_mpin.dart
Normal file
@@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:nearle/views/onboardscreens/Mpin.dart';
|
||||
|
||||
class CreateMpin extends GetResponsiveView {
|
||||
CreateMpin({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
return const _CreateMpinBody();
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateMpinBody extends StatefulWidget {
|
||||
const _CreateMpinBody();
|
||||
|
||||
@override
|
||||
State<_CreateMpinBody> createState() => _CreateMpinBodyState();
|
||||
}
|
||||
|
||||
class _CreateMpinBodyState extends State<_CreateMpinBody> {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
final List<TextEditingController> _newMpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<TextEditingController> _confirmMpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
|
||||
final List<FocusNode> _newFocusNodes = List.generate(4, (_) => FocusNode());
|
||||
final List<FocusNode> _confirmFocusNodes = List.generate(
|
||||
4,
|
||||
(_) => FocusNode(),
|
||||
);
|
||||
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_newFocusNodes[0].requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var c in [..._newMpinControllers, ..._confirmMpinControllers]) {
|
||||
c.dispose();
|
||||
}
|
||||
for (var f in [..._newFocusNodes, ..._confirmFocusNodes]) {
|
||||
f.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMpinChange(
|
||||
String value,
|
||||
int index,
|
||||
List<TextEditingController> controllers,
|
||||
List<FocusNode> nodes,
|
||||
) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
nodes[index + 1].requestFocus();
|
||||
} else if (value.isEmpty && index > 0) {
|
||||
nodes[index - 1].requestFocus();
|
||||
}
|
||||
|
||||
final isGroupFilled = controllers.every((c) => c.text.isNotEmpty);
|
||||
if (controllers == _newMpinControllers && isGroupFilled) {
|
||||
_confirmFocusNodes[0].requestFocus();
|
||||
}
|
||||
if (controllers == _confirmMpinControllers && isGroupFilled) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String getMpin(List<TextEditingController> controllers) =>
|
||||
controllers.map((c) => c.text).join();
|
||||
|
||||
bool get isMpinMatched =>
|
||||
getMpin(_newMpinControllers) == getMpin(_confirmMpinControllers);
|
||||
|
||||
bool get isAllFilled => [
|
||||
..._newMpinControllers,
|
||||
..._confirmMpinControllers,
|
||||
].every((c) => c.text.isNotEmpty);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screen = context.width < 600
|
||||
? "mobile"
|
||||
: context.width < 1100
|
||||
? "tablet"
|
||||
: "desktop";
|
||||
|
||||
final height = Get.height;
|
||||
final width = Get.width;
|
||||
|
||||
// Adjust scale based on device
|
||||
final scale = screen == "mobile"
|
||||
? 1.0
|
||||
: screen == "tablet"
|
||||
? 1.3
|
||||
: 1.6;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.06),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: height * 0.04 * scale),
|
||||
|
||||
SizedBox(
|
||||
height: height * 0.25 * scale,
|
||||
width: width * 0.6,
|
||||
child: Image.asset(
|
||||
"assets/images/CreateMpin.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.03 * scale),
|
||||
|
||||
Text(
|
||||
"Create Your MPIN",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.04 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.015 * scale),
|
||||
|
||||
Text(
|
||||
"Enter a 4-digit MPIN and confirm it to secure your account.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.02 * scale,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.04 * scale),
|
||||
|
||||
_buildMpinField(
|
||||
"Enter New MPIN",
|
||||
_newMpinControllers,
|
||||
_newFocusNodes,
|
||||
scale,
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.03 * scale),
|
||||
|
||||
_buildMpinField(
|
||||
"Confirm MPIN",
|
||||
_confirmMpinControllers,
|
||||
_confirmFocusNodes,
|
||||
scale,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: height * 0.05,
|
||||
left: width * 0.04,
|
||||
child: InkWell(
|
||||
onTap: () => Get.back(),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(width * 0.02),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black12,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors.black,
|
||||
size: width * 0.06,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: height * 0.065 * scale,
|
||||
child: ElevatedButton(
|
||||
onPressed: isAllFilled && isMpinMatched && !isLoading
|
||||
? () async {
|
||||
setState(() => isLoading = true);
|
||||
final newPin = getMpin(_newMpinControllers);
|
||||
// Set PIN directly - user ID should already be available from login flow
|
||||
final ok = await _auth.setPin(newPin);
|
||||
setState(() => isLoading = false);
|
||||
if (ok) {
|
||||
// After setting PIN, go to verify PIN page to sign-in with new PIN
|
||||
Get.to(() => Mpin());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Failed',
|
||||
'Unable to set PIN. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(width * 0.03),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: height * 0.03,
|
||||
width: height * 0.03,
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Continue",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.024 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMpinField(
|
||||
String label,
|
||||
List<TextEditingController> controllers,
|
||||
List<FocusNode> focusNodes,
|
||||
double scale,
|
||||
) {
|
||||
final height = Get.height;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.02 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.015 * scale),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) {
|
||||
return SizedBox(
|
||||
width: 50 * scale,
|
||||
height: 55 * scale,
|
||||
child: TextField(
|
||||
controller: controllers[index],
|
||||
focusNode: focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
obscureText: true,
|
||||
maxLength: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.025 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (val) =>
|
||||
_onMpinChange(val, index, controllers, focusNodes),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
343
lib/views/onboardscreens/Mpin.dart
Normal file
343
lib/views/onboardscreens/Mpin.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/views/onboardscreens/otp_page.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Mpin extends GetResponsiveView {
|
||||
Mpin({super.key});
|
||||
|
||||
@override
|
||||
Widget builder() {
|
||||
return const _MpinView();
|
||||
}
|
||||
}
|
||||
|
||||
class _MpinView extends StatefulWidget {
|
||||
const _MpinView();
|
||||
|
||||
@override
|
||||
State<_MpinView> createState() => _MpinViewState();
|
||||
}
|
||||
|
||||
class _MpinViewState extends State<_MpinView> {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
final List<TextEditingController> _mpinControllers = List.generate(
|
||||
4,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
||||
|
||||
bool isVerifying = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNodes[0].requestFocus();
|
||||
_maybeShowMasterPinReminder();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var c in _mpinControllers) {
|
||||
c.dispose();
|
||||
}
|
||||
for (var f in _focusNodes) {
|
||||
f.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onMpinChange(String value, int index) {
|
||||
if (value.isNotEmpty && index < 3) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else if (value.isEmpty && index > 0) {
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
}
|
||||
|
||||
final filled = _mpinControllers.every((c) => c.text.isNotEmpty);
|
||||
if (filled) {
|
||||
FocusScope.of(context).unfocus();
|
||||
_submitMpin(); // auto-verify when 4 digits are filled
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String getMpin() => _mpinControllers.map((c) => c.text).join();
|
||||
bool get isMpinFilled => _mpinControllers.every((c) => c.text.isNotEmpty);
|
||||
|
||||
void _clearMpinAndFocus() {
|
||||
for (final c in _mpinControllers) {
|
||||
c.clear();
|
||||
}
|
||||
if (_focusNodes.isNotEmpty) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _submitMpin() async {
|
||||
if (!mounted || isVerifying || !isMpinFilled) return;
|
||||
|
||||
// Register retry callback so AuthController bottom-sheet "Retry" button
|
||||
// can clear MPIN boxes and bring back keyboard when PIN is wrong.
|
||||
_auth.onPinRetry = _clearMpinAndFocus;
|
||||
|
||||
setState(() => isVerifying = true);
|
||||
final mpin = getMpin();
|
||||
final ok = await _auth.verifyPinWithServer(mpin);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => isVerifying = false);
|
||||
|
||||
if (ok) {
|
||||
// ✅ Wait a moment to ensure onduty is saved, then read it
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
debugPrint('[MPIN] After verification - onduty=$onduty');
|
||||
|
||||
// Navigate based on onduty value
|
||||
if (onduty == 0) {
|
||||
debugPrint('[MPIN] Navigating to Introscreen (onduty=0)');
|
||||
Get.offAll(() => SigninBanner());
|
||||
} else {
|
||||
debugPrint('[MPIN] Navigating to BottomPage (onduty=1)');
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
} else {
|
||||
// Wrong MPIN: show alert/snackbar and keep user on MPIN screen
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _maybeShowMasterPinReminder() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final forceMasterPin =
|
||||
prefs.getBool(AuthController.forceMasterPinPrefKey) ?? false;
|
||||
if (forceMasterPin) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Use ${AuthController.masterPinValue} as PIN to continue.',
|
||||
),
|
||||
duration: const Duration(seconds: 4),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final height = size.height;
|
||||
final width = size.width;
|
||||
|
||||
double scale = 1.0;
|
||||
if (Get.width < 380) scale = 0.9;
|
||||
if (Get.width > 800) scale = 1.2;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.05),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
SizedBox(
|
||||
height: height * 0.25 * scale,
|
||||
width: width * 0.6,
|
||||
child: Image.asset(
|
||||
"assets/images/Mpin.png",
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
Text(
|
||||
"Enter Your MPIN",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.05 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.01),
|
||||
|
||||
Text(
|
||||
"Access your account securely",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.024 * scale,
|
||||
color: Colors.black54,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.06),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) {
|
||||
final isFilled = _mpinControllers[index].text.isNotEmpty;
|
||||
return SizedBox(
|
||||
width: 50 * scale,
|
||||
height: 55 * scale,
|
||||
|
||||
child: RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
|
||||
onKey: (event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.backspace &&
|
||||
_mpinControllers[index].text.isEmpty &&
|
||||
index > 0) {
|
||||
_mpinControllers[index - 1].clear();
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
|
||||
child: TextField(
|
||||
controller: _mpinControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
textAlign: TextAlign.center,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 1,
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.028 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isFilled ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: "",
|
||||
contentPadding: EdgeInsets.zero,
|
||||
filled: true,
|
||||
fillColor: isFilled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.transparent,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF662582),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (value) => _onMpinChange(value, index),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.02),
|
||||
|
||||
// Retry + Forget Pin row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Clear all MPIN boxes and focus first, bringing up keyboard
|
||||
for (final c in _mpinControllers) {
|
||||
c.clear();
|
||||
}
|
||||
if (_focusNodes.isNotEmpty) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Text(
|
||||
"",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.022 * scale,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
await _auth.sendOtp();
|
||||
Get.to(OtpPage());
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: Offset(-18, 0),
|
||||
child: Text(
|
||||
"Forget Pin?",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.025 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: height * 0.05,
|
||||
left: width * 0.04,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Get.to(SignIn());
|
||||
},
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(width * 0.02),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.black12,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors.black,
|
||||
size: width * 0.06,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
339
lib/views/onboardscreens/Sign_in.dart
Normal file
339
lib/views/onboardscreens/Sign_in.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:nearle/views/onboardscreens/otp_page.dart';
|
||||
import 'package:nearle/views/onboardscreens/Mpin.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:nearle/providers/notifications/notificationservce.dart';
|
||||
|
||||
class LoginController extends GetxController {
|
||||
var isChecked = true.obs;
|
||||
final AuthController auth = Get.put(AuthController());
|
||||
}
|
||||
|
||||
class SignIn extends StatefulWidget {
|
||||
const SignIn({super.key});
|
||||
|
||||
@override
|
||||
State<SignIn> createState() => _SignInState();
|
||||
}
|
||||
|
||||
class _SignInState extends State<SignIn> {
|
||||
final LoginController controller = Get.put(LoginController());
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final FocusNode _phoneFocusNode = FocusNode();
|
||||
|
||||
bool isPhoneValid = false;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
FocusScope.of(context).requestFocus(_phoneFocusNode);
|
||||
// Request notification permission the first time Sign In screen is shown
|
||||
NotificationServce.initialize(context);
|
||||
});
|
||||
}
|
||||
|
||||
bool _validatePhoneNumber(String number) {
|
||||
final RegExp regExp = RegExp(r'^[6-9]\d{9}$');
|
||||
return regExp.hasMatch(number);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneFocusNode.dispose();
|
||||
phoneController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final height = size.height;
|
||||
final width = size.width;
|
||||
double scale = 1.0;
|
||||
if (width < 380) scale = 0.9;
|
||||
if (width > 800) scale = 1.2;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.white,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
left: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.05,
|
||||
vertical: height * 0.02,
|
||||
),
|
||||
child: GetBuilder<LoginController>(
|
||||
builder: (_) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.04),
|
||||
// Restore hero image at the top like before
|
||||
Image.asset(
|
||||
'assets/images/Nearle Bike.png',
|
||||
height: height * 0.28,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: height * 0.03),
|
||||
Text(
|
||||
"Sign In",
|
||||
style: TextStyle(
|
||||
fontSize: height * 0.05 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorConstants.primaryColor,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.02),
|
||||
Text(
|
||||
"Enter your mobile number to get started.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: FontConstants.xLarge(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.05),
|
||||
|
||||
// Phone number field
|
||||
SizedBox(
|
||||
height: height * 0.07,
|
||||
width: width * 0.9,
|
||||
child: TextField(
|
||||
controller: phoneController,
|
||||
focusNode: _phoneFocusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 10,
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
isPhoneValid = _validatePhoneNumber(value);
|
||||
});
|
||||
|
||||
if (value.length == 10 && isPhoneValid) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
labelText: 'Enter mobile number',
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: width * 0.04,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: width * 0.02,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
"assets/images/in.png",
|
||||
height: height * 0.045,
|
||||
width: width * 0.09,
|
||||
),
|
||||
SizedBox(width: width * 0.01),
|
||||
Text(
|
||||
"+91",
|
||||
style: TextStyle(
|
||||
fontSize: FontConstants.large(context),
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF662582),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Validation message
|
||||
if (!isPhoneValid && phoneController.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: width * 0.02,
|
||||
top: height * 0.005,
|
||||
),
|
||||
child: Text(
|
||||
'Enter a valid 10-digit mobile number',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade700,
|
||||
fontSize: width * 0.03,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: height * 0.02),
|
||||
|
||||
// Terms text with clickable T&C and Privacy Policy
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
children: [
|
||||
const TextSpan(text: 'By continuing, you agree to '),
|
||||
TextSpan(
|
||||
text: 'T&C',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final uri = Uri.parse(
|
||||
'https://nearle.in/terms',
|
||||
);
|
||||
final ok = await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!ok) {
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.inAppWebView,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const TextSpan(text: ' and '),
|
||||
TextSpan(
|
||||
text: 'Privacy Policy',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
final uri = Uri.parse(
|
||||
'https://nearle.in/privacy',
|
||||
);
|
||||
final ok = await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
if (!ok) {
|
||||
await launchUrl(
|
||||
uri,
|
||||
mode: LaunchMode.inAppWebView,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.02),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Button
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(width * 0.04),
|
||||
child: SizedBox(
|
||||
height: height * 0.065,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isPhoneValid && !isLoading
|
||||
? () async {
|
||||
setState(() => isLoading = true);
|
||||
final decision = await controller.auth.precheckPhone(
|
||||
phoneController.text,
|
||||
);
|
||||
setState(() => isLoading = false);
|
||||
if (decision == AuthNext.notRegistered) {
|
||||
return;
|
||||
} else if (decision == AuthNext.otp) {
|
||||
await controller.auth.sendOtp(phoneController.text);
|
||||
Get.to(OtpPage());
|
||||
} else if (decision == AuthNext.verifyPin) {
|
||||
Get.to(() => Mpin());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Unable to proceed. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isPhoneValid
|
||||
? const Color(0xFF662582)
|
||||
: Colors.grey.shade400,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: height * 0.015),
|
||||
),
|
||||
child: isLoading
|
||||
? SizedBox(
|
||||
height: height * 0.035,
|
||||
width: height * 0.035,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Next',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: width * 0.06,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
395
lib/views/onboardscreens/otp_page.dart
Normal file
395
lib/views/onboardscreens/otp_page.dart
Normal file
@@ -0,0 +1,395 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
|
||||
import 'package:nearle/views/onboardscreens/Sign_in.dart';
|
||||
import 'package:nearle/controllers/auth.dart';
|
||||
import 'package:nearle/views/onboardscreens/Creat_mpin.dart';
|
||||
import 'package:sms_autofill/sms_autofill.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class OtpPage extends GetResponsiveView {
|
||||
OtpPage({super.key});
|
||||
|
||||
@override
|
||||
Widget? phone() => _OtpPageLayout();
|
||||
@override
|
||||
Widget? tablet() => _OtpPageLayout(scale: 1.2);
|
||||
@override
|
||||
Widget? desktop() => _OtpPageLayout(scale: 1.3);
|
||||
}
|
||||
|
||||
class _OtpPageLayout extends StatefulWidget {
|
||||
final double scale;
|
||||
const _OtpPageLayout({this.scale = 1.0});
|
||||
|
||||
@override
|
||||
State<_OtpPageLayout> createState() => _OtpPageLayoutState();
|
||||
}
|
||||
|
||||
class _OtpPageLayoutState extends State<_OtpPageLayout> with CodeAutoFill {
|
||||
final AuthController _auth = Get.put(AuthController());
|
||||
final List<TextEditingController> _otpControllers = List.generate(
|
||||
6,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
|
||||
|
||||
bool isVerifying = false;
|
||||
int _secondsRemaining = 60;
|
||||
Timer? _timer;
|
||||
String _appSignature = '';
|
||||
// Consent fallback removed due to plugin AGP incompatibility
|
||||
int _smsDefaultProvider = 0; // 0 = normal, 1 = passkey provider
|
||||
int? _smsPassKey; // when provider is passkey based
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
_listenForOtp();
|
||||
_loadOtpProviderPrefs();
|
||||
// Ensure cursor starts in first box
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _focusNodes.isNotEmpty) {
|
||||
_focusNodes[0].requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_secondsRemaining = 60;
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (_secondsRemaining > 0) {
|
||||
setState(() => _secondsRemaining--);
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _resendOtp() async {
|
||||
setState(() => _secondsRemaining = 60);
|
||||
_timer?.cancel();
|
||||
_startTimer();
|
||||
await _auth.sendOtp();
|
||||
}
|
||||
|
||||
Future<void> _listenForOtp() async {
|
||||
try {
|
||||
await SmsAutoFill().unregisterListener();
|
||||
listenForCode();
|
||||
// Fetch and cache app hash for SMS retriever compatibility
|
||||
try {
|
||||
final sig = await SmsAutoFill().getAppSignature;
|
||||
if (sig.isNotEmpty) {
|
||||
_appSignature = sig;
|
||||
// Helpful for integrating with SMS provider templates
|
||||
debugPrint('[OTP] App signature hash: $_appSignature');
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {}
|
||||
|
||||
// Consent fallback temporarily disabled; use SMS Retriever with app hash
|
||||
}
|
||||
|
||||
Future<void> _loadOtpProviderPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_smsDefaultProvider = prefs.getInt('smsDefaultProvider') ?? 0;
|
||||
_smsPassKey = prefs.getInt('smsPassKey');
|
||||
if (_smsDefaultProvider == 1 && _smsPassKey != null) {
|
||||
final passKeyStr = _smsPassKey!.toString().padLeft(6, '0');
|
||||
// Prefill only if fields are empty
|
||||
if (mounted && !_otpControllers.any((c) => c.text.isNotEmpty)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
for (int i = 0; i < 6 && i < passKeyStr.length; i++) {
|
||||
_otpControllers[i].text = passKeyStr[i];
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void codeUpdated() {
|
||||
final received = code ?? '';
|
||||
if (received.isNotEmpty) {
|
||||
final digits = received.replaceAll(RegExp(r'\D'), '');
|
||||
if (digits.length >= 6) {
|
||||
final otp = digits.substring(0, 6);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
_otpControllers[i].text = otp[i];
|
||||
}
|
||||
setState(() {});
|
||||
// hide keyboard on autofill and verify with delay before navigation
|
||||
FocusScope.of(context).unfocus();
|
||||
_autoVerify(delayBeforeNav: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _autoVerify({bool delayBeforeNav = false}) async {
|
||||
if (!mounted) return;
|
||||
if (!_otpControllers.every((c) => c.text.isNotEmpty)) return;
|
||||
if (isVerifying) return;
|
||||
setState(() => isVerifying = true);
|
||||
final entered = getOtp();
|
||||
bool ok = false;
|
||||
// Accept provider passkey as valid OTP when enabled
|
||||
if (_smsDefaultProvider == 1 &&
|
||||
_smsPassKey != null &&
|
||||
entered == _smsPassKey!.toString().padLeft(6, '0')) {
|
||||
ok = true;
|
||||
} else {
|
||||
ok = await _auth.verifyOtp(entered);
|
||||
}
|
||||
setState(() => isVerifying = false);
|
||||
if (ok) {
|
||||
if (delayBeforeNav) {
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
}
|
||||
Get.to(() => CreateMpin());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _otpControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
_timer?.cancel();
|
||||
try {
|
||||
cancel();
|
||||
} catch (_) {}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onOtpChange(String value, int index) {
|
||||
if (value.isNotEmpty && index < 5) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else if (value.isNotEmpty && index == 5) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
if (value.isEmpty && index > 0) {
|
||||
// backspace: jump focus back
|
||||
_focusNodes[index - 1].requestFocus();
|
||||
_otpControllers[index - 1].selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _otpControllers[index - 1].text.length,
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String getOtp() => _otpControllers.map((e) => e.text).join();
|
||||
bool get isOtpFilled => _otpControllers.every((c) => c.text.isNotEmpty);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scale = widget.scale;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20 * scale,
|
||||
vertical: 20 * scale,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 60 * scale),
|
||||
Image.asset(
|
||||
'assets/images/verify.png',
|
||||
fit: BoxFit.contain,
|
||||
height: 200 * scale,
|
||||
),
|
||||
SizedBox(height: 24 * scale),
|
||||
Text(
|
||||
"Verify OTP",
|
||||
style: TextStyle(
|
||||
fontSize: 28 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24 * scale),
|
||||
Text(
|
||||
"Enter the 6-digit code sent to your number",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black54,
|
||||
fontSize: 19 * scale,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 40 * scale),
|
||||
|
||||
// OTP Fields (6 boxes)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(6, (index) {
|
||||
final isFilled = _otpControllers[index].text.isNotEmpty;
|
||||
return SizedBox(
|
||||
width: 45 * scale,
|
||||
height: 50 * scale,
|
||||
child: TextField(
|
||||
controller: _otpControllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
autofocus: index == 0,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 17 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isFilled ? Colors.white : Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: isFilled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.transparent,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: const BorderSide(
|
||||
color: Colors.grey,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: ColorConstants.primaryColor,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8 * scale),
|
||||
),
|
||||
),
|
||||
onChanged: (value) => _onOtpChange(value, index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(height: 16 * scale),
|
||||
|
||||
// Resend OTP
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _secondsRemaining == 0
|
||||
? () async {
|
||||
await _resendOtp();
|
||||
}
|
||||
: null,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-10, 0),
|
||||
child: Text(
|
||||
_secondsRemaining == 0
|
||||
? "Resend OTP"
|
||||
: "Resend in 00:${_secondsRemaining.toString().padLeft(2, '0')}",
|
||||
style: TextStyle(
|
||||
fontSize: 18 * scale,
|
||||
color: _secondsRemaining == 0
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.black,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Back button
|
||||
Positioned(
|
||||
top: 40 * scale,
|
||||
left: 16 * scale,
|
||||
child: InkWell(
|
||||
onTap: () => Get.to(() => SignIn()),
|
||||
borderRadius: BorderRadius.circular(30 * scale),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(8 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black12,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_back,
|
||||
color: Colors.black,
|
||||
size: 28 * scale,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
bottomNavigationBar: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
child: SizedBox(
|
||||
height: 55 * scale,
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isOtpFilled && !isVerifying
|
||||
? () async {
|
||||
// Reuse common verification flow (same as auto-verify)
|
||||
await _autoVerify();
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isOtpFilled
|
||||
? ColorConstants.primaryColor
|
||||
: Colors.grey.shade400,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10 * scale),
|
||||
),
|
||||
),
|
||||
child: isVerifying
|
||||
? SizedBox(
|
||||
height: 30 * scale,
|
||||
width: 30 * scale,
|
||||
child: const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
"Verify",
|
||||
style: TextStyle(
|
||||
fontSize: 21 * scale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
208
lib/views/onboardscreens/signin_banner.dart
Normal file
208
lib/views/onboardscreens/signin_banner.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:slider_button_lite/feature/presentation/slider_button/slider.dart';
|
||||
import 'package:slider_button_lite/feature/presentation/slider_button/slider_button_prop.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/controllers/riderlog.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
|
||||
class SigninBanner extends StatefulWidget {
|
||||
const SigninBanner({super.key});
|
||||
|
||||
@override
|
||||
State<SigninBanner> createState() => _SigninBannerState();
|
||||
}
|
||||
|
||||
class _SigninBannerState extends State<SigninBanner> {
|
||||
String _shiftText = 'Your shift: -';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// ✅ CRITICAL: Load shift info in background, don't block UI
|
||||
_loadShiftInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadShiftInfo() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
final s = (prefs.getString('starttime') ?? '').trim();
|
||||
final e = (prefs.getString('endtime') ?? '').trim();
|
||||
_shiftText = (s.isEmpty || e.isEmpty)
|
||||
? 'Your shift: -'
|
||||
: 'Your shift: $s – $e';
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Keep default text on error
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final width = size.width;
|
||||
final height = size.height;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.symmetric(horizontal: width * 0.01),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: height * 0.02),
|
||||
|
||||
Text(
|
||||
"Welcome Back!",
|
||||
style: TextStyle(
|
||||
fontSize: width * 0.07,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: const Color(0xFF6A1B9A),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: height * 0.01),
|
||||
|
||||
Text(
|
||||
"Start your ride and make today amazing!",
|
||||
style: TextStyle(
|
||||
fontSize: width * 0.04,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
CircleAvatar(
|
||||
radius: width * 0.14,
|
||||
backgroundColor: const Color(0xFFEDE7F6),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: const Color(0xFF6A1B9A),
|
||||
size: width * 0.12,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.015),
|
||||
|
||||
// ✅ CRITICAL: Show shift text immediately (no FutureBuilder blocking)
|
||||
Text(
|
||||
_shiftText,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: width * 0.04,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: height * 0.04),
|
||||
|
||||
Image.asset('assets/images/signin_banner.png'),
|
||||
SizedBox(height: height * 0.05),
|
||||
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final sliderWidth = constraints.maxWidth;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SliderButton(
|
||||
properties: SliderButtonProperties(
|
||||
height: height * 0.07,
|
||||
width: sliderWidth,
|
||||
buttonSize: height * 0.065,
|
||||
disable: false,
|
||||
isLoading: false,
|
||||
backgroundColor: const Color(0xFF6A1B9A),
|
||||
disableButtonColor: const Color(0xFFCCCCDD),
|
||||
dismissThresholds: 0.9,
|
||||
action: () async {
|
||||
try {
|
||||
final rlc = Get.find<RiderLogController>();
|
||||
// Ensure any previous break is ended when coming online
|
||||
debugPrint(
|
||||
'[SIGNIN_BANNER] Ending break before going online',
|
||||
);
|
||||
final breakEnded = await rlc
|
||||
.endBreakAuto()
|
||||
.timeout(
|
||||
const Duration(seconds: 12),
|
||||
onTimeout: () => false,
|
||||
);
|
||||
debugPrint(
|
||||
'[SIGNIN_BANNER] endBreakAuto -> $breakEnded',
|
||||
);
|
||||
// Set rider ON duty (onduty = 1)
|
||||
final ok = await rlc.setOnDuty(true);
|
||||
if (ok && context.mounted) {
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
// Show snackbar if still on this screen (unlikely after navigation)
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
ok
|
||||
? "You're now on duty"
|
||||
: "Failed to update status",
|
||||
),
|
||||
backgroundColor: ok
|
||||
? Colors.deepPurple
|
||||
: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[SIGNIN_BANNER] Error updating status: $e',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
label: Text(
|
||||
'Slide to Start',
|
||||
style: TextStyle(
|
||||
fontSize: width * 0.05,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
alignLabel: Alignment.center,
|
||||
icon: ClipOval(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
child: SizedBox(
|
||||
width: height * 0.065,
|
||||
height: height * 0.065,
|
||||
child: const Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: Color(0xFF6A1B9A),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: height * 0.04),
|
||||
// Add extra bottom padding for devices with navigation bars
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/views/onboardscreens/splashscreen.dart
Normal file
46
lib/views/onboardscreens/splashscreen.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/introscreens/introscreen.dart';
|
||||
|
||||
|
||||
class Splashscreen extends StatefulWidget {
|
||||
const Splashscreen({super.key});
|
||||
|
||||
@override
|
||||
State<Splashscreen> createState() => _SplashscreenState();
|
||||
}
|
||||
|
||||
class _SplashscreenState extends State<Splashscreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Timer(const Duration(seconds: 3), () {
|
||||
Get.to(() => Introscreen());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
elevation: 0,
|
||||
),
|
||||
backgroundColor: ColorConstants.secondaryColor,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
|
||||
children: [
|
||||
SizedBox(height: 230,),
|
||||
Center(child: Image.asset("assets/images/splashimg.png",fit: BoxFit.contain,height:180,width: 180,)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
264
lib/views/updatescreen/UpdateScreen.dart
Normal file
264
lib/views/updatescreen/UpdateScreen.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
import 'dart:io';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:new_version_plus/new_version_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:nearle/views/introscreens/introscreen.dart';
|
||||
import 'package:nearle/widget/Bottom_page.dart';
|
||||
import 'package:nearle/views/onboardscreens/signin_banner.dart';
|
||||
|
||||
class UpdateScreen extends StatefulWidget {
|
||||
final bool mIsForceUpdate;
|
||||
final String mCurrentVersion;
|
||||
final String mUpdateVersion;
|
||||
|
||||
const UpdateScreen({
|
||||
super.key,
|
||||
this.mIsForceUpdate = true,
|
||||
required this.mCurrentVersion,
|
||||
required this.mUpdateVersion,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UpdateScreen> createState() => _UpdateScreenState();
|
||||
}
|
||||
|
||||
class _UpdateScreenState extends State<UpdateScreen>
|
||||
with WidgetsBindingObserver {
|
||||
bool _isCheckingVersion = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && !_isCheckingVersion) {
|
||||
checkVersionAndNavigate();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkVersionAndNavigate() async {
|
||||
if (_isCheckingVersion) return;
|
||||
_isCheckingVersion = true;
|
||||
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
final newVersion = NewVersionPlus(
|
||||
iOSId: '284882215',
|
||||
androidId: "com.nearle.partner",
|
||||
);
|
||||
|
||||
final status = await newVersion.getVersionStatus();
|
||||
|
||||
if (status != null) {
|
||||
if (!status.canUpdate) {
|
||||
if (mounted) {
|
||||
_navigateToNextScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error checking version: $e");
|
||||
} finally {
|
||||
_isCheckingVersion = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _navigateToNextScreen() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final isLoggedOut = prefs.getBool('logged_out') == true;
|
||||
final savedUserId = prefs.getInt('userid');
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (!isLoggedOut && savedUserId != null && savedUserId > 0) {
|
||||
final onduty = prefs.getInt('onduty') ?? 0;
|
||||
|
||||
if (onduty == 0) {
|
||||
Get.offAll(() => const SigninBanner());
|
||||
} else {
|
||||
Get.offAll(() => const BottomPage());
|
||||
}
|
||||
} else {
|
||||
Get.offAll(() => Introscreen());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double h = Get.height;
|
||||
double w = Get.width;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (widget.mIsForceUpdate) {
|
||||
SystemNavigator.pop();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
systemOverlayStyle: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarBrightness: Brightness.light,
|
||||
),
|
||||
),
|
||||
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
bottom: false, // Bottom safe area handled in bottomNavigationBar
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
/// top spacing (responsive)
|
||||
SizedBox(height: h * 0.06),
|
||||
|
||||
/// 🚀 Image (auto scales)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: w * 0.08),
|
||||
child: Image.asset(
|
||||
"assets/images/update.png",
|
||||
height: h * 0.28,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: h * 0.03),
|
||||
|
||||
/// 🔥 Title
|
||||
Text(
|
||||
"A New Update is Available!",
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.025,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: h * 0.015),
|
||||
|
||||
/// 📄 Subtitle
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: w * 0.08),
|
||||
child: Text(
|
||||
"New features are here to make your app experience even smoother and more user-friendly!",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.018,
|
||||
color: Colors.grey,
|
||||
height: 1.4,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: h * 0.03),
|
||||
|
||||
/// Version Text
|
||||
Text(
|
||||
"Available version: ${widget.mUpdateVersion}",
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.017,
|
||||
color: Colors.grey,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
|
||||
Spacer(), // pushes content for large screens
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
/// ⭐ Bottom Button (fixed, responsive) with bottom safe area
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
bottom: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
w * 0.06,
|
||||
0,
|
||||
w * 0.06,
|
||||
h * 0.03,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: h * 0.065,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: ColorConstants.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: () => downloadActions(),
|
||||
child: Text(
|
||||
"Update Now",
|
||||
style: TextStyle(
|
||||
fontSize: h * 0.022,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void downloadActions() async {
|
||||
String url;
|
||||
var s = Platform.isAndroid ? "Android" : "Ios";
|
||||
|
||||
if (s == "Android") {
|
||||
url = 'https://play.google.com/store/apps/details?id=com.nearle.partner';
|
||||
} else {
|
||||
url = 'https://apps.apple.com/us/app/nearle/id1596895375ls=1';
|
||||
}
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw 'Could not launch App';
|
||||
}
|
||||
}
|
||||
}
|
||||
125
lib/widget/Bottom_page.dart
Normal file
125
lib/widget/Bottom_page.dart
Normal file
@@ -0,0 +1,125 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nearle/views/Dashboard/deliveries/deliveries.dart';
|
||||
import 'package:nearle/views/Dashboard/home/homepage.dart';
|
||||
import 'package:nearle/views/Dashboard/summary/summary.dart';
|
||||
import 'package:nearle/views/Dashboard/profile/Profilepage.dart';
|
||||
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:nearle/views/helpers/constants/Font_constant.dart';
|
||||
class BottomPage extends StatefulWidget {
|
||||
final int initialIndex;
|
||||
final Widget? overridePage;
|
||||
final List<String> acceptedOrders;
|
||||
const BottomPage({
|
||||
super.key,
|
||||
this.initialIndex = 0,
|
||||
this.overridePage,
|
||||
this.acceptedOrders = const [],
|
||||
});
|
||||
@override
|
||||
State<BottomPage> createState() => _BottomPageState();
|
||||
}
|
||||
class _BottomPageState extends State<BottomPage> {
|
||||
late int selected;
|
||||
late final List<Widget> _pages;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selected = widget.initialIndex;
|
||||
_pages = [
|
||||
widget.overridePage ?? const Homepage(),
|
||||
const MyDeliveries(),
|
||||
const Summary(), // Removed Cartpage - active deliveries now shown in deliveries page banner
|
||||
const ProfilePage(),
|
||||
];
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
textTheme: Theme.of(
|
||||
context,
|
||||
).textTheme.apply(fontFamily: FontConstants.fontFamily),
|
||||
),
|
||||
child: Scaffold(
|
||||
body: IndexedStack(index: selected, children: _pages),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black12,
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: BottomNavigationBar(
|
||||
backgroundColor: Colors.white,
|
||||
currentIndex: selected,
|
||||
onTap: (index) => setState(() => selected = index),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedItemColor: ColorConstants.primaryColor,
|
||||
unselectedItemColor: Colors.grey,
|
||||
selectedFontSize: 14,
|
||||
unselectedFontSize: 12,
|
||||
showUnselectedLabels: true,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: ImageIcon(
|
||||
AssetImage("assets/images/homeicon.png"),
|
||||
size: 30,
|
||||
color: Colors.grey,
|
||||
),
|
||||
activeIcon: ImageIcon(
|
||||
AssetImage("assets/images/selecthome.png"),
|
||||
size: 30,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
label: 'HOME',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: ImageIcon(
|
||||
AssetImage("assets/images/deliveryicon.png"),
|
||||
size: 30,
|
||||
color: Colors.grey,
|
||||
),
|
||||
activeIcon: ImageIcon(
|
||||
AssetImage("assets/images/selecteddelivery.png"),
|
||||
size: 30,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
label: 'DELIVERIES',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: ImageIcon(
|
||||
AssetImage("assets/images/summary.png"),
|
||||
size: 30,
|
||||
color: Colors.grey,
|
||||
),
|
||||
activeIcon: ImageIcon(
|
||||
AssetImage("assets/images/selectedsummary.png"),
|
||||
size: 30,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
label: 'SUMMARY',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: ImageIcon(
|
||||
AssetImage("assets/images/profileicon.png"),
|
||||
size: 30,
|
||||
color: Colors.grey,
|
||||
),
|
||||
activeIcon: ImageIcon(
|
||||
AssetImage("assets/images/selectedprofile.png"),
|
||||
size: 30,
|
||||
color: ColorConstants.primaryColor,
|
||||
),
|
||||
label: 'PROFILE',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user