Miler rider app: surface system, visible design language, backend lifecycle

Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

File diff suppressed because it is too large Load Diff

View 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,
),
),
),
),
),
),
),
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,284 @@
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
// Was `doormilelauncher.png`, which is not in this repo — the
// same file ships here under the name below, and the scratch
// card was rendering an exception instead of its cover.
image: Image.asset(
'assets/images/doormile_icon.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;
}
}

View File

@@ -0,0 +1,891 @@
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> {
DoormileMapController? _mapController;
late final LatLng _pickupLocation;
late final LatLng _dropLocation;
List<DoormileMapMarker> _markers = const [];
List<DoormileMapPolyline> _polylines = const [];
bool _isNavigating = false; // Prevent multiple clicks
@override
void initState() {
super.initState();
_resolveLocationsFromDelivery();
_setMarkers();
_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;
}
Future<void> _setMarkers() async {
final pickupIcon = await MapMarkerIcons.pin(Colors.red);
final dropIcon = await MapMarkerIcons.pin(Colors.green);
if (!mounted) return;
setState(() {
_markers = [
DoormileMapMarker(
id: 'pickup',
position: _pickupLocation,
icon: pickupIcon,
),
DoormileMapMarker(id: 'drop', position: _dropLocation, icon: dropIcon),
];
});
}
Future<void> _createRealRoute() async {
final route = await RoutingService.getRoute(
origin: _pickupLocation,
destination: _dropLocation,
);
if (!mounted) return;
// Fall back to a straight line so the screen still shows the connection
// when routing is unavailable.
final points = (route != null && route.points.isNotEmpty)
? route.points
: <LatLng>[_pickupLocation, _dropLocation];
if (route == null) debugPrint('[MAP_PREVIEW] No route found');
setState(() {
_polylines = [
DoormileMapPolyline(
id: 'real_route',
points: points,
color: ColorConstants.primaryColor,
width: 6,
),
];
});
_fitMapToRoute();
}
Future<void> _fitMapToRoute() async {
final controller = _mapController;
if (!mounted || controller == null) return;
await controller.fitBounds(
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),
),
),
padding: 80,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
SizedBox.expand(
child: DoormileMap(
initialCameraPosition: CameraPosition(
target: _pickupLocation,
zoom: 14.5,
),
markers: _markers,
polylines: _polylines,
onMapReady: (controller) {
_mapController = controller;
_fitMapToRoute();
},
),
),
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(
'doormile/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,
),
),
],
);
}
}

View File

@@ -0,0 +1,40 @@
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),
),
);
}
}

View File

