2026-03-13-2

This commit is contained in:
Hamza-Ayed
2026-03-13 22:43:46 +03:00
parent fdfea5582a
commit e2341b104f
4 changed files with 1007 additions and 330 deletions

View File

@@ -263,6 +263,14 @@ class MapPassengerController extends GetxController {
bool currentLocationToFormPlaces3 = false; bool currentLocationToFormPlaces3 = false;
bool currentLocationToFormPlaces4 = false; bool currentLocationToFormPlaces4 = false;
List currentLocationToFormPlacesAll = []; List currentLocationToFormPlacesAll = [];
// ── Multi-Waypoint (max 2 stops) ──────────────────────────────────────────
List<LatLng?> menuWaypoints = [null, null];
List<String> menuWaypointNames = ['', ''];
int activeMenuWaypointCount = 0;
bool isPickingWaypoint = false;
int pickingWaypointIndex = -1;
late String driverToken = ''; late String driverToken = '';
int carsOrder = 0; int carsOrder = 0;
int wayPointIndex = 0; int wayPointIndex = 0;
@@ -4906,6 +4914,82 @@ Intaleq Team''';
update(); 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() { void changeHeightPointsPageForRider() {
isPointsPageForRider = !isPointsPageForRider; isPointsPageForRider = !isPointsPageForRider;
heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0; heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0;
@@ -6375,10 +6459,12 @@ Intaleq Team''';
double lngDest = double.parse(coordDestination[1]); double lngDest = double.parse(coordDestination[1]);
myDestination = LatLng(latDest, lngDest); myDestination = LatLng(latDest, lngDest);
// 2. الاتصال بالسيرفر - New OSRM format // 2. الاتصال بالسيرفر - New OSRM format (with multi-waypoint support)
var originCoords = origin.split(','); var originCoords = origin.split(',');
// Build waypoint coordinates for OSRM (inserted between origin and destination)
String waypointCoords = _buildOsrmWaypointCoords();
String dynamicApiUrl = 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'); var uri = Uri.parse('$dynamicApiUrl?steps=false&overview=full');
Log.print('Requesting Route URI (Attempt: ${attemptCount + 1}): $uri'); Log.print('Requesting Route URI (Attempt: ${attemptCount + 1}): $uri');
@@ -6506,14 +6592,6 @@ Intaleq Team''';
isDrawingRoute = false; isDrawingRoute = false;
isLoading = false; isLoading = false;
if (minLat != null) {
LatLngBounds boundsData = LatLngBounds(
northeast: LatLng(maxLat!, maxLng!),
southwest: LatLng(minLat!, minLng!));
mapController
?.animateCamera(CameraUpdate.newLatLngBounds(boundsData, 100));
}
// 6. إضافة الماركرز // 6. إضافة الماركرز
durationToAdd = Duration(seconds: durationToRide); durationToAdd = Duration(seconds: durationToRide);
hours = durationToAdd.inHours; hours = durationToAdd.inHours;
@@ -6537,6 +6615,25 @@ Intaleq Team''';
'$distance ${'KM'.tr}${hours > 0 ? '$hours H $minutes m' : '$minutes m'}'), '$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. رسم الخط (النظام الجديد لجميع الأجهزة) // 7. رسم الخط (النظام الجديد لجميع الأجهزة)
if (polyLines.isNotEmpty) clearPolyline(); if (polyLines.isNotEmpty) clearPolyline();
@@ -6547,8 +6644,16 @@ Intaleq Team''';
// إظهار الباتم شيت للسعر // إظهار الباتم شيت للسعر
bottomSheet(); bottomSheet();
// تشغيل الأنيميشن الخفيف لومضات المسار // 8. Compute bounds from all polyline points
_playRouteAnimation(polylineCoordinates); LatLngBounds? boundsData;
if (minLat != null) {
boundsData = LatLngBounds(
northeast: LatLng(maxLat!, maxLng!),
southwest: LatLng(minLat!, minLng!));
}
// تشغيل الأنيميشن الخفيف لومضات المسار + fit camera after
_playRouteAnimation(polylineCoordinates, boundsData);
} catch (e, stackTrace) { } catch (e, stackTrace) {
// 🚨 هذا السطر سيفضح المشكلة الحقيقية! 🚨 // 🚨 هذا السطر سيفضح المشكلة الحقيقية! 🚨
print('🚨 CRITICAL ERROR IN getDirectionMap: $e'); print('🚨 CRITICAL ERROR IN getDirectionMap: $e');
@@ -6563,25 +6668,28 @@ Intaleq Team''';
} }
} }
// --- دالة الأنيميشن الجديدة --- // --- دالة الأنيميشن مع ألوان مختلفة لكل قطعة ---
Future<void> _playRouteAnimation(List<LatLng> coords) async { Future<void> _playRouteAnimation(
const String routeId = 'route_solid'; List<LatLng> coords, LatLngBounds? bounds) async {
// Segment colors matching UI dots: green → amber → purple → red
const List<Color> 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 إلى ما يناسبك) // Loading animation (4 flashes)
Color finalColor = Color lightColor = Colors.grey.shade400;
AppColor.primaryColor; // اللون النهائي الثابت (مثل الأزرق) Color darkColor = Colors.grey.shade700;
Color lightColor = Colors.grey.shade400; // لون التحديث الفاتح (رمادي)
Color darkColor = Colors.grey.shade700; // لون التحديث الغامق (رمادي غامق)
// تكرار العملية 4 مرات لإعطاء تأثير التحميل والتحديث
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
// الحالة 1: لون فاتح وعرض أقل polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_'));
polyLines.removeWhere((p) => p.polylineId.value == routeId);
polyLines.add(Polyline( polyLines.add(Polyline(
polylineId: const PolylineId(routeId), polylineId: const PolylineId('route_solid'),
points: coords, points: coords,
width: 5, width: 5,
color: lightColor, color: i.isEven ? lightColor : darkColor,
endCap: Cap.roundCap, endCap: Cap.roundCap,
startCap: Cap.roundCap, startCap: Cap.roundCap,
jointType: JointType.round, jointType: JointType.round,
@@ -6589,36 +6697,76 @@ Intaleq Team''';
)); ));
update(); update();
await Future.delayed(const Duration(milliseconds: 250)); await Future.delayed(const Duration(milliseconds: 250));
}
// الحالة 2: لون غامق وعرض أكبر // After animation: draw coloured segments if we have waypoints
polyLines.removeWhere((p) => p.polylineId.value == routeId); polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_'));
if (activeMenuWaypointCount > 0) {
// Find split indices: closest polyline point to each active waypoint
List<int> 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<int> 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( polyLines.add(Polyline(
polylineId: const PolylineId(routeId), polylineId: const PolylineId('route_solid'),
points: coords, points: coords,
width: 6, width: 6,
color: darkColor, color: AppColor.primaryColor,
endCap: Cap.roundCap, endCap: Cap.roundCap,
startCap: Cap.roundCap, startCap: Cap.roundCap,
jointType: JointType.round, 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(); 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(); final DateTime currentTime = DateTime.now();
newTime = currentTime.add(durationToAdd); newTime = currentTime.add(durationToAdd);
averageDuration = (durationToRide / 60) / distance; 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) { bool _isAirport(String s) {

View File

@@ -1429,6 +1429,17 @@ class MyTranslation extends Translations {
"الآن حرك الخريطة إلى نقطة الانطلاق الخاصة بك", "الآن حرك الخريطة إلى نقطة الانطلاق الخاصة بك",
'Move map to your pickup point': '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 set start location': "حرك الخريطة لتعيين موقع الانطلاق",
'Move map to your work location': "حرك الخريطة إلى موقع عملك", 'Move map to your work location': "حرك الخريطة إلى موقع عملك",
'Move map to your home location': "حرك الخريطة إلى موقع منزلك", 'Move map to your home location': "حرك الخريطة إلى موقع منزلك",

View File

@@ -13,10 +13,11 @@ import 'dart:ui';
import '../../../constant/info.dart'; import '../../../constant/info.dart';
import '../../../controller/functions/tts.dart'; import '../../../controller/functions/tts.dart';
import '../../../controller/home/map_passenger_controller.dart'; import '../../../controller/home/map_passenger_controller.dart';
import '../../../print.dart';
import '../../widgets/mydialoug.dart'; import '../../widgets/mydialoug.dart';
// --- CarType class (Unchanged) --- // ─────────────────────────────────────────────────────────────────────────────
// CAR TYPE MODEL
// ─────────────────────────────────────────────────────────────────────────────
class CarType { class CarType {
final String carType; final String carType;
final String carDetail; final String carDetail;
@@ -27,7 +28,6 @@ class CarType {
{required this.carType, required this.carDetail, required this.image}); {required this.carType, required this.carDetail, required this.image});
} }
// --- List of Car Types (Unchanged) ---
List<CarType> carTypes = [ List<CarType> carTypes = [
CarType( CarType(
carType: 'Fixed Price', carType: 'Fixed Price',
@@ -55,7 +55,9 @@ List<CarType> carTypes = [
image: 'assets/images/roundtrip.png'), image: 'assets/images/roundtrip.png'),
]; ];
// --- Main Widget --- // ─────────────────────────────────────────────────────────────────────────────
// MAIN WIDGET
// ─────────────────────────────────────────────────────────────────────────────
class CarDetailsTypeToChoose extends StatelessWidget { class CarDetailsTypeToChoose extends StatelessWidget {
CarDetailsTypeToChoose({super.key}); CarDetailsTypeToChoose({super.key});
final textToSpeechController = Get.put(TextToSpeechController()); final textToSpeechController = Get.put(TextToSpeechController());
@@ -82,152 +84,214 @@ class CarDetailsTypeToChoose extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// Main Bottom Sheet Design
return Positioned( return Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: Container( child: ClipRRect(
decoration: BoxDecoration( borderRadius: const BorderRadius.only(
color: AppColor topLeft: Radius.circular(28),
.secondaryColor, // Solid background for better performance topRight: Radius.circular(28),
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: Column( child: BackdropFilter(
mainAxisSize: MainAxisSize.min, filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
children: [ child: Container(
// Drag Handle decoration: BoxDecoration(
Center( color: AppColor.secondaryColor.withAlpha(240),
child: Container( borderRadius: const BorderRadius.only(
width: 50, topLeft: Radius.circular(28),
height: 5, topRight: Radius.circular(28),
margin: const EdgeInsets.symmetric(vertical: 12), ),
decoration: BoxDecoration( boxShadow: [
color: Colors.grey.withOpacity(0.3), BoxShadow(
borderRadius: BorderRadius.circular(10), 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) // ── Header ───────────────────────────────────────────
_buildModernHeader(controller), _buildHeader(controller),
// Warning Message (if any) // ── Negative Balance Warning ─────────────────────────
_buildNegativeBalanceWarning(controller), _buildNegativeBalanceWarning(controller),
// Car List // ── Car Selection List ───────────────────────────────
SizedBox( SizedBox(
height: 165, // Fixed height for consistency height: 170,
child: ListView.separated( child: ListView.builder(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: padding: const EdgeInsets.fromLTRB(20, 8, 20, 12),
const EdgeInsets.symmetric(horizontal: 20, vertical: 10), itemCount: carTypes.length,
itemCount: carTypes.length, itemBuilder: (context, index) {
separatorBuilder: (context, index) => final carType = carTypes[index];
const SizedBox(width: 12), final isSelected = controller.selectedIndex == index;
itemBuilder: (context, index) { return Padding(
final carType = carTypes[index]; padding: EdgeInsets.only(
final isSelected = controller.selectedIndex == index; right: index < carTypes.length - 1 ? 10 : 0),
return _buildVerticalCarCard( child: _buildCarCard(
context, controller, carType, isSelected, index); 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 --- // ═══════════════════════════════════════════════════════════════════════════
// HEADER
Widget _buildModernHeader(MapPassengerController controller) { // ═══════════════════════════════════════════════════════════════════════════
Widget _buildHeader(MapPassengerController controller) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5), padding: const EdgeInsets.fromLTRB(22, 4, 22, 8),
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Row(
child: Text( children: [
'Choose your ride'.tr, // Title
style: AppStyle.headTitle.copyWith( Expanded(
fontSize: 20, child: Text(
fontWeight: FontWeight.w800, 'Choose your ride'.tr,
letterSpacing: 0.5), 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),
),
),
],
), ),
const SizedBox(height: 10),
// Trip Info Pill // Trip Stats Row
Container( Row(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), children: [
decoration: BoxDecoration( // Distance chip
color: AppColor.primaryColor.withOpacity(0.08), _buildStatChip(
borderRadius: BorderRadius.circular(20), icon: Icons.route_rounded,
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)), value:
), '${controller.distance.toStringAsFixed(1)} ${'KM'.tr}',
child: Row( color: AppColor.primaryColor,
mainAxisSize: MainAxisSize.min, ),
children: [ const SizedBox(width: 8),
Icon(Icons.directions_car_filled_outlined, // Duration chip
size: 16, color: AppColor.primaryColor), _buildStatChip(
const SizedBox(width: 6), icon: Icons.schedule_rounded,
Text( value: controller.hours > 0
'${controller.distance.toStringAsFixed(1)} ${'KM'.tr}', ? '${controller.hours}h ${controller.minutes}m'
style: AppStyle.subtitle.copyWith( : '${controller.minutes} ${'min'.tr}',
fontSize: 13, color: const Color(0xFF6366F1), // Indigo
fontWeight: FontWeight.bold, ),
color: AppColor.primaryColor), const Spacer(),
), // Price preview for selected car
if (controller.selectedIndex >= 0 &&
controller.selectedIndex < carTypes.length)
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 8), padding:
height: 12, const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
width: 1, decoration: BoxDecoration(
color: Colors.grey.shade400), gradient: LinearGradient(
Icon(Icons.access_time_filled_rounded, colors: [
size: 16, color: AppColor.primaryColor), AppColor.primaryColor,
const SizedBox(width: 6), AppColor.primaryColor.withAlpha(200),
Text( ],
controller.hours > 0 ),
? '${controller.hours}h ${controller.minutes}m' borderRadius: BorderRadius.circular(20),
: '${controller.minutes} min', ),
style: AppStyle.subtitle.copyWith( child: Text(
'${_getPassengerPriceText(carTypes[controller.selectedIndex], controller)} ${'SYP'.tr}',
style: const TextStyle(
color: Colors.white,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: AppColor.primaryColor), ),
),
), ),
], ],
), ),
)
], ],
), ),
); );
} }
Widget _buildVerticalCarCard( Widget _buildStatChip(
BuildContext context, {required IconData icon, required String value, required Color color}) {
MapPassengerController controller, return Container(
CarType carType, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
bool isSelected, decoration: BoxDecoration(
int index) { 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( return GestureDetector(
onTap: () { onTap: () {
controller.selectCarFromList(index); controller.selectCarFromList(index);
@@ -235,67 +299,97 @@ class CarDetailsTypeToChoose extends StatelessWidget {
context, controller, carType, textToSpeechController); context, controller, carType, textToSpeechController);
}, },
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeOutCubic,
width: 110, width: 108,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected gradient: isSelected
? AppColor.primaryColor.withOpacity(0.05) ? LinearGradient(
: Colors.white, begin: Alignment.topLeft,
borderRadius: BorderRadius.circular(18), 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( border: Border.all(
color: isSelected color:
? AppColor.primaryColor isSelected ? AppColor.primaryColor : Colors.grey.shade200,
: Colors.grey.withOpacity(0.2),
width: isSelected ? 2.0 : 1.0, width: isSelected ? 2.0 : 1.0,
), ),
boxShadow: isSelected boxShadow: [
? [ BoxShadow(
BoxShadow( color: isSelected
color: AppColor.primaryColor.withOpacity(0.2), ? AppColor.primaryColor.withAlpha(40)
blurRadius: 8, : Colors.black.withAlpha(12),
offset: const Offset(0, 4), blurRadius: isSelected ? 12 : 4,
) offset: const Offset(0, 3),
] ),
: [ ],
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
), ),
child: Stack( child: Stack(
alignment: Alignment.center,
children: [ 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(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.fromLTRB(8, 10, 8, 8),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Image with subtle scaling if selected // Car image
AnimatedScale( AnimatedScale(
scale: isSelected ? 1.1 : 1.0, scale: isSelected ? 1.12 : 1.0,
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 300),
child: Image.asset( child: Image.asset(
carType.image, carType.image,
height: 50, height: 48,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 10),
// Car Type Text // Car name
FittedBox( FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
carType.carType.tr, carType.carType.tr,
style: AppStyle.subtitle.copyWith( style: TextStyle(
fontWeight: fontWeight:
isSelected ? FontWeight.w800 : FontWeight.w600, isSelected ? FontWeight.w800 : FontWeight.w600,
fontSize: 14, fontSize: 13,
color: color: isSelected
isSelected ? AppColor.primaryColor : Colors.black87, ? AppColor.primaryColor
: Colors.grey.shade800,
), ),
maxLines: 1, maxLines: 1,
), ),
@@ -303,23 +397,27 @@ class CarDetailsTypeToChoose extends StatelessWidget {
const SizedBox(height: 6), const SizedBox(height: 6),
// Price Tag // Price tag
Container( Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 8, vertical: 4), horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? AppColor.primaryColor ? AppColor.primaryColor
: Colors.grey.shade100, : Colors.grey.shade50,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(10),
border: isSelected
? null
: Border.all(color: Colors.grey.shade200),
), ),
child: FittedBox( child: FittedBox(
child: Text( child: Text(
'${_getPassengerPriceText(carType, controller)} ${'SYP'.tr}', '${_getPassengerPriceText(carType, controller)} ${'SYP'.tr}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
color: isSelected ? Colors.white : Colors.black87, 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( Widget _buildPromoButton(
BuildContext context, MapPassengerController controller) { BuildContext context, MapPassengerController controller) {
if (controller.promoTaken) return const SizedBox.shrink(); if (controller.promoTaken) return const SizedBox.shrink();
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 5), padding: const EdgeInsets.fromLTRB(20, 6, 20, 4),
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => _showPromoCodeDialog(context, controller), onTap: () => _showPromoCodeDialog(context, controller),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), padding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.05), gradient: LinearGradient(
colors: [
Colors.amber.shade50,
Colors.orange.shade50,
],
),
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.grey.withOpacity(0.2)), border: Border.all(color: Colors.amber.shade200),
), ),
child: Row( child: Row(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(7),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.1), color: Colors.amber.shade100,
shape: BoxShape.circle), shape: BoxShape.circle,
child: Icon(Icons.confirmation_number_outlined, ),
color: AppColor.primaryColor, size: 20), child: Icon(Icons.percent_rounded,
color: Colors.amber.shade800, size: 16),
), ),
const SizedBox(width: 14), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Promo Code'.tr, 'Have a Promo Code?'.tr,
style: AppStyle.subtitle.copyWith( style: TextStyle(
fontSize: 14, fontWeight: FontWeight.bold), fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.amber.shade900,
),
), ),
Text( Text(
'Have a promo code?'.tr, 'Tap to apply your discount'.tr,
style: AppStyle.subtitle.copyWith( style: TextStyle(
fontSize: 12, color: Colors.grey.shade600), fontSize: 11, color: Colors.amber.shade700),
), ),
], ],
), ),
), ),
Icon(Icons.arrow_forward_ios_rounded, Container(
size: 16, color: Colors.grey.shade400) 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) { Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
final passengerWallet = final passengerWallet =
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0; double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
if (passengerWallet < 0.0) { if (passengerWallet < 0.0) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.redColor.withOpacity(0.1), color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.redColor.withOpacity(0.3)), border: Border.all(color: Colors.red.shade200),
), ),
child: Row( child: Row(
children: [ children: [
Icon(Icons.info_outline_rounded, Container(
color: AppColor.redColor, size: 24), padding: const EdgeInsets.all(6),
const SizedBox(width: 12), 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( Expanded(
child: Text( child: Text(
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.', '${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.',
style: AppStyle.subtitle.copyWith( style: TextStyle(
color: AppColor.redColor, color: Colors.red.shade800,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 13), fontSize: 12,
),
), ),
), ),
], ],
@@ -437,8 +551,9 @@ class CarDetailsTypeToChoose extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// --- Logic Helpers (Copied from your previous code to ensure functionality) --- // ═══════════════════════════════════════════════════════════════════════════
// PRICING HELPERS (Unchanged logic)
// ═══════════════════════════════════════════════════════════════════════════
String _getPassengerPriceText( String _getPassengerPriceText(
CarType carType, MapPassengerController mapPassengerController) { CarType carType, MapPassengerController mapPassengerController) {
double rawPrice; double rawPrice;
@@ -476,8 +591,9 @@ class CarDetailsTypeToChoose extends StatelessWidget {
return formatter.format(roundedPrice); return formatter.format(roundedPrice);
} }
// --- Dialogs (Styled consistently) --- // ═══════════════════════════════════════════════════════════════════════════
// DIALOGS
// ═══════════════════════════════════════════════════════════════════════════
void _showPromoCodeDialog( void _showPromoCodeDialog(
BuildContext context, MapPassengerController controller) { BuildContext context, MapPassengerController controller) {
Get.dialog( 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, alignment: Alignment.topCenter,
children: [ children: [
Container( Container(
margin: const EdgeInsets.only(top: 60), margin: const EdgeInsets.only(top: 55),
padding: const EdgeInsets.fromLTRB(24, 70, 24, 24), padding: const EdgeInsets.fromLTRB(24, 65, 24, 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.secondaryColor, color: AppColor.secondaryColor,
borderRadius: BorderRadius.circular(28.0), borderRadius: BorderRadius.circular(28.0),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(30),
blurRadius: 30,
offset: const Offset(0, 10),
),
],
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -581,7 +704,10 @@ class CarDetailsTypeToChoose extends StatelessWidget {
children: [ children: [
Text( Text(
carType.carType.tr, carType.carType.tr,
style: AppStyle.headTitle.copyWith(fontSize: 22), style: AppStyle.headTitle.copyWith(
fontSize: 22,
fontWeight: FontWeight.w800,
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
InkWell( InkWell(
@@ -589,38 +715,75 @@ class CarDetailsTypeToChoose extends StatelessWidget {
_getCarDescription( _getCarDescription(
mapPassengerController, carType)), mapPassengerController, carType)),
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
child: Padding( child: Container(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: AppColor.primaryColor.withAlpha(20),
shape: BoxShape.circle,
),
child: Icon(Icons.volume_up_rounded, 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( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.05), gradient: LinearGradient(
borderRadius: BorderRadius.circular(16)), 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( child: Text(
_getCarDescription(mapPassengerController, carType), _getCarDescription(mapPassengerController, carType),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: AppStyle.subtitle.copyWith( style: AppStyle.subtitle.copyWith(
color: Colors.black87, color: Colors.grey.shade700,
fontSize: 15, fontSize: 14,
height: 1.4, height: 1.5,
), ),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 20),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () => Get.back(), 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, child: Text('Back'.tr,
style: TextStyle(color: Colors.grey.shade600)), style:
TextStyle(color: Colors.grey.shade600)),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -641,22 +804,38 @@ class CarDetailsTypeToChoose extends StatelessWidget {
], ],
), ),
), ),
// Floating car image
Positioned( Positioned(
top: 0, top: 0,
child: Hero( child: Container(
tag: 'car_${carType.carType}', padding: const EdgeInsets.all(10),
child: Image.asset(carType.image, height: 130), 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( String _getCarDescription(
MapPassengerController mapPassengerController, CarType carType) { MapPassengerController mapPassengerController, CarType carType) {
switch (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 { class BurcMoney extends StatelessWidget {
const BurcMoney({super.key}); const BurcMoney({super.key});
@@ -810,13 +991,13 @@ class BurcMoney extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.redColor.withOpacity(0.8), color: AppColor.redColor.withAlpha(220),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(14),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.red.withAlpha(40),
blurRadius: 4, blurRadius: 12,
offset: const Offset(0, 2), offset: const Offset(0, 4),
), ),
], ],
), ),

View File

@@ -191,20 +191,19 @@ class _ExpandedView extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// ── هيدر "Quick Actions" ───────────────────────────────────────── // ── Header ────────────────────────────────────────────────────────
Padding( Padding(
padding: const EdgeInsets.fromLTRB(18, 14, 12, 8), padding: const EdgeInsets.fromLTRB(18, 14, 12, 8),
child: Row( child: Row(
children: [ children: [
Text( Text(
'Quick Actions'.tr, 'Plan Your Route'.tr,
style: AppStyle.title.copyWith( style: AppStyle.title.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 16, fontSize: 16,
), ),
), ),
const Spacer(), const Spacer(),
// زر إغلاق
GestureDetector( GestureDetector(
onTap: controller.changeMainBottomMenuMap, onTap: controller.changeMainBottomMenuMap,
child: Container( child: Container(
@@ -222,36 +221,236 @@ class _ExpandedView extends StatelessWidget {
), ),
const Divider(height: 1, thickness: 0.5), 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), 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(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: _WhatsAppLinkButton(controller: controller), child: _WhatsAppLinkButton(controller: controller),
@@ -259,7 +458,7 @@ class _ExpandedView extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
// ── Order for someone else ──────────────────────────────────────── // ── Order type ───────────────────────────────────────────────────
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: _OrderTypeButton(controller: controller), 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) { String _getModeTitle(BuildContext context) {
if (controller.isPickingWaypoint) {
return 'Move map to set stop'.tr +
' ${controller.pickingWaypointIndex + 1}'.tr;
}
if (controller.passengerStartLocationFromMap) { if (controller.passengerStartLocationFromMap) {
return 'Move map to your pickup point'.tr; return 'Move map to your pickup point'.tr;
} else if (controller.startLocationFromMap) { } else if (controller.startLocationFromMap) {
@@ -295,6 +551,9 @@ class _MapPickerOverlay extends StatelessWidget {
} }
String _getConfirmLabel(BuildContext context) { String _getConfirmLabel(BuildContext context) {
if (controller.isPickingWaypoint) {
return 'Set as Stop'.tr;
}
if (controller.passengerStartLocationFromMap) { if (controller.passengerStartLocationFromMap) {
return 'Confirm Pickup Location'.tr; return 'Confirm Pickup Location'.tr;
} else if (controller.workLocationFromMap) { } else if (controller.workLocationFromMap) {
@@ -306,6 +565,7 @@ class _MapPickerOverlay extends StatelessWidget {
} }
IconData _getModeIcon() { IconData _getModeIcon() {
if (controller.isPickingWaypoint) return Icons.add_location_alt_rounded;
if (controller.passengerStartLocationFromMap) if (controller.passengerStartLocationFromMap)
return Icons.person_pin_circle_rounded; return Icons.person_pin_circle_rounded;
if (controller.workLocationFromMap) return Icons.work_rounded; if (controller.workLocationFromMap) return Icons.work_rounded;
@@ -314,6 +574,7 @@ class _MapPickerOverlay extends StatelessWidget {
} }
Color _getModeColor() { Color _getModeColor() {
if (controller.isPickingWaypoint) return Colors.orange.shade600;
if (controller.passengerStartLocationFromMap) return Colors.green.shade600; if (controller.passengerStartLocationFromMap) return Colors.green.shade600;
if (controller.workLocationFromMap) return Colors.blue.shade600; if (controller.workLocationFromMap) return Colors.blue.shade600;
if (controller.homeLocationFromMap) return Colors.orange.shade600; if (controller.homeLocationFromMap) return Colors.orange.shade600;
@@ -428,6 +689,8 @@ class _MapPickerOverlay extends StatelessWidget {
controller.startLocationFromMap = false; controller.startLocationFromMap = false;
controller.workLocationFromMap = false; controller.workLocationFromMap = false;
controller.homeLocationFromMap = false; controller.homeLocationFromMap = false;
controller.isPickingWaypoint = false;
controller.pickingWaypointIndex = -1;
// أعد الخريطة لحالتها المنهارة // أعد الخريطة لحالتها المنهارة
if (!controller.isMainBottomMenuMap) { if (!controller.isMainBottomMenuMap) {
controller.isMainBottomMenuMap = true; controller.isMainBottomMenuMap = true;
@@ -498,6 +761,24 @@ class _MapPickerOverlay extends StatelessWidget {
controller.newMyLocation.longitude, 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.clearPolyline();
controller.data = []; controller.data = [];
@@ -593,21 +874,35 @@ class _MapPickerOverlay extends StatelessWidget {
controller.update(); controller.update();
// ─── 3. العمليات الـ async بعد حفظ الوجهة بأمان ────────────────────── // ─── 3. العمليات الـ async بعد حفظ الوجهة بأمان ──────────────────────
if (!controller.isAnotherOreder) { try {
// تحريك الكاميرا لموقع الراكب ليختار منه نقطة الانطلاق if (controller.isAnotherOreder) {
// ملاحظة: هذا يُغير newMyLocation - لكن myDestination محفوظ بأمان ✅ // ✅ "Order for someone else": move camera to the OTHER person's
await controller.mapController?.animateCamera( // start location (set via the start search form)
CameraUpdate.newLatLng(LatLng( await controller.mapController?.animateCamera(
controller.passengerLocation.latitude, CameraUpdate.newLatLng(LatLng(
controller.passengerLocation.longitude, 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. إشعار المستخدم ────────────────────────────────────────────── // ─── 4. إشعار المستخدم ──────────────────────────────────────────────
Get.snackbar( Get.snackbar(
'Destination Set'.tr, '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, backgroundColor: Colors.green.shade600,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.TOP, 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 { class _RecentPlaceChip extends StatelessWidget {
final MapPassengerController controller; final MapPassengerController controller;