Files
2026-08-11 13:16:33 +05:30

406 lines
18 KiB
Dart

part of 'pickups.dart';
Future<void> _showMyOptionsSheet(
BuildContext context,
Map<String, dynamic> pickup,
_MyPickupsState? parentState,
) async {
int selected = -1;
bool isLoading = false;
final List<String> skipReasons = [
'Customer unreachable',
'Customer not at the location',
];
await showModalBottomSheet(
context: context,
sheetAnimationStyle: kMilerSheetStyle,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: StatefulBuilder(
builder: (context, setState) {
// The reason list is the same question Update Status asks, so it is
// the same widget — see [_ReasonRow]. It used to be two outlined
// white cards on a white sheet, each with its icon inside a second
// box, and the selected one swapped its border for a thicker
// coloured one.
Widget reasonRow(String title, int index, IconData icon) {
return _ReasonRow(
label: title,
icon: icon,
accent: ColorConstants.primary,
selected: selected == index,
// No rule either side of the filled row: a hairline across the
// tint turns the chosen answer back into a box.
showRule:
index > 0 && selected != index && selected != index - 1,
onTap: isLoading
? null
: () => setState(() => selected = index),
);
}
Future<void> handleConfirm() async {
if (selected == -1 || isLoading) return;
final dc = Get.put(PickupsController(), permanent: true);
final skipStatus = await dc.checkSkipStatus();
final int skipCount = skipStatus['count'] ?? 0;
bool applyPenalty = false;
if (skipCount >= 2) {
final bool? confirm = await showAppDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
),
elevation: 0,
backgroundColor: ColorConstants.pureSurface,
child: Padding(
padding: EdgeInsets.all(24.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: ColorConstants.errorLight,
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_amber_rounded,
color: ColorConstants.errorRed,
size: 36.sp,
),
),
SizedBox(height: 20.h),
Text(
'Skip Limit Reached',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.onSurface,
),
),
SizedBox(height: 10.h),
Text(
'You have exceeded the limit of 2 skips within 3 hours.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15.sp,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.onSurfaceVariant,
height: 1.4,
),
),
SizedBox(height: 14.h),
Container(
padding: EdgeInsets.all(12.r),
// Fill, no outline — the same rule the sheets
// now follow. A tinted panel is already a panel.
decoration: BoxDecoration(
color: ColorConstants.warningContainer,
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: ColorConstants.warning,
size: 18.sp,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
"Proceeding will forfeit your bonus points for this session.",
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.rewardAmber,
fontFamily: FontConstants.fontFamily,
height: 1.3,
),
),
),
],
),
),
SizedBox(height: 24.h),
Row(
children: [
Expanded(
child: MilerButton(
label: "Cancel",
onPressed: () =>
Navigator.pop(context, false),
variant: MilerButtonVariant.outlined,
color: ColorConstants.onSurfaceVariant,
height: ButtonSizes.secondary,
),
),
SizedBox(width: 12.w),
Expanded(
child: MilerButton(
label: "Confirm skip",
onPressed: () =>
Navigator.pop(context, true),
color: ColorConstants.errorRed,
height: ButtonSizes.secondary,
),
),
],
),
],
),
),
);
},
);
if (confirm != true) return;
applyPenalty = true;
}
setState(() => isLoading = true);
try {
final dc = Get.put(PickupsController(), permanent: true);
final d = pickup;
final int pickupId = int.tryParse('${d['pickupid'] ?? 0}') ?? 0;
final int orderHeaderId =
int.tryParse('${d['orderheaderid'] ?? 0}') ?? 0;
final String reason =
selected >= 0 && selected < skipReasons.length
? skipReasons[selected]
: '';
if (pickupId > 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 pickupId=$pickupId orderHeaderId=$orderHeaderId reason="$reason"',
);
final ok = await dc.updateSkippedStatus(
pickupId: pickupId,
orderHeaderId: orderHeaderId,
ridersLat: riderLatStr,
ridersLng: riderLngStr,
notes: reason,
);
debugPrint('[SKIP] Status update result: $ok');
// Optimistic skip — reflect it locally and move on even if the
// API fails (e.g. mock/offline data), so skip never appears
// unresponsive and the rider can proceed to the next stop.
await dc.registerSkip(applyPenalty: applyPenalty);
// Recorded before the sheet closes, and regardless of which
// screen the skip was taken from: the map screen passes no
// `parentState`, so the branch below is the only thing that
// remembered a skip and it did so in memory, on one screen.
// Activity is where the rider now finds a skipped stop, and
// it reads this store. See [addSkippedBooking].
await addSkippedBooking(d, reason: reason);
if (context.mounted) {
Navigator.pop(context);
if (parentState != null) {
parentState.markOrderAsSkipped(d, reason);
} else {
debugPrint(
'[SKIP] Skipped from Nav/Map screen, sheet closed.',
);
}
}
} else {
debugPrint(
'[SKIP] Invalid pickupId: $pickupId or orderHeaderId: $orderHeaderId',
);
if (context.mounted) {
AppFeedback.error(context, 'Invalid pickup information');
}
}
} catch (e) {
debugPrint('[SKIP] Error updating skip status: $e');
if (context.mounted) {
AppFeedback.error(context, 'Something went wrong — try again');
}
} finally {
if (context.mounted) {
setState(() => isLoading = false);
}
}
}
// Frosted, like every other sheet in the app — see
// [milerGlassSheet].
return milerGlassSheet(
child: Padding(
padding: EdgeInsets.fromLTRB(20.w, 12.h, 20.w, 20.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Container(
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: ColorConstants.borderSubtle,
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
),
),
),
SizedBox(height: 16.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Skip this pickup',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 19.sp,
fontWeight: FontWeight.w800,
letterSpacing: -0.3,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.slateText,
),
),
SizedBox(height: 2.h),
Text(
'Tell us why so we can reassign it',
style: TextStyle(
fontSize: 12.5.sp,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
color: ColorConstants.secondaryText,
),
),
],
),
),
SizedBox(width: 10.w),
InkWell(
onTap: isLoading
? null
: () => Navigator.pop(context),
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
child: Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: ColorConstants.neutralLight,
shape: BoxShape.circle,
),
child: Icon(
Icons.close_rounded,
color: ColorConstants.slateText,
size: 18.sp,
),
),
),
],
),
SizedBox(height: 16.h),
// ── Which pickup ──
//
// The sheet asked the rider to skip "this pickup" and never
// said which one. He arrives here from a swipe on a list of
// near-identical rows, and skipping the wrong stop is not an
// action he can take back — it goes to the office as a
// refusal against a customer who was standing there waiting.
_StopBrief(pickup: pickup, showRule: true),
SizedBox(height: 8.h),
reasonRow(
'Customer unreachable',
0,
Icons.phone_missed_rounded,
),
reasonRow(
'Customer not at the location',
1,
Icons.person_off_outlined,
),
SizedBox(height: 20.h),
if (isLoading)
Container(
height: ButtonSizes.primary,
alignment: Alignment.center,
decoration: BoxDecoration(
// Matches the button it stands in for.
color: ColorConstants.errorRed,
borderRadius: BorderRadius.circular(
ButtonSizes.radius,
),
),
child: const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
),
)
else
// ── Skipping is destructive, and looks it ──
//
// This was `primary` while the penalty confirmation
// nested inside the same flow was `errorRed` — one action
// in two colours, two taps apart. Skipping tells the hub
// a customer was not served; it is the destructive branch
// of this screen and takes the destructive colour, the
// same one Reject uses on Home.
MilerButton(
label: 'Confirm skip',
icon: Icons.skip_next_rounded,
color: ColorConstants.errorRed,
onPressed: selected == -1 ? null : handleConfirm,
),
],
),
),
);
},
),
);
},
);
}