@@ -0,0 +1,743 @@
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> {
DoormileMapController? mapController;
List<DoormileMapMarker> markers = const [];
List<DoormileMapPolyline> polylines = const [];
LatLng? currentLocation;
bool _isLoading = true;
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 {
final List<DoormileMapMarker> 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 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 MapMarkerIcons.numberedCircle(
displayStep,
primaryColor: ColorConstants.primaryColor,
isSkipped: isSkipped,
isNext: isNext,
);
tempMarkers.add(
DoormileMapMarker(
id: 'delivery_$orderId',
position: LatLng(lat, lon),
icon: icon,
anchor: MarkerAnchor.center,
onTap: () {
_showCustomerDetailsSheet(delivery, displayStep);
},
),
);
stepPoints.add(_StepPoint(displayStep, LatLng(lat, lon)));
}
if (currentLocation != null) {
tempMarkers.add(
DoormileMapMarker(
id: 'current_location',
position: currentLocation!,
icon: await MapMarkerIcons.dot(Colors.blueAccent),
anchor: MarkerAnchor.center,
),
);
stepPoints.insert(0, _StepPoint(0, currentLocation!));
}
stepPoints.sort((a, b) => a.step.compareTo(b.step));
final newPolylines = await _buildRoutePolylines(stepPoints);
if (mounted) {
setState(() {
markers = tempMarkers;
polylines = newPolylines;
});
}
await Future.delayed(const Duration(milliseconds: 200));
_fitBoundsToMarkersAndPolylines();
}
/// Builds the route through every stop in one Directions call.
///
/// This used to issue one request per consecutive pair of stops, so opening
/// this screen with N stops cost N billable requests. OSRM takes every stop
/// as a coordinate in one request, so the whole route is a single call.
Future<List<DoormileMapPolyline>> _buildRoutePolylines(
List<_StepPoint> stepPoints,
) async {
if (stepPoints.length < 2) return const [];
final route = await RoutingService.getRoute(
origin: stepPoints.first.position,
destination: stepPoints.last.position,
waypoints: stepPoints
.sublist(1, stepPoints.length - 1)
.map((s) => s.position)
.toList(),
);
if (route != null && route.points.isNotEmpty) {
return [
DoormileMapPolyline(
id: 'route',
points: route.points,
width: 6,
color: Colors.blue,
),
];
}
debugPrint('[CUSTOMER_MAP] Routing unavailable, drawing direct segments');
return [
DoormileMapPolyline(
id: 'route_fallback',
points: stepPoints.map((s) => s.position).toList(),
width: 4,
color: Colors.blue.shade200,
),
];
}
Future<void> _fitBoundsToMarkersAndPolylines() async {
final controller = mapController;
if (controller == null) return;
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
bool hasPoint = false;
void include(LatLng pos) {
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 m in markers) {
include(m.position);
}
for (final poly in polylines) {
for (final pos in poly.points) {
include(pos);
}
}
if (!hasPoint) return;
await controller.fitBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
padding: 80,
);
}
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())
: DoormileMap(
initialCameraPosition: initialCameraPosition,
markers: markers,
polylines: polylines,
myLocationEnabled: true,
onMapReady: (controller) {
mapController = controller;
_fitBoundsToMarkersAndPolylines();
},
),
);
}
}

File diff suppressed because it is too large Load Diff

View 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,
),
),
],
),
),
),
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,430 @@
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,
),
),
),
),
],
),
);
},
),
);
},
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,145 @@
// Active Delivery Banner Widget for Home Page
// ignore_for_file: unused_import
import 'package:flutter/material.dart';
import 'package:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'package:miler/xpress/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,
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,636 @@
import 'package:flutter/material.dart';
import 'package:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'dart:async';
import 'package:miler/xpress/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,
),
),
),
),
);
}
}

View 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:miler/xpress/views/Dashboard/profile/informations/help_center.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/profile.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/saved_address.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/faq.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/notifications_page.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/support_ticket.dart';
import 'package:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'package:miler/xpress/views/helpers/constants/Font_constant.dart';
import 'package:miler/views/onboardscreens/Sign_in.dart';
import 'package:miler/xpress/controllers/profile_controller.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/order_alert_sound.dart';
import 'package:miler/xpress/controllers/rewards_controller.dart';
import 'package:miler/xpress/views/Dashboard/profile/rewards_card.dart';
import 'package:miler/xpress/views/Dashboard/profile/informations/rider_rewards_page.dart';
import 'package:miler/xpress/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');
}
DoormileMqttService().disconnect();
prefs.setBool('logged_out', true);
Get.offAll(() => const SignIn());
},
child: Text(
"Yes",
style: TextStyle(
fontSize: 19.sp,
color: Colors.black,
fontFamily: FontConstants.fontFamily,
),
),
),
],
),
),
);
},
);
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:miler/xpress/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://doormile.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),
],
);
}),
);
}
}

View File

@@ -0,0 +1,456 @@
import 'package:flutter/material.dart';
import 'package:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'package:miler/xpress/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 Doormile 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 Doormile Xpress?',
initiallyExpanded: true,
child: const Text(
'Doormile 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,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,204 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/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'] ?? 'Doormile').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,
),
),
),
],
),
),
);
},
),
),
);
}
}

