part of 'pickups.dart'; Future _showMyOptionsSheet( BuildContext context, Map pickup, _MyPickupsState? parentState, ) async { int selected = -1; bool isLoading = false; final List 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 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( 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, ), ], ), ), ); }, ), ); }, ); }