From e2341b104f4ff6ad4db4fdf26140dac982139c0a Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 13 Mar 2026 22:43:46 +0300 Subject: [PATCH] 2026-03-13-2 --- .../home/map_passenger_controller.dart | 242 +++++-- lib/controller/local/translations.dart | 11 + .../car_details_widget_to_go.dart | 665 +++++++++++------- .../map_widget.dart/main_bottom_Menu_map.dart | 419 +++++++++-- 4 files changed, 1007 insertions(+), 330 deletions(-) diff --git a/lib/controller/home/map_passenger_controller.dart b/lib/controller/home/map_passenger_controller.dart index 51f8f55..232344b 100644 --- a/lib/controller/home/map_passenger_controller.dart +++ b/lib/controller/home/map_passenger_controller.dart @@ -263,6 +263,14 @@ class MapPassengerController extends GetxController { bool currentLocationToFormPlaces3 = false; bool currentLocationToFormPlaces4 = false; List currentLocationToFormPlacesAll = []; + + // ── Multi-Waypoint (max 2 stops) ────────────────────────────────────────── + List menuWaypoints = [null, null]; + List menuWaypointNames = ['', '']; + int activeMenuWaypointCount = 0; + bool isPickingWaypoint = false; + int pickingWaypointIndex = -1; + late String driverToken = ''; int carsOrder = 0; int wayPointIndex = 0; @@ -4906,6 +4914,82 @@ Intaleq Team'''; update(); } + // ── Multi-Waypoint Methods ────────────────────────────────────────────────── + void addMenuWaypoint() { + if (activeMenuWaypointCount >= 2) return; + activeMenuWaypointCount++; + // Increase expanded bottom menu height to accommodate new waypoint row + mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); + update(); + } + + void removeMenuWaypoint(int index) { + if (index < 0 || index >= 2) return; + // Shift items if removing first waypoint while second exists + if (index == 0 && activeMenuWaypointCount == 2) { + menuWaypoints[0] = menuWaypoints[1]; + menuWaypointNames[0] = menuWaypointNames[1]; + } + menuWaypoints[activeMenuWaypointCount - 1] = null; + menuWaypointNames[activeMenuWaypointCount - 1] = ''; + activeMenuWaypointCount--; + mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); + update(); + } + + void clearAllMenuWaypoints() { + menuWaypoints = [null, null]; + menuWaypointNames = ['', '']; + activeMenuWaypointCount = 0; + isPickingWaypoint = false; + pickingWaypointIndex = -1; + update(); + } + + void startPickingWaypointOnMap(int index) { + pickingWaypointIndex = index; + isPickingWaypoint = true; + isPickerShown = true; + heightPickerContainer = 150; + // Close the expanded menu to show the map picker + isMainBottomMenuMap = true; + mainBottomMenuMapHeight = Get.height * .22; + update(); + } + + void setMenuWaypointFromMap(int index, LatLng position) { + if (index < 0 || index >= 2) return; + menuWaypoints[index] = position; + menuWaypointNames[index] = + '${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}'; + isPickingWaypoint = false; + pickingWaypointIndex = -1; + isPickerShown = false; + // Re-open expanded menu + isMainBottomMenuMap = false; + mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); + update(); + } + + void setMenuWaypointFromSearch(int index, LatLng pos, String name) { + if (index < 0 || index >= 2) return; + menuWaypoints[index] = pos; + menuWaypointNames[index] = name; + update(); + } + + /// Build OSRM waypoint coordinate string for the route URL + String _buildOsrmWaypointCoords() { + String coords = ''; + for (int i = 0; i < activeMenuWaypointCount; i++) { + final wp = menuWaypoints[i]; + if (wp != null) { + coords += ';${wp.longitude},${wp.latitude}'; + } + } + return coords; + } + void changeHeightPointsPageForRider() { isPointsPageForRider = !isPointsPageForRider; heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0; @@ -6375,10 +6459,12 @@ Intaleq Team'''; double lngDest = double.parse(coordDestination[1]); myDestination = LatLng(latDest, lngDest); - // 2. الاتصال بالسيرفر - New OSRM format + // 2. الاتصال بالسيرفر - New OSRM format (with multi-waypoint support) var originCoords = origin.split(','); + // Build waypoint coordinates for OSRM (inserted between origin and destination) + String waypointCoords = _buildOsrmWaypointCoords(); String dynamicApiUrl = - '${AppLink.routesOsm}/route/v1/driving/${originCoords[1]},${originCoords[0]};$lngDest,$latDest'; + '${AppLink.routesOsm}/route/v1/driving/${originCoords[1]},${originCoords[0]}$waypointCoords;$lngDest,$latDest'; var uri = Uri.parse('$dynamicApiUrl?steps=false&overview=full'); Log.print('Requesting Route URI (Attempt: ${attemptCount + 1}): $uri'); @@ -6506,14 +6592,6 @@ Intaleq Team'''; isDrawingRoute = false; isLoading = false; - if (minLat != null) { - LatLngBounds boundsData = LatLngBounds( - northeast: LatLng(maxLat!, maxLng!), - southwest: LatLng(minLat!, minLng!)); - mapController - ?.animateCamera(CameraUpdate.newLatLngBounds(boundsData, 100)); - } - // 6. إضافة الماركرز durationToAdd = Duration(seconds: durationToRide); hours = durationToAdd.inHours; @@ -6537,6 +6615,25 @@ Intaleq Team'''; '$distance ${'KM'.tr} ⌛ ${hours > 0 ? '$hours H $minutes m' : '$minutes m'}'), )); + // 6b. Add waypoint markers (amber for stop 1, deep purple for stop 2) + for (int i = 0; i < activeMenuWaypointCount; i++) { + final wp = menuWaypoints[i]; + if (wp != null) { + markers.add(Marker( + markerId: MarkerId('waypoint_$i'), + position: wp, + icon: BitmapDescriptor.defaultMarkerWithHue( + i == 0 + ? BitmapDescriptor.hueOrange + : BitmapDescriptor.hueViolet, + ), + infoWindow: InfoWindow( + title: '${'Stop'.tr} ${i + 1}', + snippet: menuWaypointNames[i]), + )); + } + } + // 7. رسم الخط (النظام الجديد لجميع الأجهزة) if (polyLines.isNotEmpty) clearPolyline(); @@ -6547,8 +6644,16 @@ Intaleq Team'''; // إظهار الباتم شيت للسعر bottomSheet(); - // تشغيل الأنيميشن الخفيف لومضات المسار - _playRouteAnimation(polylineCoordinates); + // 8. Compute bounds from all polyline points + LatLngBounds? boundsData; + if (minLat != null) { + boundsData = LatLngBounds( + northeast: LatLng(maxLat!, maxLng!), + southwest: LatLng(minLat!, minLng!)); + } + + // تشغيل الأنيميشن الخفيف لومضات المسار + fit camera after + _playRouteAnimation(polylineCoordinates, boundsData); } catch (e, stackTrace) { // 🚨 هذا السطر سيفضح المشكلة الحقيقية! 🚨 print('🚨 CRITICAL ERROR IN getDirectionMap: $e'); @@ -6563,25 +6668,28 @@ Intaleq Team'''; } } - // --- دالة الأنيميشن الجديدة --- - Future _playRouteAnimation(List coords) async { - const String routeId = 'route_solid'; + // --- دالة الأنيميشن مع ألوان مختلفة لكل قطعة --- + Future _playRouteAnimation( + List coords, LatLngBounds? bounds) async { + // Segment colors matching UI dots: green → amber → purple → red + const List segmentColors = [ + Color(0xFF109642), // Green (start → stop 1) + Color(0xFFF59E0B), // Amber (stop 1 → stop 2) + Color(0xFF7C3AED), // Purple (last segment → dest) + Color(0xFFEF4444), // Red (fallback) + ]; - // الألوان المطلوبة (بإمكانك تغيير AppColor.primaryColor إلى ما يناسبك) - Color finalColor = - AppColor.primaryColor; // اللون النهائي الثابت (مثل الأزرق) - Color lightColor = Colors.grey.shade400; // لون التحديث الفاتح (رمادي) - Color darkColor = Colors.grey.shade700; // لون التحديث الغامق (رمادي غامق) + // Loading animation (4 flashes) + Color lightColor = Colors.grey.shade400; + Color darkColor = Colors.grey.shade700; - // تكرار العملية 4 مرات لإعطاء تأثير التحميل والتحديث for (int i = 0; i < 4; i++) { - // الحالة 1: لون فاتح وعرض أقل - polyLines.removeWhere((p) => p.polylineId.value == routeId); + polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_')); polyLines.add(Polyline( - polylineId: const PolylineId(routeId), + polylineId: const PolylineId('route_solid'), points: coords, width: 5, - color: lightColor, + color: i.isEven ? lightColor : darkColor, endCap: Cap.roundCap, startCap: Cap.roundCap, jointType: JointType.round, @@ -6589,36 +6697,76 @@ Intaleq Team'''; )); update(); await Future.delayed(const Duration(milliseconds: 250)); + } - // الحالة 2: لون غامق وعرض أكبر - polyLines.removeWhere((p) => p.polylineId.value == routeId); + // After animation: draw coloured segments if we have waypoints + polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_')); + + if (activeMenuWaypointCount > 0) { + // Find split indices: closest polyline point to each active waypoint + List splitIndices = []; + for (int w = 0; w < activeMenuWaypointCount; w++) { + final wp = menuWaypoints[w]; + if (wp == null) continue; + int bestIdx = 0; + double bestDist = double.infinity; + for (int j = 0; j < coords.length; j++) { + final dx = coords[j].latitude - wp.latitude; + final dy = coords[j].longitude - wp.longitude; + final d = dx * dx + dy * dy; + if (d < bestDist) { + bestDist = d; + bestIdx = j; + } + } + splitIndices.add(bestIdx); + } + splitIndices.sort(); + + // Build segments: [0..split0], [split0..split1], ..., [splitN..end] + List boundaries = [0, ...splitIndices, coords.length - 1]; + for (int s = 0; s < boundaries.length - 1; s++) { + int from = boundaries[s]; + int to = boundaries[s + 1] + 1; // inclusive end + if (to > coords.length) to = coords.length; + if (from >= to - 1) continue; // skip empty + final segCoords = coords.sublist(from, to); + if (segCoords.length < 2) continue; + final color = segmentColors[s % segmentColors.length]; + polyLines.add(Polyline( + polylineId: PolylineId('route_seg_$s'), + points: segCoords, + width: 6, + color: color, + endCap: Cap.roundCap, + startCap: Cap.roundCap, + jointType: JointType.round, + zIndex: 3 + s, + )); + } + } else { + // Single leg: solid primary color polyLines.add(Polyline( - polylineId: const PolylineId(routeId), + polylineId: const PolylineId('route_solid'), points: coords, width: 6, - color: darkColor, + color: AppColor.primaryColor, endCap: Cap.roundCap, startCap: Cap.roundCap, jointType: JointType.round, - zIndex: 2, + zIndex: 3, )); - update(); - await Future.delayed(const Duration(milliseconds: 250)); } - - // بعد الانتهاء من الأنيميشن، يتم تثبيت المسار على اللون الأساسي للتطبيق - polyLines.removeWhere((p) => p.polylineId.value == routeId); - polyLines.add(Polyline( - polylineId: const PolylineId(routeId), - points: coords, - width: 6, - color: finalColor, - endCap: Cap.roundCap, - startCap: Cap.roundCap, - jointType: JointType.round, - zIndex: 3, - )); update(); + + // Fit camera to full route bounds AFTER polylines are drawn + if (bounds != null) { + await Future.delayed(const Duration(milliseconds: 500)); + try { + mapController + ?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 100)); + } catch (_) {} + } } // --- دالة المساعدة لإعادة المحاولة --- @@ -7341,7 +7489,9 @@ Intaleq Team'''; final DateTime currentTime = DateTime.now(); newTime = currentTime.add(durationToAdd); averageDuration = (durationToRide / 60) / distance; - final int totalMinutes = (durationToRide / 60).floor(); + // +5 minutes per waypoint stop surcharge + final int waypointSurchargeMinutes = activeMenuWaypointCount * 5; + final int totalMinutes = (durationToRide / 60).floor() + waypointSurchargeMinutes; // ====== أدوات مساعدة ====== bool _isAirport(String s) { diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 897a114..03d8784 100644 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -1429,6 +1429,17 @@ class MyTranslation extends Translations { "الآن حرك الخريطة إلى نقطة الانطلاق الخاصة بك", 'Move map to your pickup point': "حرك الخريطة إلى نقطة الانطلاق الخاصة بك", + 'You have a balance of': "لديك رصيد بقيمة", + 'Negative Balance:': "رصيد سلبي:", + 'You have a negative balance of': "لديك رصيد سلبي بقيمة", + 'Have a Promo Code?': "هل لديك رمز عرض؟", + 'Tap to apply your discount': "اضغط لتطبيق خصمك", + 'Plan Your Route': "خطط طريقك", + 'Add a Stop': "إضافة نقطة توقف", + 'stop(s)': "نقطة توقف", + 'min added to fare': "دقيقة مضافة للأجرة", + 'Set as Stop': "تعيين كنقطة توقف", + 'Move map to set stop': "حرك الخريطة لتعيين نقطة التوقف", 'Move map to set start location': "حرك الخريطة لتعيين موقع الانطلاق", 'Move map to your work location': "حرك الخريطة إلى موقع عملك", 'Move map to your home location': "حرك الخريطة إلى موقع منزلك", diff --git a/lib/views/home/map_widget.dart/car_details_widget_to_go.dart b/lib/views/home/map_widget.dart/car_details_widget_to_go.dart index 25b04ca..bcb4470 100644 --- a/lib/views/home/map_widget.dart/car_details_widget_to_go.dart +++ b/lib/views/home/map_widget.dart/car_details_widget_to_go.dart @@ -13,10 +13,11 @@ import 'dart:ui'; import '../../../constant/info.dart'; import '../../../controller/functions/tts.dart'; import '../../../controller/home/map_passenger_controller.dart'; -import '../../../print.dart'; import '../../widgets/mydialoug.dart'; -// --- CarType class (Unchanged) --- +// ───────────────────────────────────────────────────────────────────────────── +// CAR TYPE MODEL +// ───────────────────────────────────────────────────────────────────────────── class CarType { final String carType; final String carDetail; @@ -27,7 +28,6 @@ class CarType { {required this.carType, required this.carDetail, required this.image}); } -// --- List of Car Types (Unchanged) --- List carTypes = [ CarType( carType: 'Fixed Price', @@ -55,7 +55,9 @@ List carTypes = [ image: 'assets/images/roundtrip.png'), ]; -// --- Main Widget --- +// ───────────────────────────────────────────────────────────────────────────── +// MAIN WIDGET +// ───────────────────────────────────────────────────────────────────────────── class CarDetailsTypeToChoose extends StatelessWidget { CarDetailsTypeToChoose({super.key}); final textToSpeechController = Get.put(TextToSpeechController()); @@ -82,152 +84,214 @@ class CarDetailsTypeToChoose extends StatelessWidget { return const SizedBox.shrink(); } - // Main Bottom Sheet Design return Positioned( bottom: 0, left: 0, right: 0, - child: Container( - decoration: BoxDecoration( - color: AppColor - .secondaryColor, // Solid background for better performance - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(30), - topRight: Radius.circular(30), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 20, - spreadRadius: 5, - offset: const Offset(0, -5), - ), - ], + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Drag Handle - Center( - child: Container( - width: 50, - height: 5, - margin: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.3), - borderRadius: BorderRadius.circular(10), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Container( + decoration: BoxDecoration( + color: AppColor.secondaryColor.withAlpha(240), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(30), + blurRadius: 30, + spreadRadius: 0, + offset: const Offset(0, -8), ), - ), + ], ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Drag Handle ────────────────────────────────────── + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(top: 12, bottom: 8), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + ), - // Header (Title + Trip Info) - _buildModernHeader(controller), + // ── Header ─────────────────────────────────────────── + _buildHeader(controller), - // Warning Message (if any) - _buildNegativeBalanceWarning(controller), + // ── Negative Balance Warning ───────────────────────── + _buildNegativeBalanceWarning(controller), - // Car List - SizedBox( - height: 165, // Fixed height for consistency - child: ListView.separated( - physics: const BouncingScrollPhysics(), - scrollDirection: Axis.horizontal, - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - itemCount: carTypes.length, - separatorBuilder: (context, index) => - const SizedBox(width: 12), - itemBuilder: (context, index) { - final carType = carTypes[index]; - final isSelected = controller.selectedIndex == index; - return _buildVerticalCarCard( - context, controller, carType, isSelected, index); - }, - ), + // ── Car Selection List ─────────────────────────────── + SizedBox( + height: 170, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), + itemCount: carTypes.length, + itemBuilder: (context, index) { + final carType = carTypes[index]; + final isSelected = controller.selectedIndex == index; + return Padding( + padding: EdgeInsets.only( + right: index < carTypes.length - 1 ? 10 : 0), + child: _buildCarCard( + context, controller, carType, isSelected, index), + ); + }, + ), + ), + + // ── Promo Code & Actions ───────────────────────────── + _buildPromoButton(context, controller), + + SizedBox( + height: MediaQuery.of(context).padding.bottom + 10), + ], ), - - // Promo Code Button - _buildPromoButton(context, controller), - - // Safe Area spacing - SizedBox(height: MediaQuery.of(context).padding.bottom + 10), - ], + ), ), ), ); }); } - // --- UI Components --- - - Widget _buildModernHeader(MapPassengerController controller) { + // ═══════════════════════════════════════════════════════════════════════════ + // HEADER + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildHeader(MapPassengerController controller) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.fromLTRB(22, 4, 22, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - 'Choose your ride'.tr, - style: AppStyle.headTitle.copyWith( - fontSize: 20, - fontWeight: FontWeight.w800, - letterSpacing: 0.5), - ), + Row( + children: [ + // Title + Expanded( + child: Text( + 'Choose your ride'.tr, + style: AppStyle.headTitle.copyWith( + fontSize: 20, + fontWeight: FontWeight.w800, + letterSpacing: 0.3, + ), + ), + ), + // Close button + GestureDetector( + onTap: () { + controller.isBottomSheetShown = false; + controller.update(); + }, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon(Icons.close_rounded, + size: 18, color: Colors.grey.shade600), + ), + ), + ], ), - - // Trip Info Pill - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: AppColor.primaryColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.directions_car_filled_outlined, - size: 16, color: AppColor.primaryColor), - const SizedBox(width: 6), - Text( - '${controller.distance.toStringAsFixed(1)} ${'KM'.tr}', - style: AppStyle.subtitle.copyWith( - fontSize: 13, - fontWeight: FontWeight.bold, - color: AppColor.primaryColor), - ), + const SizedBox(height: 10), + // Trip Stats Row + Row( + children: [ + // Distance chip + _buildStatChip( + icon: Icons.route_rounded, + value: + '${controller.distance.toStringAsFixed(1)} ${'KM'.tr}', + color: AppColor.primaryColor, + ), + const SizedBox(width: 8), + // Duration chip + _buildStatChip( + icon: Icons.schedule_rounded, + value: controller.hours > 0 + ? '${controller.hours}h ${controller.minutes}m' + : '${controller.minutes} ${'min'.tr}', + color: const Color(0xFF6366F1), // Indigo + ), + const Spacer(), + // Price preview for selected car + if (controller.selectedIndex >= 0 && + controller.selectedIndex < carTypes.length) Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - height: 12, - width: 1, - color: Colors.grey.shade400), - Icon(Icons.access_time_filled_rounded, - size: 16, color: AppColor.primaryColor), - const SizedBox(width: 6), - Text( - controller.hours > 0 - ? '${controller.hours}h ${controller.minutes}m' - : '${controller.minutes} min', - style: AppStyle.subtitle.copyWith( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primaryColor, + AppColor.primaryColor.withAlpha(200), + ], + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_getPassengerPriceText(carTypes[controller.selectedIndex], controller)} ${'SYP'.tr}', + style: const TextStyle( + color: Colors.white, fontSize: 13, - fontWeight: FontWeight.bold, - color: AppColor.primaryColor), + fontWeight: FontWeight.w700, + ), + ), ), - ], - ), - ) + ], + ), ], ), ); } - Widget _buildVerticalCarCard( - BuildContext context, - MapPassengerController controller, - CarType carType, - bool isSelected, - int index) { + Widget _buildStatChip( + {required IconData icon, required String value, required Color color}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: color.withAlpha(20), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: color.withAlpha(50)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 5), + Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: color, + ), + ), + ], + ), + ); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CAR CARD + // ═══════════════════════════════════════════════════════════════════════════ + Widget _buildCarCard(BuildContext context, MapPassengerController controller, + CarType carType, bool isSelected, int index) { return GestureDetector( onTap: () { controller.selectCarFromList(index); @@ -235,67 +299,97 @@ class CarDetailsTypeToChoose extends StatelessWidget { context, controller, carType, textToSpeechController); }, child: AnimatedContainer( - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - width: 110, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + width: 108, decoration: BoxDecoration( - color: isSelected - ? AppColor.primaryColor.withOpacity(0.05) - : Colors.white, - borderRadius: BorderRadius.circular(18), + gradient: isSelected + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppColor.primaryColor.withAlpha(18), + AppColor.primaryColor.withAlpha(8), + ], + ) + : null, + color: isSelected ? null : Colors.white, + borderRadius: BorderRadius.circular(16), border: Border.all( - color: isSelected - ? AppColor.primaryColor - : Colors.grey.withOpacity(0.2), + color: + isSelected ? AppColor.primaryColor : Colors.grey.shade200, width: isSelected ? 2.0 : 1.0, ), - boxShadow: isSelected - ? [ - BoxShadow( - color: AppColor.primaryColor.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 4), - ) - ] - : [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ) - ], + boxShadow: [ + BoxShadow( + color: isSelected + ? AppColor.primaryColor.withAlpha(40) + : Colors.black.withAlpha(12), + blurRadius: isSelected ? 12 : 4, + offset: const Offset(0, 3), + ), + ], ), child: Stack( - alignment: Alignment.center, children: [ + // Selected indicator + if (isSelected) + Positioned( + top: 6, + right: 6, + child: Container( + width: 18, + height: 18, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primaryColor, + AppColor.primaryColor.withAlpha(200), + ], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppColor.primaryColor.withAlpha(60), + blurRadius: 6, + ), + ], + ), + child: + const Icon(Icons.check, size: 11, color: Colors.white), + ), + ), + + // Card content Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.fromLTRB(8, 10, 8, 8), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Image with subtle scaling if selected + // Car image AnimatedScale( - scale: isSelected ? 1.1 : 1.0, - duration: const Duration(milliseconds: 250), + scale: isSelected ? 1.12 : 1.0, + duration: const Duration(milliseconds: 300), child: Image.asset( carType.image, - height: 50, + height: 48, fit: BoxFit.contain, ), ), - const SizedBox(height: 12), + const SizedBox(height: 10), - // Car Type Text + // Car name FittedBox( fit: BoxFit.scaleDown, child: Text( carType.carType.tr, - style: AppStyle.subtitle.copyWith( + style: TextStyle( fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600, - fontSize: 14, - color: - isSelected ? AppColor.primaryColor : Colors.black87, + fontSize: 13, + color: isSelected + ? AppColor.primaryColor + : Colors.grey.shade800, ), maxLines: 1, ), @@ -303,23 +397,27 @@ class CarDetailsTypeToChoose extends StatelessWidget { const SizedBox(height: 6), - // Price Tag + // Price tag Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), decoration: BoxDecoration( color: isSelected ? AppColor.primaryColor - : Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), + : Colors.grey.shade50, + borderRadius: BorderRadius.circular(10), + border: isSelected + ? null + : Border.all(color: Colors.grey.shade200), ), child: FittedBox( child: Text( '${_getPassengerPriceText(carType, controller)} ${'SYP'.tr}', style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: isSelected ? Colors.white : Colors.black87, + fontSize: 11, + fontWeight: FontWeight.w700, + color: + isSelected ? Colors.white : Colors.grey.shade700, ), ), ), @@ -327,75 +425,80 @@ class CarDetailsTypeToChoose extends StatelessWidget { ], ), ), - - // Checkmark Badge for Selected Item - if (isSelected) - Positioned( - top: 8, - right: 8, - child: Container( - padding: const EdgeInsets.all(2), - decoration: const BoxDecoration( - color: AppColor.primaryColor, - shape: BoxShape.circle, - ), - child: const Icon(Icons.check, size: 12, color: Colors.white), - ), - ), ], ), ), ); } + // ═══════════════════════════════════════════════════════════════════════════ + // PROMO BUTTON + // ═══════════════════════════════════════════════════════════════════════════ Widget _buildPromoButton( BuildContext context, MapPassengerController controller) { if (controller.promoTaken) return const SizedBox.shrink(); return Padding( - padding: const EdgeInsets.fromLTRB(20, 10, 20, 5), + padding: const EdgeInsets.fromLTRB(20, 6, 20, 4), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _showPromoCodeDialog(context, controller), borderRadius: BorderRadius.circular(14), child: Container( - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + padding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 14), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.05), + gradient: LinearGradient( + colors: [ + Colors.amber.shade50, + Colors.orange.shade50, + ], + ), borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.grey.withOpacity(0.2)), + border: Border.all(color: Colors.amber.shade200), ), child: Row( children: [ Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(7), decoration: BoxDecoration( - color: AppColor.primaryColor.withOpacity(0.1), - shape: BoxShape.circle), - child: Icon(Icons.confirmation_number_outlined, - color: AppColor.primaryColor, size: 20), + color: Colors.amber.shade100, + shape: BoxShape.circle, + ), + child: Icon(Icons.percent_rounded, + color: Colors.amber.shade800, size: 16), ), - const SizedBox(width: 14), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Promo Code'.tr, - style: AppStyle.subtitle.copyWith( - fontSize: 14, fontWeight: FontWeight.bold), + 'Have a Promo Code?'.tr, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: Colors.amber.shade900, + ), ), Text( - 'Have a promo code?'.tr, - style: AppStyle.subtitle.copyWith( - fontSize: 12, color: Colors.grey.shade600), + 'Tap to apply your discount'.tr, + style: TextStyle( + fontSize: 11, color: Colors.amber.shade700), ), ], ), ), - Icon(Icons.arrow_forward_ios_rounded, - size: 16, color: Colors.grey.shade400) + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.amber.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.arrow_forward_ios_rounded, + size: 12, color: Colors.amber.shade800), + ), ], ), ), @@ -404,30 +507,41 @@ class CarDetailsTypeToChoose extends StatelessWidget { ); } + // ═══════════════════════════════════════════════════════════════════════════ + // NEGATIVE BALANCE WARNING + // ═══════════════════════════════════════════════════════════════════════════ Widget _buildNegativeBalanceWarning(MapPassengerController controller) { final passengerWallet = double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0; if (passengerWallet < 0.0) { return Container( - margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColor.redColor.withOpacity(0.1), + color: Colors.red.shade50, borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColor.redColor.withOpacity(0.3)), + border: Border.all(color: Colors.red.shade200), ), child: Row( children: [ - Icon(Icons.info_outline_rounded, - color: AppColor.redColor, size: 24), - const SizedBox(width: 12), + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.red.shade100, + shape: BoxShape.circle, + ), + child: Icon(Icons.warning_amber_rounded, + color: Colors.red.shade700, size: 18), + ), + const SizedBox(width: 10), Expanded( child: Text( '${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.', - style: AppStyle.subtitle.copyWith( - color: AppColor.redColor, - fontWeight: FontWeight.w600, - fontSize: 13), + style: TextStyle( + color: Colors.red.shade800, + fontWeight: FontWeight.w600, + fontSize: 12, + ), ), ), ], @@ -437,8 +551,9 @@ class CarDetailsTypeToChoose extends StatelessWidget { return const SizedBox.shrink(); } - // --- Logic Helpers (Copied from your previous code to ensure functionality) --- - + // ═══════════════════════════════════════════════════════════════════════════ + // PRICING HELPERS (Unchanged logic) + // ═══════════════════════════════════════════════════════════════════════════ String _getPassengerPriceText( CarType carType, MapPassengerController mapPassengerController) { double rawPrice; @@ -476,8 +591,9 @@ class CarDetailsTypeToChoose extends StatelessWidget { return formatter.format(roundedPrice); } - // --- Dialogs (Styled consistently) --- - + // ═══════════════════════════════════════════════════════════════════════════ + // DIALOGS + // ═══════════════════════════════════════════════════════════════════════════ void _showPromoCodeDialog( BuildContext context, MapPassengerController controller) { Get.dialog( @@ -548,7 +664,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { ), ), ), - barrierColor: Colors.black.withOpacity(0.5), + barrierColor: Colors.black.withAlpha(130), ); } @@ -567,11 +683,18 @@ class CarDetailsTypeToChoose extends StatelessWidget { alignment: Alignment.topCenter, children: [ Container( - margin: const EdgeInsets.only(top: 60), - padding: const EdgeInsets.fromLTRB(24, 70, 24, 24), + margin: const EdgeInsets.only(top: 55), + padding: const EdgeInsets.fromLTRB(24, 65, 24, 20), decoration: BoxDecoration( color: AppColor.secondaryColor, borderRadius: BorderRadius.circular(28.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(30), + blurRadius: 30, + offset: const Offset(0, 10), + ), + ], ), child: Column( mainAxisSize: MainAxisSize.min, @@ -581,7 +704,10 @@ class CarDetailsTypeToChoose extends StatelessWidget { children: [ Text( carType.carType.tr, - style: AppStyle.headTitle.copyWith(fontSize: 22), + style: AppStyle.headTitle.copyWith( + fontSize: 22, + fontWeight: FontWeight.w800, + ), ), const SizedBox(width: 8), InkWell( @@ -589,38 +715,75 @@ class CarDetailsTypeToChoose extends StatelessWidget { _getCarDescription( mapPassengerController, carType)), borderRadius: BorderRadius.circular(20), - child: Padding( - padding: const EdgeInsets.all(4.0), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: AppColor.primaryColor.withAlpha(20), + shape: BoxShape.circle, + ), child: Icon(Icons.volume_up_rounded, - color: AppColor.primaryColor, size: 24), + color: AppColor.primaryColor, size: 20), ), ) ], ), - const SizedBox(height: 16), + const SizedBox(height: 6), + // Price badge in dialog Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 6), decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.05), - borderRadius: BorderRadius.circular(16)), + gradient: LinearGradient( + colors: [ + AppColor.primaryColor, + AppColor.primaryColor.withAlpha(200), + ], + ), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_getPassengerPriceText(carType, mapPassengerController)} ${'SYP'.tr}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Colors.white, + ), + ), + ), + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade100), + ), child: Text( _getCarDescription(mapPassengerController, carType), textAlign: TextAlign.center, style: AppStyle.subtitle.copyWith( - color: Colors.black87, - fontSize: 15, - height: 1.4, + color: Colors.grey.shade700, + fontSize: 14, + height: 1.5, ), ), ), - const SizedBox(height: 24), + const SizedBox(height: 20), Row( children: [ Expanded( child: TextButton( onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide(color: Colors.grey.shade200), + ), + ), child: Text('Back'.tr, - style: TextStyle(color: Colors.grey.shade600)), + style: + TextStyle(color: Colors.grey.shade600)), ), ), const SizedBox(width: 12), @@ -641,22 +804,38 @@ class CarDetailsTypeToChoose extends StatelessWidget { ], ), ), + // Floating car image Positioned( top: 0, - child: Hero( - tag: 'car_${carType.carType}', - child: Image.asset(carType.image, height: 130), + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(20), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Hero( + tag: 'car_${carType.carType}', + child: Image.asset(carType.image, height: 80), + ), ), ), ], ), ), - barrierColor: Colors.black.withOpacity(0.6), + barrierColor: Colors.black.withAlpha(150), ); } - // --- Logic Helpers (Keep unchanged) --- - + // ═══════════════════════════════════════════════════════════════════════════ + // LOGIC HELPERS (Unchanged) + // ═══════════════════════════════════════════════════════════════════════════ String _getCarDescription( MapPassengerController mapPassengerController, CarType carType) { switch (carType.carType) { @@ -787,7 +966,9 @@ class CarDetailsTypeToChoose extends StatelessWidget { } } -// --- BurcMoney Widget (Unchanged) --- +// ───────────────────────────────────────────────────────────────────────────── +// BURC MONEY WIDGET (Floating negative balance banner) +// ───────────────────────────────────────────────────────────────────────────── class BurcMoney extends StatelessWidget { const BurcMoney({super.key}); @@ -810,13 +991,13 @@ class BurcMoney extends StatelessWidget { child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColor.redColor.withOpacity(0.8), - borderRadius: BorderRadius.circular(8), + color: AppColor.redColor.withAlpha(220), + borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), + color: Colors.red.withAlpha(40), + blurRadius: 12, + offset: const Offset(0, 4), ), ], ), diff --git a/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart b/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart index 54a149d..ab396a3 100644 --- a/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart +++ b/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart @@ -191,20 +191,19 @@ class _ExpandedView extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // ── هيدر "Quick Actions" ───────────────────────────────────────── + // ── Header ──────────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(18, 14, 12, 8), child: Row( children: [ Text( - 'Quick Actions'.tr, + 'Plan Your Route'.tr, style: AppStyle.title.copyWith( fontWeight: FontWeight.w700, fontSize: 16, ), ), const Spacer(), - // زر إغلاق GestureDetector( onTap: controller.changeMainBottomMenuMap, child: Container( @@ -222,36 +221,236 @@ class _ExpandedView extends StatelessWidget { ), const Divider(height: 1, thickness: 0.5), - const SizedBox(height: 10), - - // ── موقع البداية ───────────────────────────────────────────────── - if (!controller.isAnotherOreder) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _LocationRow( - icon: Icons.my_location_rounded, - iconColor: AppColor.primaryColor, - label: controller.currentLocationString, - isStart: true, - ), - ) - else - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: formSearchPlacesStart(), - ), - - const SizedBox(height: 6), - - // ── حقل الوجهة ──────────────────────────────────────────────────── - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: formSearchPlacesDestenation(), - ), - const SizedBox(height: 12), - // ── زر WhatsApp ─────────────────────────────────────────────────── + // ── Route Timeline (no IntrinsicHeight) ───────────────────────── + // Start location row with green dot + _buildTimelineItem( + dotColor: AppColor.primaryColor, + showTopLine: false, + showBottomLine: true, + child: !controller.isAnotherOreder + ? _TimelineRow( + icon: Icons.my_location_rounded, + iconColor: AppColor.primaryColor, + bgColor: AppColor.primaryColor, + label: controller.currentLocationString, + ) + : Padding( + padding: const EdgeInsets.only(right: 16), + child: formSearchPlacesStart(), + ), + ), + + // Waypoint stop rows + ...List.generate(controller.activeMenuWaypointCount, (index) { + final wpName = controller.menuWaypointNames[index]; + final isSet = controller.menuWaypoints[index] != null; + // Stop 1 = amber/orange, Stop 2 = deep purple + final Color dotColor = + index == 0 ? Colors.amber.shade700 : Colors.deepPurple.shade400; + return _buildTimelineItem( + dotColor: dotColor, + showTopLine: true, + showBottomLine: true, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: index == 0 + ? Colors.amber.shade50 + : Colors.deepPurple.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isSet + ? (index == 0 + ? Colors.amber.shade300 + : Colors.deepPurple.shade200) + : Colors.grey.shade200, + ), + ), + child: Row( + children: [ + // Numbered badge + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: index == 0 + ? [Colors.amber.shade500, Colors.amber.shade700] + : [ + Colors.deepPurple.shade300, + Colors.deepPurple.shade500 + ], + ), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: GestureDetector( + onTap: () { + controller.changeMainBottomMenuMap(); + controller.startPickingWaypointOnMap(index); + }, + child: Text( + isSet ? wpName : '${'Stop'.tr} ${index + 1}', + style: TextStyle( + fontSize: 13, + color: isSet + ? (index == 0 + ? Colors.amber.shade900 + : Colors.deepPurple.shade700) + : Colors.grey.shade400, + fontWeight: isSet ? FontWeight.w500 : FontWeight.w400, + fontStyle: + isSet ? FontStyle.normal : FontStyle.italic, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + GestureDetector( + onTap: () { + controller.changeMainBottomMenuMap(); + controller.startPickingWaypointOnMap(index); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Icon(Icons.map_outlined, + color: index == 0 + ? Colors.amber.shade600 + : Colors.deepPurple.shade400, + size: 18), + ), + ), + GestureDetector( + onTap: () => controller.removeMenuWaypoint(index), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Colors.red.shade50, + shape: BoxShape.circle, + ), + child: Icon(Icons.close_rounded, + color: Colors.red.shade400, size: 13), + ), + ), + ], + ), + ), + ); + }), + + // Add Stop button + if (controller.activeMenuWaypointCount < 2) + _buildTimelineItem( + dotColor: Colors.grey.shade300, + isDotDashed: true, + showTopLine: true, + showBottomLine: true, + child: GestureDetector( + onTap: () => controller.addMenuWaypoint(), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.orange.shade200), + color: Colors.orange.shade50.withAlpha(100), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_circle_outline_rounded, + color: Colors.orange.shade500, size: 16), + const SizedBox(width: 6), + Text( + 'Add a Stop'.tr, + style: TextStyle( + color: Colors.orange.shade700, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + '+5 ${'min'.tr}', + style: TextStyle( + color: Colors.orange.shade700, + fontSize: 9, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ), + + // Destination row with red dot + _buildTimelineItem( + dotColor: Colors.red.shade500, + showTopLine: true, + showBottomLine: false, + child: Padding( + padding: const EdgeInsets.only(right: 16), + child: formSearchPlacesDestenation(), + ), + ), + + // ── Surcharge info ─────────────────────────────────────────────── + if (controller.activeMenuWaypointCount > 0) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 6), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.schedule_rounded, + size: 14, color: Colors.orange.shade600), + const SizedBox(width: 6), + Text( + '${controller.activeMenuWaypointCount} ${'stop(s)'.tr} · +${controller.activeMenuWaypointCount * 5} ${'min added to fare'.tr}', + style: TextStyle( + fontSize: 11, + color: Colors.orange.shade700, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 10), + + // ── WhatsApp ───────────────────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _WhatsAppLinkButton(controller: controller), @@ -259,7 +458,7 @@ class _ExpandedView extends StatelessWidget { const SizedBox(height: 10), - // ── Order for someone else ──────────────────────────────────────── + // ── Order type ─────────────────────────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _OrderTypeButton(controller: controller), @@ -269,6 +468,59 @@ class _ExpandedView extends StatelessWidget { ], ); } + + /// Builds a single timeline row: [dot + line] | [child widget] + Widget _buildTimelineItem({ + required Color dotColor, + required bool showTopLine, + required bool showBottomLine, + required Widget child, + bool isDotDashed = false, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Timeline indicator column (dot + lines) + SizedBox( + width: 24, + child: Column( + children: [ + // Top connecting line + if (showTopLine) + Container( + width: 2, + height: 8, + color: Colors.grey.shade300, + ), + // Dot + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: isDotDashed ? Colors.transparent : dotColor, + shape: BoxShape.circle, + border: Border.all(color: dotColor, width: 2), + ), + ), + // Bottom connecting line + if (showBottomLine) + Container( + width: 2, + height: 8, + color: Colors.grey.shade300, + ), + ], + ), + ), + const SizedBox(width: 10), + // Content + Expanded(child: child), + ], + ), + ); + } } // ───────────────────────────────────────────────────────────────────────────── @@ -282,6 +534,10 @@ class _MapPickerOverlay extends StatelessWidget { // ── الحصول على نص الحالة الحالية ───────────────────────────────────────── String _getModeTitle(BuildContext context) { + if (controller.isPickingWaypoint) { + return 'Move map to set stop'.tr + + ' ${controller.pickingWaypointIndex + 1}'.tr; + } if (controller.passengerStartLocationFromMap) { return 'Move map to your pickup point'.tr; } else if (controller.startLocationFromMap) { @@ -295,6 +551,9 @@ class _MapPickerOverlay extends StatelessWidget { } String _getConfirmLabel(BuildContext context) { + if (controller.isPickingWaypoint) { + return 'Set as Stop'.tr; + } if (controller.passengerStartLocationFromMap) { return 'Confirm Pickup Location'.tr; } else if (controller.workLocationFromMap) { @@ -306,6 +565,7 @@ class _MapPickerOverlay extends StatelessWidget { } IconData _getModeIcon() { + if (controller.isPickingWaypoint) return Icons.add_location_alt_rounded; if (controller.passengerStartLocationFromMap) return Icons.person_pin_circle_rounded; if (controller.workLocationFromMap) return Icons.work_rounded; @@ -314,6 +574,7 @@ class _MapPickerOverlay extends StatelessWidget { } Color _getModeColor() { + if (controller.isPickingWaypoint) return Colors.orange.shade600; if (controller.passengerStartLocationFromMap) return Colors.green.shade600; if (controller.workLocationFromMap) return Colors.blue.shade600; if (controller.homeLocationFromMap) return Colors.orange.shade600; @@ -428,6 +689,8 @@ class _MapPickerOverlay extends StatelessWidget { controller.startLocationFromMap = false; controller.workLocationFromMap = false; controller.homeLocationFromMap = false; + controller.isPickingWaypoint = false; + controller.pickingWaypointIndex = -1; // أعد الخريطة لحالتها المنهارة if (!controller.isMainBottomMenuMap) { controller.isMainBottomMenuMap = true; @@ -498,6 +761,24 @@ class _MapPickerOverlay extends StatelessWidget { controller.newMyLocation.longitude, ); + // ── CASE 0: Waypoint picker mode ────────────────────────────────────── + if (controller.isPickingWaypoint && controller.pickingWaypointIndex >= 0) { + final int wpIndex = controller.pickingWaypointIndex; + controller.setMenuWaypointFromMap(wpIndex, currentCameraPosition); + + Get.snackbar( + 'Stop ${wpIndex + 1} Set'.tr, + 'Waypoint has been set successfully'.tr, + backgroundColor: Colors.orange.shade600, + colorText: Colors.white, + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 2), + margin: const EdgeInsets.all(12), + borderRadius: 12, + ); + return; + } + controller.clearPolyline(); controller.data = []; @@ -593,21 +874,35 @@ class _MapPickerOverlay extends StatelessWidget { controller.update(); // ─── 3. العمليات الـ async بعد حفظ الوجهة بأمان ────────────────────── - if (!controller.isAnotherOreder) { - // تحريك الكاميرا لموقع الراكب ليختار منه نقطة الانطلاق - // ملاحظة: هذا يُغير newMyLocation - لكن myDestination محفوظ بأمان ✅ - await controller.mapController?.animateCamera( - CameraUpdate.newLatLng(LatLng( - controller.passengerLocation.latitude, - controller.passengerLocation.longitude, - )), - ); + try { + if (controller.isAnotherOreder) { + // ✅ "Order for someone else": move camera to the OTHER person's + // start location (set via the start search form) + await controller.mapController?.animateCamera( + CameraUpdate.newLatLng(LatLng( + controller.newStartPointLocation.latitude, + controller.newStartPointLocation.longitude, + )), + ); + } else { + // Normal flow: move camera to the passenger's own location + await controller.mapController?.animateCamera( + CameraUpdate.newLatLng(LatLng( + controller.passengerLocation.latitude, + controller.passengerLocation.longitude, + )), + ); + } + } catch (_) { + // Guard against disposed GoogleMapController } // ─── 4. إشعار المستخدم ────────────────────────────────────────────── Get.snackbar( 'Destination Set'.tr, - 'Now move the map to your pickup point'.tr, + controller.isAnotherOreder + ? 'Now set the pickup point for the other person'.tr + : 'Now move the map to your pickup point'.tr, backgroundColor: Colors.green.shade600, colorText: Colors.white, snackPosition: SnackPosition.TOP, @@ -664,6 +959,46 @@ class _LocationRow extends StatelessWidget { } } +/// صف في التايملاين (للعرض داخل عمود المسار) +class _TimelineRow extends StatelessWidget { + final IconData icon; + final Color iconColor; + final Color bgColor; + final String label; + const _TimelineRow({ + required this.icon, + required this.iconColor, + required this.bgColor, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: bgColor.withAlpha(15), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: bgColor.withAlpha(40)), + ), + child: Row( + children: [ + Icon(icon, color: iconColor, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: AppStyle.subtitle.copyWith(fontSize: 12.5), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } +} + /// شريحة الأماكن الأخيرة class _RecentPlaceChip extends StatelessWidget { final MapPassengerController controller;