View 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:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'package:miler/xpress/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});
}

View File

@@ -0,0 +1,287 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:miler/xpress/controllers/profile_controller.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/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),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:miler/xpress/views/helpers/constants/Font_constant.dart';
import 'package:miler/xpress/controllers/rewards_controller.dart';
import 'package:miler/xpress/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,
),
),
),
);
}
}

View File

@@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/views/helpers/constants/Font_constant.dart';
import 'package:miler/xpress/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(),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,659 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:miler/xpress/controllers/support_ticket.dart';
import 'package:miler/xpress/views/helpers/constants/Colorconstants.dart';
import 'package:miler/xpress/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,
),
);
}
}
}

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:miler/xpress/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://doormile.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),
],
);
}),
),
);
}
}

View File

@@ -0,0 +1,492 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:miler/xpress/controllers/rewards_controller.dart';
import 'package:miler/xpress/views/helpers/constants/Font_constant.dart';
import 'package:miler/xpress/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(
"Doormile 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,
),
),
),
],
),
),
],
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,590 @@
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:miler/xpress/Models/summary/riderweeklykms.dart';
import 'package:miler/xpress/views/helpers/constants/Font_constant.dart';
import 'package:miler/xpress/controllers/summary_controller.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/data/delivery_api.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: [
// `assets/images/cancel.png` came across with the Xpress port and
// does not exist in this repo, so this card threw on every build for
// an Xpress tenant — a red exception box where the cancelled count
// should be. Drawn instead of loaded: the glyph is the same mark at
// the same size and cannot go missing.
Icon(
Icons.cancel_rounded,
size: 36.h,
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('${DeliveryApi.weeklyKms}?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);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
/// The delivery line's palette.
///
/// ── Why these three values changed and nothing else did ──
///
/// The delivery screens are a verbatim port of the Xpress-rider app: same
/// layout, same components, same 4-tab bottom bar. What was *not* kept is the
/// brand. Xpress-rider was purple (#662582) with a lilac tint (#E7D3EF), and a
/// rider signing in to Doormile should not be looking at another company's
/// colour — the whole point of the port is that both lines are Doormile.
///
/// So the primary and its two tints are now the Doormile red the parcel line
/// uses (`lib/views/helpers/constants/Colorconstants.dart`, `primary`), and
/// every other entry below is untouched. Those others are semantic — greens for
/// success, ambers for categories — and carry no brand.
class ColorConstants {
/// Doormile red. Was #662582, Xpress-rider's purple.
static const primaryColor = Color(0xFF960019);
/// The 8%-ish wash behind selected rows and icon badges. Was #E7D3EF, the
/// lilac derived from the purple; now the red's equivalent, so a tinted
/// surface still reads as "the brand, quietly" rather than as a second hue.
static Color? primaryColor1 = const Color(0xFFF6E0E2);
static Color? secondaryColor = Colors.white;
/// Same wash as [primaryColor1], reached through the hex-string helper the
/// ported screens use on a handful of call sites.
static Color? ternaryColor = "#F6E0E2".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"));
}
}
}

View 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,
);
}
}

View File

