feat: refactor financial wallet UI components and add offline map service support

This commit is contained in:
Hamza-Ayed
2026-04-21 00:35:30 +03:00
parent 4293d20561
commit b92db3bb39
99 changed files with 22888 additions and 27387 deletions

View File

@@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:sefer_driver/controller/home/payment/captain_wallet_controller.dart';
import '../../../../../constant/style.dart';
import '../../../../../controller/functions/encrypt_decrypt.dart';
import '../../../../widgets/elevated_btn.dart';
import '../../../../../controller/home/captin/home_captain_controller.dart';
@@ -91,45 +90,56 @@ class ConnectWidget extends StatelessWidget {
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: homeCaptainController.isActive
? [Colors.green.shade400, Colors.green.shade700]
: [Colors.grey.shade400, Colors.grey.shade700],
? [const Color(0xFF00C853), const Color(0xFF00E676)]
: [Colors.grey.shade600, Colors.grey.shade400],
),
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: homeCaptainController.isActive
? Colors.green.withOpacity(0.3)
: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 2),
color: (homeCaptainController.isActive
? const Color(0xFF00C853)
: Colors.grey)
.withOpacity(0.4),
spreadRadius: 0,
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: CupertinoButton(
onPressed: homeCaptainController.onButtonSelected,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
horizontal: 20, vertical: 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
homeCaptainController.isActive
? CupertinoIcons.check_mark_circled_solid
: CupertinoIcons.circle,
color: Colors.white,
size: 24,
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
homeCaptainController.isActive
? Icons.power_settings_new_rounded
: Icons.power_off_rounded,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 8),
const SizedBox(width: 10),
Text(
homeCaptainController.isActive
? 'Connected'.tr
: 'Not Connected'.tr,
? 'Online'.tr
: 'Offline'.tr,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],

View File

