713 lines
24 KiB
Dart
713 lines
24 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
|
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
|
import 'package:miler/views/helpers/constants/design_constants.dart';
|
|
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
|
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// REVIEW & PAY — the last screen before a shipment exists.
|
|
///
|
|
/// ```
|
|
/// verify parcel ──▶ shipment desk ──▶ [ REVIEW & PAY ] ──▶ order created
|
|
/// everything on
|
|
/// one screen
|
|
/// ```
|
|
///
|
|
/// ── Why this replaced a review, a payment page and a confirm sheet ──
|
|
///
|
|
/// The rider used to close a logistics stop across four screens: the verify
|
|
/// page's own review, then the shipment desk, then a confirm sheet, then a
|
|
/// payment page. Three of those asked him to look at the same facts again, and
|
|
/// the one that took the money was the furthest from the one that priced it —
|
|
/// so the number he quoted the customer and the number he collected were two
|
|
/// screens apart with no way to see them together.
|
|
///
|
|
/// This is the single review: what is being shipped, where from, where to, what
|
|
/// it weighs, what it costs, and how the customer is paying — then one button
|
|
/// that says the amount out loud. **`Pay ₹150`**, not `Confirm`. A rider taking
|
|
/// cash on a doorstep should never have to remember what the total was.
|
|
///
|
|
/// ── The button is the commit ──
|
|
///
|
|
/// Pressing it does three things in the order the contract requires: records
|
|
/// the payment, then posts `pickup-complete`, which converts the booking into a
|
|
/// live consignment with a tracking number. Only then does the screen change.
|
|
/// If any of it fails the rider stays here with the reason — nothing is
|
|
/// reported as created that was not created.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/// How the customer paid. The wire values the payment endpoint expects.
|
|
enum ShipmentPayMethod { cash, upi }
|
|
|
|
extension ShipmentPayMethodX on ShipmentPayMethod {
|
|
String get label => switch (this) {
|
|
ShipmentPayMethod.cash => 'Cash',
|
|
ShipmentPayMethod.upi => 'UPI',
|
|
};
|
|
|
|
IconData get icon => switch (this) {
|
|
ShipmentPayMethod.cash => LucideIcons.banknote,
|
|
ShipmentPayMethod.upi => LucideIcons.qrCode,
|
|
};
|
|
|
|
/// Lower-case, matching what `UpdatePickupProvider.submitPayment` maps from.
|
|
String get wireName => name;
|
|
}
|
|
|
|
class ShipmentReviewPage extends StatefulWidget {
|
|
/// The stop being closed.
|
|
final Map<String, dynamic> pickup;
|
|
|
|
/// What the shipment desk captured — from, to, weight, zone, price.
|
|
final Map<String, dynamic> shipment;
|
|
|
|
/// Takes the money and creates the order. Returns null on success, or a
|
|
/// message to show the rider when it could not be completed.
|
|
///
|
|
/// Injected rather than called directly so this page stays a widget: the
|
|
/// caller owns the provider, the geofence and the controller.
|
|
final Future<String?> Function(ShipmentPayMethod method, double amount)
|
|
onConfirm;
|
|
|
|
/// Read once the order exists, for the receipt line.
|
|
final String Function() trackingNo;
|
|
|
|
const ShipmentReviewPage({
|
|
super.key,
|
|
required this.pickup,
|
|
required this.shipment,
|
|
required this.onConfirm,
|
|
required this.trackingNo,
|
|
});
|
|
|
|
@override
|
|
State<ShipmentReviewPage> createState() => _ShipmentReviewPageState();
|
|
}
|
|
|
|
class _ShipmentReviewPageState extends State<ShipmentReviewPage>
|
|
with TickerProviderStateMixin {
|
|
ShipmentPayMethod _method = ShipmentPayMethod.cash;
|
|
bool _busy = false;
|
|
bool _created = false;
|
|
String? _error;
|
|
|
|
// ── Built in initState, deliberately, not as `late final` ──
|
|
//
|
|
// A `late final` controller is only constructed on first read — and on a
|
|
// review the rider abandons, the first read is `dispose()`. Creating a Ticker
|
|
// there looks up `TickerMode` on an element that is already deactivated,
|
|
// which throws while the tree is being torn down. Eager construction is the
|
|
// only ordering that is correct for every exit from this screen.
|
|
late final AnimationController _successController;
|
|
late final Animation<double> _tickScale;
|
|
late final Animation<double> _ringScale;
|
|
late final Animation<double> _copyFade;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_successController = AnimationController(
|
|
vsync: this,
|
|
duration: DesignConstants.motionCelebrate,
|
|
);
|
|
_tickScale = CurvedAnimation(
|
|
parent: _successController,
|
|
// Overshoots and settles — the shape of a stamp coming down, which is
|
|
// what this moment is. A linear tick reads as a loading state that
|
|
// stopped.
|
|
curve: Curves.elasticOut,
|
|
);
|
|
_ringScale = CurvedAnimation(
|
|
parent: _successController,
|
|
curve: const Interval(0, 0.55, curve: Curves.easeOutCubic),
|
|
);
|
|
_copyFade = CurvedAnimation(
|
|
parent: _successController,
|
|
curve: const Interval(0.35, 1, curve: Curves.easeOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_successController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
double get _amount {
|
|
final v = widget.shipment['price'];
|
|
if (v is num) return v.toDouble();
|
|
return double.tryParse('${v ?? ''}') ?? 0;
|
|
}
|
|
|
|
String get _currency => (widget.shipment['currency'] ?? 'INR').toString();
|
|
|
|
/// `₹150` — the figure on the button and in the summary, from one place so
|
|
/// the two can never disagree.
|
|
String get _amountLabel {
|
|
final symbol = _currency == 'INR' ? '₹' : '$_currency ';
|
|
return '$symbol${_amount.toStringAsFixed(0)}';
|
|
}
|
|
|
|
String _s(dynamic v) => (v ?? '').toString().trim();
|
|
|
|
Map<String, dynamic> get _from =>
|
|
(widget.shipment['from'] as Map?)?.cast<String, dynamic>() ?? {};
|
|
Map<String, dynamic> get _to =>
|
|
(widget.shipment['to'] as Map?)?.cast<String, dynamic>() ?? {};
|
|
|
|
Future<void> _confirm() async {
|
|
if (_busy) return;
|
|
setState(() {
|
|
_busy = true;
|
|
_error = null;
|
|
});
|
|
|
|
final failure = await widget.onConfirm(_method, _amount);
|
|
if (!mounted) return;
|
|
|
|
if (failure != null) {
|
|
// Stayed here on purpose. The order does not exist, and a success
|
|
// animation over a failed create is the one thing this screen must never
|
|
// do — the rider would ride off believing a shipment had been raised.
|
|
setState(() {
|
|
_busy = false;
|
|
_error = failure;
|
|
});
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_busy = false;
|
|
_created = true;
|
|
});
|
|
_successController.forward();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopScope(
|
|
// Once the order exists, going back would offer to create it twice.
|
|
canPop: !_created && !_busy,
|
|
child: Scaffold(
|
|
backgroundColor: ColorConstants.daylightSurface,
|
|
appBar: _created ? null : milerPageBar('Review & pay'),
|
|
body: SafeArea(child: _created ? _successView() : _reviewView()),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Review ─────────────────────────────────────────────────────────────
|
|
|
|
Widget _reviewView() {
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
// Full-bleed bands, like the verification screen this page is the
|
|
// end of. The section headers moved inside the band they head — a
|
|
// heading on the page and a card under it puts two left margins on
|
|
// one screen, which is the thing bands exist to stop. See
|
|
// [MilerBand].
|
|
padding: EdgeInsets.only(bottom: 24.h),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_routeCard(),
|
|
SizedBox(height: MilerBand.gap),
|
|
_shipmentCard(),
|
|
SizedBox(height: MilerBand.gap),
|
|
_paymentCard(),
|
|
if (_error != null) ...[
|
|
SizedBox(height: MilerBand.gap),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: MilerBand.pad),
|
|
child: InfoBanner(
|
|
icon: LucideIcons.circleAlert,
|
|
text: _error!,
|
|
color: ColorConstants.errorRed,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
_payBar(),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// From → to, drawn as a journey rather than two labelled fields.
|
|
Widget _routeCard() {
|
|
return MilerBand(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_routeEnd(
|
|
icon: LucideIcons.circleDot,
|
|
color: ColorConstants.primary,
|
|
label: 'FROM',
|
|
address: _s(_from['address']),
|
|
pincode: _s(_from['pincode']),
|
|
),
|
|
// The connector. Reads as one journey, which two stacked address
|
|
// blocks do not.
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 9.w),
|
|
child: SizedBox(
|
|
height: 22.h,
|
|
child: VerticalDivider(
|
|
width: 2.w,
|
|
thickness: 2,
|
|
color: ColorConstants.borderSubtle,
|
|
),
|
|
),
|
|
),
|
|
_routeEnd(
|
|
icon: LucideIcons.mapPin,
|
|
color: ColorConstants.acceptGreen,
|
|
label: 'TO',
|
|
address: _s(_to['address']),
|
|
pincode: _s(_to['pincode']),
|
|
city: _s(_to['city']),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _routeEnd({
|
|
required IconData icon,
|
|
required Color color,
|
|
required String label,
|
|
required String address,
|
|
required String pincode,
|
|
String city = '',
|
|
}) {
|
|
final tail = [
|
|
if (city.isNotEmpty) city,
|
|
if (pincode.isNotEmpty) pincode,
|
|
].join(' · ');
|
|
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 18.sp, color: color),
|
|
SizedBox(width: 10.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 10.5.sp,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.6,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
SizedBox(height: 2.h),
|
|
Text(
|
|
address.isEmpty ? 'Not captured' : address,
|
|
style: TextStyle(
|
|
fontSize: 13.5.sp,
|
|
fontWeight: FontWeight.w600,
|
|
height: 1.3,
|
|
color: address.isEmpty
|
|
? ColorConstants.secondaryText
|
|
: ColorConstants.slateText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
if (tail.isNotEmpty)
|
|
Padding(
|
|
padding: EdgeInsets.only(top: 2.h),
|
|
child: Text(
|
|
tail,
|
|
style: TextStyle(
|
|
fontSize: 11.5.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _shipmentCard() {
|
|
final weight = widget.shipment['weight'];
|
|
final km = widget.shipment['approxkm'];
|
|
|
|
return MilerBand(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SectionHeader(title: 'Shipment'),
|
|
SizedBox(height: 4.h),
|
|
_row('Weight', weight == null ? '—' : '$weight kg'),
|
|
_divider(),
|
|
_row('Category', _s(widget.shipment['category'])),
|
|
_divider(),
|
|
_row('Service', _s(widget.shipment['servicetype'])),
|
|
if (_s(widget.shipment['zone']).isNotEmpty) ...[
|
|
_divider(),
|
|
_row('Zone', _s(widget.shipment['zone'])),
|
|
],
|
|
if (km is num) ...[
|
|
_divider(),
|
|
_row(
|
|
'Distance',
|
|
'≈ ${km.toStringAsFixed(1)} km',
|
|
// Said plainly: it is a straight line between two pins, and the
|
|
// fare comes from the zone. Letting the rider assume otherwise is
|
|
// how a customer gets told the wrong basis for a charge.
|
|
note: 'straight line — the fare uses the zone',
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _paymentCard() {
|
|
return MilerBand(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SectionHeader(title: 'Payment'),
|
|
SizedBox(height: 10.h),
|
|
Row(
|
|
children: [
|
|
for (final m in ShipmentPayMethod.values) ...[
|
|
Expanded(child: _methodTile(m)),
|
|
if (m != ShipmentPayMethod.values.last) SizedBox(width: 8.w),
|
|
],
|
|
],
|
|
),
|
|
SizedBox(height: 14.h),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Amount to collect',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: ColorConstants.slateText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
_amountLabel,
|
|
style: TextStyle(
|
|
fontSize: 20.sp,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: -0.4,
|
|
color: ColorConstants.primary,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _methodTile(ShipmentPayMethod m) {
|
|
final selected = _method == m;
|
|
return GestureDetector(
|
|
onTap: _busy ? null : () => setState(() => _method = m),
|
|
child: AnimatedContainer(
|
|
duration: DesignConstants.motionState,
|
|
padding: EdgeInsets.symmetric(vertical: 12.h),
|
|
decoration: BoxDecoration(
|
|
color: selected
|
|
? ColorConstants.tint(ColorConstants.primary, 0.08)
|
|
: ColorConstants.daylightSurface,
|
|
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
|
border: Border.all(
|
|
color: selected
|
|
? ColorConstants.primary
|
|
: ColorConstants.borderSubtle,
|
|
width: selected ? 1.6 : 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
m.icon,
|
|
size: 20.sp,
|
|
color: selected
|
|
? ColorConstants.primary
|
|
: ColorConstants.secondaryText,
|
|
),
|
|
SizedBox(height: 5.h),
|
|
Text(
|
|
m.label,
|
|
style: TextStyle(
|
|
fontSize: 12.5.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: selected
|
|
? ColorConstants.primary
|
|
: ColorConstants.slateText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// The commit. Names the amount, because that is the fact the rider is acting
|
|
/// on and the one he will be asked for at the door.
|
|
Widget _payBar() {
|
|
return Container(
|
|
padding: EdgeInsets.fromLTRB(16.w, 10.h, 16.w, 12.h),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.pureSurface,
|
|
border: Border(
|
|
top: BorderSide(color: ColorConstants.borderSubtle, width: 1),
|
|
),
|
|
boxShadow: DesignConstants.shadowSm,
|
|
),
|
|
child: PrimaryButton(
|
|
label: _amount > 0
|
|
? 'Pay $_amountLabel'
|
|
// A prepaid or zero-rated shipment still has to be created; the
|
|
// button says what it does rather than asking for money nobody owes.
|
|
: 'Create order',
|
|
loading: _busy,
|
|
onPressed: _busy ? null : _confirm,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _row(String label, String value, {String? note}) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 7.h),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 12.5.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
value.isEmpty ? '—' : value,
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w700,
|
|
color: ColorConstants.slateText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (note != null)
|
|
Padding(
|
|
padding: EdgeInsets.only(top: 2.h),
|
|
child: Text(
|
|
note,
|
|
style: TextStyle(
|
|
fontSize: 10.5.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _divider() => Divider(height: 1.h, color: ColorConstants.borderSubtle);
|
|
|
|
// ── Success ────────────────────────────────────────────────────────────
|
|
//
|
|
// One moment, three beats, staggered so they read as a sequence rather than
|
|
// a screen that swapped: the ring opens, the tick stamps down inside it, and
|
|
// the words arrive last. All of it drives off one controller, so the timing
|
|
// cannot drift.
|
|
|
|
Widget _successView() {
|
|
final tracking = widget.trackingNo();
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 24.w),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Spacer(),
|
|
AnimatedBuilder(
|
|
animation: _successController,
|
|
builder: (_, _) => SizedBox(
|
|
width: 132.w,
|
|
height: 132.w,
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
// The ring, which expands and fades — a single ripple, not a
|
|
// loop. A repeating pulse would keep asking for attention on
|
|
// a screen whose job is finished.
|
|
Transform.scale(
|
|
scale: 0.6 + (_ringScale.value * 0.55),
|
|
child: Opacity(
|
|
opacity: (1 - _ringScale.value).clamp(0.0, 1.0) * 0.35,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: ColorConstants.acceptGreen,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Transform.scale(
|
|
scale: _tickScale.value.clamp(0.0, 1.2),
|
|
child: Container(
|
|
width: 92.w,
|
|
height: 92.w,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: ColorConstants.acceptGreen,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: ColorConstants.acceptGreen.withValues(
|
|
alpha: 0.28,
|
|
),
|
|
blurRadius: 24,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
LucideIcons.check,
|
|
size: 46.sp,
|
|
color: ColorConstants.onAccent,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 26.h),
|
|
FadeTransition(
|
|
opacity: _copyFade,
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'Your order has been created',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 19.sp,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: -0.4,
|
|
color: ColorConstants.slateText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
_amount > 0
|
|
? '$_amountLabel collected by ${_method.label}'
|
|
: 'Shipment created',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: ColorConstants.secondaryText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
// The receipt. Real data from `pickup-complete`, so a customer
|
|
// asking "what's my tracking number?" gets an answer on the
|
|
// doorstep instead of a phone call to the hub.
|
|
if (tracking.isNotEmpty) ...[
|
|
SizedBox(height: 18.h),
|
|
Container(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 16.w,
|
|
vertical: 10.h,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.daylightSurface,
|
|
borderRadius: BorderRadius.circular(
|
|
DesignConstants.radiusFull,
|
|
),
|
|
border: Border.all(color: ColorConstants.borderSubtle),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
LucideIcons.truck,
|
|
size: 15.sp,
|
|
color: ColorConstants.secondaryText,
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
tracking,
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.3,
|
|
color: ColorConstants.slateText,
|
|
fontFamily: FontConstants.fontFamily,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const Spacer(),
|
|
FadeTransition(
|
|
opacity: _copyFade,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(bottom: 16.h),
|
|
child: PrimaryButton(
|
|
label: 'Done',
|
|
onPressed: () => Navigator.of(context).pop(<String, dynamic>{
|
|
// The shape `map.dart` already reads, so the handoff screen,
|
|
// the compliance stamping and the next-stop list all keep
|
|
// working untouched.
|
|
'outcome': 'completed',
|
|
'isDelivery': false,
|
|
'paymentMethod': _method.label,
|
|
'amountCollected': _amount,
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|