@@ -0,0 +1,81 @@
import 'package:miler/xpress/data/delivery_api.dart';
/// The delivery line's endpoint names, kept as a thin façade over [DeliveryApi].
///
/// ── Why this class still exists ──
///
/// The ported screens reference `ApiConstants.currentDeliveryLive` and friends
/// on roughly thirty call sites, most of them inside 1000-line `part` files that
/// were brought over verbatim. Renaming those call sites would mean editing the
/// ported flow, which is the one thing this port was meant not to do.
///
/// So the names survive and the values now resolve to Doormile routes. Every
/// URL — including the ones that used to be inline in `homepage.dart`,
/// `cartpage.dart`, `deliveries.dart` and `summary.dart` — is defined once, in
/// [DeliveryApi], with a LIVE / MAPPED / ABSENT marker saying whether the route
/// exists on the backend today. Read that file before trusting any call here.
///
/// ── The dev/live pairs collapsed ──
///
/// The old backend had a `dev` and a `live` deployment selected by a string
/// (`mainRoute`), so every endpoint came in two. Doormile has one API and one
/// base URL, so both members of each pair now return the same value. They are
/// kept only so the ported call sites — which branch on
/// `ApiConstants.mainRoute == 'live'` — keep compiling and land on the same
/// place either way.
class ApiConstants {
/// Retained because ported call sites branch on it. Both branches now resolve
/// to the same Doormile route.
static String mainDev = "dev";
static String mainRoute = "live";
//Delivery Queue — MAPPED to /miler/assignments
static String get deliveryQueueDev => DeliveryApi.deliveryQueue;
static String get deliveryQueueLive => DeliveryApi.deliveryQueue;
//Current Delivery — MAPPED to /miler/bookings
static String get currentDeliveryDev => DeliveryApi.currentDeliveries;
static String get currentDeliveryLive => DeliveryApi.currentDeliveries;
//Current Delivery V3 (was date-bounded) — MAPPED to /miler/bookings
static String get currentDeliveryV3Dev => DeliveryApi.currentDeliveries;
static String get currentDeliveryV3Live => DeliveryApi.currentDeliveries;
//Update Delivery — ABSENT, see DeliveryApi.updateDelivery
static String get updateDeliveryDev => DeliveryApi.updateDelivery;
static String get updateDeliveryLive => DeliveryApi.updateDelivery;
//Get Rider Log — LIVE
static String get getRiderLogDev => DeliveryApi.getRiderLog;
static String get getRiderLogLive => DeliveryApi.getRiderLog;
//Create Rider Log — LIVE
static String get createRiderLogDev => DeliveryApi.createRiderLog;
static String get createRiderLogLive => DeliveryApi.createRiderLog;
//Update Rider Log — LIVE (same collection, PUT semantics at the call site)
static String get updateRiderLogDev => DeliveryApi.createRiderLog;
static String get updateRiderLogLive => DeliveryApi.createRiderLog;
//Get Rider Count — ABSENT on Doormile; nearest real read is duty status.
static String get getRiderCountDev => DeliveryApi.currentDuty;
static String get getRiderCountLive => DeliveryApi.currentDuty;
//Create Break Rider Log — LIVE
static String get createBreakRiderLogDev => DeliveryApi.startBreak;
static String get createBreakRiderLogLive => DeliveryApi.startBreak;
//Update Break Rider Log — LIVE
static String get updateBreakRiderLogDev => DeliveryApi.endBreak;
static String get updateBreakRiderLogLive => DeliveryApi.endBreak;
//Create Delivery Log — ABSENT, see DeliveryApi.createDeliveryLog
static String get createDeliveryLogDev => DeliveryApi.createDeliveryLog;
static String get createDeliveryLogLive => DeliveryApi.createDeliveryLog;
//Summary — LIVE (/miler/earnings)
static String get summaryApiLive => DeliveryApi.earnings;
//Summary Rider Weekly KMs — ABSENT, see DeliveryApi.weeklyKms
static String get summaryriderkmLive => DeliveryApi.weeklyKms;
}

View File

@@ -0,0 +1,19 @@
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 = 'doormile/riders/{riderId}/status';
static const String topicRiderProfile = 'doormile/riders/{riderId}/profile';
static const String topicRiderLocation = 'doormile/riders/{riderId}/location';
static const String topicRiderTelemetry =
'doormile/riders/{riderId}/telemetry';
static const String topicRiderLogs = 'doormile/riders/{riderId}/logs';
// Status Values
static const String statusOnline = 'Online';
static const String statusOffline = 'Offline';
static const String statusIdle = 'Idle';
}