@@ -2,7 +2,6 @@ import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/firebase/local_notification.dart';
import 'package:sefer_driver/main.dart';
import 'package:sefer_driver/views/auth/captin/otp_page.dart';
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
import 'package:flutter/material.dart';
@@ -13,7 +12,6 @@ import 'package:sefer_driver/views/widgets/mydialoug.dart';
import '../../../../../constant/colors.dart';
import '../../../../../constant/links.dart';
import '../../../../../controller/firebase/firbase_messge.dart';
import '../../../../../controller/firebase/notification_service.dart';
import '../../../../../controller/functions/crud.dart';
import '../../../../../controller/home/captin/order_request_controller.dart';
@@ -21,319 +19,279 @@ import '../../../../../controller/home/navigation/navigation_view.dart';
import '../../../../../print.dart';
import '../../../../Rate/ride_calculate_driver.dart';
// ─────────────────────────────────────────────
// Design Tokens (Responsive)
// ─────────────────────────────────────────────
class _T {
static Color surface(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark
? const Color(0xFF16213E)
: Colors.white;
static const Color accent = Color(0xFFF0A500);
static const Color blue = Color(0xFF3498DB);
static Color border(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark
? const Color(0xFF2A2A4A)
: Colors.grey.withOpacity(0.2);
static const double radius = 14.0;
}
// ─────────────────────────────────────────────
// Left Side Menu
// ─────────────────────────────────────────────
/// Returns the vertical icon column anchored to the left-center of the map.
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
final firebaseMessagesController =
Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
return GetBuilder<HomeCaptainController>(
builder: (controller) => Positioned(
bottom: Get.height * .2,
left: 6,
child: Column(
children: [
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: Builder(builder: (context) {
return IconButton(
onPressed: () async {
await checkForPendingOrderFromServer();
box.read(BoxName.rideArgumentsFromBackground) != 'failure'
? Get.to(() => PassengerLocationMapPage(),
arguments:
box.read(BoxName.rideArgumentsFromBackground))
: MyDialog().getDialog(
'Ride info'.tr,
'you dont have accepted ride'.tr,
() {
Get.back();
},
);
// 'box.read(BoxName.rideArgumentsFromBackground): ${box.read(BoxName.rideArgumentsFromBackground)}');
},
icon: Icon(
Icons.directions_car_rounded,
size: 29,
color:
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
? AppColor.redColor
: AppColor.greenColor,
),
);
}),
),
const SizedBox(
height: 5,
),
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: IconButton(
onLongPress: () {
box.write(BoxName.statusDriverLocation, 'off');
},
onPressed: () {
// NotificationController1()
// .showNotification('Sefer Driver'.tr, ''.tr, '', '');
final now = DateTime.now();
DateTime? lastRequestTime =
box.read(BoxName.lastTimeStaticThrottle);
if (lastRequestTime == null ||
now.difference(lastRequestTime).inMinutes >= 2) {
// Update the last request time to now
lastRequestTime = now;
box.write(BoxName.lastTimeStaticThrottle, lastRequestTime);
// Navigate to the RideCalculateDriver page
Get.to(() => RideCalculateDriver());
} else {
// Optionally show a message or handle the throttling case
final minutesLeft =
2 - now.difference(lastRequestTime).inMinutes;
// Get.snackbar(
// '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
// '');
NotificationController().showNotification(
'Intaleq Driver'.tr,
'${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
'ding',
'');
}
},
icon: const Icon(
FontAwesome5.chart_bar,
size: 29,
color: AppColor.blueColor,
builder: (ctrl) => Positioned(
// Place just above the bottom status bar
bottom: 100,
left: 10,
child: Builder(builder: (context) {
return Container(
decoration: BoxDecoration(
color: _T.surface(context),
borderRadius: BorderRadius.circular(_T.radius),
border: Border.all(color: _T.border(context)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(2, 4),
),
),
],
),
const SizedBox(
height: 5,
),
// Platform.isAndroid
// ?
int.parse(box.read(BoxName.carYear).toString()) > 2023
? AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: Builder(builder: (context) {
return IconButton(
onPressed: () async {
// mySnakeBarError('ad');
Get.to(() => const VipOrderPage());
},
icon: const Icon(
Octicons.watch,
size: 29,
color: AppColor.blueColor,
),
padding: const EdgeInsets.symmetric(vertical: 6),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 1. Active Ride shortcut ──────────
_MenuIcon(
icon: Icons.directions_car_rounded,
color:
box.read(BoxName.rideArgumentsFromBackground) == 'failure'
? Colors.red.shade400
: Colors.green.shade400,
tooltip: 'Active Ride'.tr,
onTap: () async {
await checkForPendingOrderFromServer();
if (box.read(BoxName.rideArgumentsFromBackground) != 'failure') {
Get.to(
() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArgumentsFromBackground),
);
}),
)
: const SizedBox(),
// const SizedBox(
// height: 5,
// ),
AnimatedContainer(
duration: const Duration(microseconds: 200),
width: controller.widthMapTypeAndTraffic,
decoration: BoxDecoration(
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.blueColor),
borderRadius: BorderRadius.circular(15)),
child: Builder(builder: (context) {
return IconButton(
onPressed: () async {
box.remove(BoxName.agreeTerms);
Get.to(() => const NavigationView());
// box.write(BoxName.statusDriverLocation, 'off');
} else {
MyDialog().getDialog(
'Ride info'.tr,
'you dont have accepted ride'.tr,
() => Get.back(),
);
}
},
icon: const Icon(
FontAwesome5.map,
size: 29,
color: AppColor.blueColor,
),
);
}),
),
// AnimatedContainer(
// duration: const Duration(microseconds: 200),
// width: controller.widthMapTypeAndTraffic,
// decoration: BoxDecoration(
// color: AppColor.secondaryColor,
// border: Border.all(color: AppColor.blueColor),
// borderRadius: BorderRadius.circular(15)),
// child: Builder(builder: (context) {
// return IconButton(
// onPressed: () async {
// NotificationService.sendNotification(
// target: 'service', // الإرسال لجميع المشتركين في "service"
// title: 'طلب خدمة جديد',
// body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
// isTopic: true,
// category: 'new_service_request', // فئة توضح نوع الإشعار
// );
// },
// icon: const Icon(
// FontAwesome5.grin_tears,
// size: 29,
// color: AppColor.blueColor,
// ),
// );
// }),
// ),
),
const SizedBox(
height: 5,
_Divider(context),
// ── 2. Earnings Chart ────────────────
_MenuIcon(
icon: FontAwesome5.chart_bar,
color: _T.blue,
tooltip: 'Earnings'.tr,
onTap: () {
final now = DateTime.now();
DateTime? lastTime = box.read(BoxName.lastTimeStaticThrottle);
if (lastTime == null ||
now.difference(lastTime).inMinutes >= 2) {
box.write(BoxName.lastTimeStaticThrottle, now);
Get.to(() => RideCalculateDriver());
} else {
final left = 2 - now.difference(lastTime).inMinutes;
NotificationController().showNotification(
'Intaleq Driver'.tr,
'${'Please wait'.tr} $left ${"minutes before trying again.".tr}',
'ding',
'',
);
}
},
onLongPress: () =>
box.write(BoxName.statusDriverLocation, 'off'),
),
// ── 3. VIP Orders (2023+ cars only) ──
if (int.tryParse(box.read(BoxName.carYear).toString()) != null &&
int.parse(box.read(BoxName.carYear).toString()) > 2023) ...[
_Divider(context),
_MenuIcon(
icon: Octicons.watch,
color: _T.accent,
tooltip: 'VIP Orders'.tr,
onTap: () => Get.to(() => const VipOrderPage()),
),
],
],
),
],
),
);
}),
),
);
}
// ─────────────────────────────────────────────
// Reusable sub-widgets
// ─────────────────────────────────────────────
class _MenuIcon extends StatelessWidget {
final IconData icon;
final Color color;
final String tooltip;
final VoidCallback onTap;
final VoidCallback? onLongPress;
const _MenuIcon({
required this.icon,
required this.color,
required this.tooltip,
required this.onTap,
this.onLongPress,
});
@override
Widget build(BuildContext context) => Tooltip(
message: tooltip,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(10),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: Icon(icon, color: color, size: 26),
),
),
);
}
/// Thin separator between icons
class _Divider extends StatelessWidget {
final BuildContext context;
const _Divider(this.context);
@override
Widget build(BuildContext context) => Container(
height: 1,
width: 32,
margin: const EdgeInsets.symmetric(horizontal: 6),
color: _T.border(this.context),
);
}
// ─────────────────────────────────────────────
// Server Helpers (unchanged logic)
// ─────────────────────────────────────────────
Future<void> checkForPendingOrderFromServer() async {
bool _isProcessingOrder = false;
if (_isProcessingOrder) return;
bool isProcessing = false;
if (isProcessing) return;
final driverId = box.read(BoxName.driverID)?.toString();
if (driverId == null) return; // Can't check without a driver ID
if (driverId == null) return;
_isProcessingOrder = true; // Lock
isProcessing = true;
try {
// You need to create this CRUD method
var response = await CRUD().post(
link: AppLink.getArgumentAfterAppliedFromBackground,
payload: {'driver_id': driverId},
);
Log.print('response: ${response}');
Log.print('response: $response');
// Assuming the server returns order data if found, or 'failure'/'none' if not
if (response['status'] == 'success') {
final Map<String, dynamic> orderInfoFromServer = response['message'];
final Map<String, dynamic> rideArguments =
_transformServerDataToAppArguments(orderInfoFromServer);
// 2. Build the new arguments map, matching your Flutter structure
final Map<String, dynamic> orderInfo = response['message'];
final Map<String, dynamic> rideArgs =
_transformServerDataToAppArguments(orderInfo);
/////////////
final customerToken = (response)['message']['token_passenger'];
final orderId = (response)['message']['ride_id'].toString();
box.write(BoxName.rideArgumentsFromBackground, rideArguments);
final customerToken = response['message']['token_passenger'];
final orderId = response['message']['ride_id'].toString();
box.write(BoxName.rideArgumentsFromBackground, rideArgs);
box.write(BoxName.statusDriverLocation, 'on');
box.write(BoxName.rideStatus, 'Apply');
Get.put(OrderRequestController()).changeApplied();
// MyDialog().getDialog(orderId.toString(), customerToken, () {});
// Now proceed with the UI flow
// _sendAcceptanceNotification(customerToken, orderId.toString());
// await _bringAppToForegroundAndNavigate(orderId);
Get.to(() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArgumentsFromBackground));
Get.to(
() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArgumentsFromBackground),
);
} else {
box.write(BoxName.rideArgumentsFromBackground, 'failure');
}
} catch (e) {
} catch (_) {
// silent
} finally {
_isProcessingOrder = false; // Release lock
isProcessing = false;
}
}
Map<String, dynamic> _transformServerDataToAppArguments(
Map<String, dynamic> serverData) {
// Helper function to safely get and convert values to String
String _getString(String key, [String defaultValue = 'unknown']) {
// serverData[key] might be an int, double, or string. .toString() handles all.
// If it's null, use the default value.
return serverData[key]?.toString() ?? defaultValue;
}
Map<String, dynamic> d) {
String s(String key, [String def = 'unknown']) => d[key]?.toString() ?? def;
return {
'passengerLocation': _getString('passenger_location'),
'passengerDestination': _getString('passenger_destination'),
'Duration': _getString('duration'),
'totalCost': _getString('total_cost'),
'Distance': _getString('distance'),
'name': _getString('name'),
'phone': _getString('phone'),
'email': _getString('email'),
'tokenPassenger': _getString('token_passenger'),
'direction': _getString('direction_url'),
'DurationToPassenger': _getString('duration_to_passenger'),
'rideId': _getString('ride_id'),
'passengerId': _getString('passenger_id'),
'driverId': _getString('driver_id'),
'durationOfRideValue': _getString('duration_of_ride'),
'paymentAmount': _getString('payment_amount'),
'paymentMethod': _getString('payment_method'),
'passengerWalletBurc': _getString('passenger_wallet_burc'),
'timeOfOrder': _getString('time_of_order'),
'totalPassenger': _getString('total_passenger'),
'carType': _getString('car_type'),
'kazan': _getString('kazan'),
'startNameLocation': _getString('start_name_location'),
'endNameLocation': _getString('end_name_location'),
// --- Special Handling ---
// Steps (handle null values by providing an empty string)
'step0': _getString('step0'),
'step1': _getString('step1'),
'step2': _getString('step2'),
'step3': _getString('step3'),
'step4': _getString('step4'),
// Boolean conversion (1/0 from server to 'true'/'false' string for the app)
'WalletChecked': (serverData['wallet_checked'] == 1).toString(),
// Logic-based conversion for isHaveSteps
// Your app's `rideArguments` expects 'startEnd', so we provide that if has_steps is 1.
// You might need to adjust this logic if 'haveSteps' is also a possibility.
'isHaveSteps': (serverData['has_steps'] == 1)
? 'startEnd'
: 'noSteps', // Providing a default
'passengerLocation': s('passenger_location'),
'passengerDestination': s('passenger_destination'),
'Duration': s('duration'),
'totalCost': s('total_cost'),
'Distance': s('distance'),
'name': s('name'),
'phone': s('phone'),
'email': s('email'),
'tokenPassenger': s('token_passenger'),
'direction': s('direction_url'),
'DurationToPassenger': s('duration_to_passenger'),
'rideId': s('ride_id'),
'passengerId': s('passenger_id'),
'driverId': s('driver_id'),
'durationOfRideValue': s('duration_of_ride'),
'paymentAmount': s('payment_amount'),
'paymentMethod': s('payment_method'),
'passengerWalletBurc': s('passenger_wallet_burc'),
'timeOfOrder': s('time_of_order'),
'totalPassenger': s('total_passenger'),
'carType': s('car_type'),
'kazan': s('kazan'),
'startNameLocation': s('start_name_location'),
'endNameLocation': s('end_name_location'),
'step0': s('step0'),
'step1': s('step1'),
'step2': s('step2'),
'step3': s('step3'),
'step4': s('step4'),
'WalletChecked': (d['wallet_checked'] == 1).toString(),
'isHaveSteps': (d['has_steps'] == 1) ? 'startEnd' : 'noSteps',
};
}
void _sendAcceptanceNotification(String? customerToken, rideId) {
try {
if (customerToken == null) return;
void _sendAcceptanceNotification(String? customerToken, dynamic rideId) {
if (customerToken == null || customerToken.isEmpty) return;
List<String> bodyToPassenger = [
box.read(BoxName.driverID).toString(),
box.read(BoxName.nameDriver).toString(),
box.read(BoxName.tokenDriver).toString(),
rideId.toString()
];
List<String> body = [
box.read(BoxName.driverID).toString(),
box.read(BoxName.nameDriver).toString(),
box.read(BoxName.tokenDriver).toString(),
rideId.toString(),
];
// Safely check for customer token
final String? token = customerToken;
if (token != null && token.isNotEmpty) {
NotificationService.sendNotification(
target: token.toString(),
title: 'Accepted Ride'.tr,
body: 'your ride is Accepted'.tr,
isTopic: false, // Important: this is a token
tone: 'start',
driverList: bodyToPassenger, category: 'Accepted Ride',
);
} else {}
} catch (e) {}
NotificationService.sendNotification(
target: customerToken,
title: 'Accepted Ride'.tr,
body: 'your ride is Accepted'.tr,
isTopic: false,
tone: 'start',
driverList: body,
category: 'Accepted Ride',
);
}

View File

@@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:math';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
class ZonesController extends GetxController {
Map<String, List<LatLng>> generateZoneMap(